@exulu/backend 1.60.0 → 1.61.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/dist/index.cjs CHANGED
@@ -606,15 +606,15 @@ var init_check_record_access = __esm({
606
606
  "use strict";
607
607
  init_cjs_shims();
608
608
  checkRecordAccessCache = /* @__PURE__ */ new Map();
609
- checkRecordAccess = async (record, request, user) => {
609
+ checkRecordAccess = async (record, request2, user) => {
610
610
  const setRecordAccessCache = (hasAccess2) => {
611
- checkRecordAccessCache.set(`${record.id}-${request}-${user?.id}`, {
611
+ checkRecordAccessCache.set(`${record.id}-${request2}-${user?.id}`, {
612
612
  hasAccess: hasAccess2,
613
613
  expiresAt: new Date(Date.now() + 1e3 * 60 * 1)
614
614
  // 1 minute
615
615
  });
616
616
  };
617
- const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request}-${user?.id}`);
617
+ const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request2}-${user?.id}`);
618
618
  if (cachedAccess && cachedAccess.expiresAt > /* @__PURE__ */ new Date()) {
619
619
  return cachedAccess.hasAccess;
620
620
  }
@@ -626,7 +626,7 @@ var init_check_record_access = __esm({
626
626
  const isAdmin = user ? user.super_admin : false;
627
627
  const isApi = user ? user.type === "api" : false;
628
628
  const isAdminApi = isApi && (!user.scope_mode || user.scope_mode === "admin");
629
- const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
629
+ const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request2 === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
630
630
  let hasAccess = "none";
631
631
  if (isPublic || isCreator || isAdmin || isAdminApi || isAgentsScopedApi) {
632
632
  setRecordAccessCache(true);
@@ -638,7 +638,7 @@ var init_check_record_access = __esm({
638
638
  return false;
639
639
  }
640
640
  hasAccess = record.RBAC?.users?.find((x) => x.id === user.id)?.rights || "none";
641
- if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
641
+ if (!hasAccess || hasAccess === "none" || hasAccess !== request2) {
642
642
  console.error(
643
643
  `[EXULU] Your current user ${user.id} does not have access to this record, current access type is: ${hasAccess}.`
644
644
  );
@@ -655,7 +655,7 @@ var init_check_record_access = __esm({
655
655
  return false;
656
656
  }
657
657
  hasAccess = record.RBAC?.roles?.find((x) => x.id === user.role?.id)?.rights || "none";
658
- if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
658
+ if (!hasAccess || hasAccess === "none" || hasAccess !== request2) {
659
659
  console.error(
660
660
  `[EXULU] Your current role ${user.role?.name} does not have access to this record, current access type is: ${hasAccess}.`
661
661
  );
@@ -1520,7 +1520,7 @@ var init_uppy = __esm({
1520
1520
  if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
1521
1521
  if (attempt < maxRetries) {
1522
1522
  const backoffMs = Math.pow(2, attempt) * 1e3;
1523
- await new Promise((resolve6) => setTimeout(resolve6, backoffMs));
1523
+ await new Promise((resolve7) => setTimeout(resolve7, backoffMs));
1524
1524
  s3Client = void 0;
1525
1525
  getS3Client(config);
1526
1526
  continue;
@@ -3291,7 +3291,7 @@ async function withRetry(generateFn, maxRetries = 3) {
3291
3291
  if (attempt === maxRetries) {
3292
3292
  throw error;
3293
3293
  }
3294
- await new Promise((resolve6) => setTimeout(resolve6, Math.pow(2, attempt) * 1e3));
3294
+ await new Promise((resolve7) => setTimeout(resolve7, Math.pow(2, attempt) * 1e3));
3295
3295
  }
3296
3296
  }
3297
3297
  throw lastError;
@@ -4087,7 +4087,7 @@ var init_schemas = __esm({
4087
4087
  });
4088
4088
 
4089
4089
  // src/postgres/core-schema.ts
4090
- var agentMessagesSchema, agentSessionsSchema, skillsSchema, variablesSchema, projectsSchema, agentsSchema, modelsSchema, usersSchema, platformConfigurationsSchema, embedderSettingsSchema, promptLibrarySchema, promptFavoritesSchema, contextPresetsSchema, addCoreFields, coreSchemas;
4090
+ var agentMessagesSchema, agentSessionsSchema, skillsSchema, variablesSchema, projectsSchema, agentsSchema, modelsSchema, usersSchema, platformConfigurationsSchema, embedderSettingsSchema, promptLibrarySchema, promptFavoritesSchema, transcriptionJobsSchema, imageGenerationsSchema, contextPresetsSchema, addCoreFields, coreSchemas;
4091
4091
  var init_core_schema = __esm({
4092
4092
  "src/postgres/core-schema.ts"() {
4093
4093
  "use strict";
@@ -4500,6 +4500,10 @@ var init_core_schema = __esm({
4500
4500
  name: "anthropic_token",
4501
4501
  type: "text"
4502
4502
  },
4503
+ {
4504
+ name: "personal_system_prompt",
4505
+ type: "longText"
4506
+ },
4503
4507
  {
4504
4508
  name: "role",
4505
4509
  type: "uuid"
@@ -4508,6 +4512,7 @@ var init_core_schema = __esm({
4508
4512
  };
4509
4513
  platformConfigurationsSchema = {
4510
4514
  type: "platform_configurations",
4515
+ RBAC: true,
4511
4516
  name: {
4512
4517
  plural: "platform_configurations",
4513
4518
  singular: "platform_configuration"
@@ -4627,6 +4632,60 @@ var init_core_schema = __esm({
4627
4632
  }
4628
4633
  ]
4629
4634
  };
4635
+ transcriptionJobsSchema = {
4636
+ type: "transcription_jobs",
4637
+ name: {
4638
+ plural: "transcription_jobs",
4639
+ singular: "transcription_job"
4640
+ },
4641
+ RBAC: true,
4642
+ fields: [
4643
+ { name: "audio", type: "file" },
4644
+ { name: "title", type: "text" },
4645
+ { name: "status", type: "text", index: true },
4646
+ { name: "whisper_job_id", type: "text" },
4647
+ { name: "raw_segments", type: "json" },
4648
+ { name: "speakers", type: "json" },
4649
+ { name: "language", type: "text" },
4650
+ { name: "duration_seconds", type: "number" },
4651
+ { name: "project_id", type: "uuid", required: false },
4652
+ { name: "target_rights_mode", type: "text", default: "private" },
4653
+ { name: "target_rbac_users", type: "json" },
4654
+ { name: "target_rbac_roles", type: "json" },
4655
+ { name: "saved_item_id", type: "uuid", required: false },
4656
+ { name: "error", type: "text" }
4657
+ ]
4658
+ };
4659
+ imageGenerationsSchema = {
4660
+ type: "image_generations",
4661
+ name: {
4662
+ plural: "image_generations",
4663
+ singular: "image_generation"
4664
+ },
4665
+ // Access is gated by the parent agent_sessions RBAC — rows have no
4666
+ // independent visibility, so no row-level RBAC fields are needed here.
4667
+ RBAC: false,
4668
+ fields: [
4669
+ { name: "session_id", type: "uuid", required: true, index: true },
4670
+ { name: "tool_call_id", type: "text", required: true, index: true },
4671
+ { name: "user_id", type: "number", required: true, index: true },
4672
+ { name: "operation", type: "text", required: true },
4673
+ // 'generate' | 'edit'
4674
+ { name: "model", type: "text", required: true },
4675
+ { name: "prompt", type: "longText", required: true },
4676
+ { name: "applied_style_id", type: "uuid", required: false },
4677
+ { name: "applied_style_markdown", type: "longText", required: false },
4678
+ { name: "size", type: "text", required: false },
4679
+ { name: "quality", type: "text", required: false },
4680
+ { name: "n", type: "number", default: 1 },
4681
+ { name: "reference_image_keys", type: "json", required: false },
4682
+ { name: "mask_image_key", type: "text", required: false },
4683
+ { name: "image_keys", type: "json", required: true },
4684
+ { name: "revised_prompts", type: "json", required: false },
4685
+ { name: "selected", type: "boolean", default: false },
4686
+ { name: "error", type: "text", required: false }
4687
+ ]
4688
+ };
4630
4689
  contextPresetsSchema = {
4631
4690
  type: "context_presets",
4632
4691
  name: {
@@ -4717,7 +4776,9 @@ var init_core_schema = __esm({
4717
4776
  promptLibrarySchema: () => addCoreFields(promptLibrarySchema),
4718
4777
  embedderSettingsSchema: () => addCoreFields(embedderSettingsSchema),
4719
4778
  promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
4720
- contextPresetsSchema: () => addCoreFields(contextPresetsSchema)
4779
+ contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
4780
+ transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
4781
+ imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
4721
4782
  };
4722
4783
  if (license["agent-feedback"]) {
4723
4784
  schemas.feedbackSchema = () => addCoreFields(feedbackSchema);
@@ -7619,7 +7680,11 @@ var init_catalog = __esm({
7619
7680
  supports_vision: !!m.model_info?.supports_vision,
7620
7681
  supports_function_calling: !!m.model_info?.supports_function_calling,
7621
7682
  supports_pdf_input: !!m.model_info?.supports_pdf_input,
7622
- supports_audio_input: !!m.model_info?.supports_audio_input
7683
+ supports_audio_input: !!m.model_info?.supports_audio_input,
7684
+ sizes: Array.isArray(m.model_info?.sizes) ? m.model_info.sizes : null,
7685
+ qualities: Array.isArray(m.model_info?.qualities) ? m.model_info.qualities : null,
7686
+ supports_edit: !!m.model_info?.supports_edit,
7687
+ max_n: typeof m.model_info?.max_n === "number" ? m.model_info.max_n : null
7623
7688
  }));
7624
7689
  _cache = { expiresAt: Date.now() + CACHE_TTL_MS2, items };
7625
7690
  return items.filter((m) => m.type !== "speech_to_text" && m.type !== "text_to_speech");
@@ -10022,7 +10087,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
10022
10087
  );
10023
10088
  if (attempt < retries) {
10024
10089
  const backoffMs = 500 * Math.pow(2, attempt - 1);
10025
- await new Promise((resolve6) => setTimeout(resolve6, backoffMs));
10090
+ await new Promise((resolve7) => setTimeout(resolve7, backoffMs));
10026
10091
  }
10027
10092
  }
10028
10093
  }
@@ -10226,7 +10291,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
10226
10291
  } = await validateWorkflowPayload(data, providers);
10227
10292
  const retries2 = 3;
10228
10293
  let attempts = 0;
10229
- const promise = new Promise(async (resolve6, reject) => {
10294
+ const promise = new Promise(async (resolve7, reject) => {
10230
10295
  while (attempts < retries2) {
10231
10296
  try {
10232
10297
  const messages2 = await processUiMessagesFlow({
@@ -10241,7 +10306,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
10241
10306
  config,
10242
10307
  variables: data.inputs
10243
10308
  });
10244
- resolve6(messages2);
10309
+ resolve7(messages2);
10245
10310
  break;
10246
10311
  } catch (error) {
10247
10312
  console.error(
@@ -10252,7 +10317,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
10252
10317
  if (attempts >= retries2) {
10253
10318
  reject(new Error(error instanceof Error ? error.message : String(error)));
10254
10319
  }
10255
- await new Promise((resolve7) => setTimeout((resolve8) => resolve8(true), 2e3));
10320
+ await new Promise((resolve8) => setTimeout((resolve9) => resolve9(true), 2e3));
10256
10321
  }
10257
10322
  }
10258
10323
  });
@@ -10302,7 +10367,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
10302
10367
  } = await validateEvalPayload(data, providers);
10303
10368
  const retries2 = 3;
10304
10369
  let attempts = 0;
10305
- const promise = new Promise(async (resolve6, reject) => {
10370
+ const promise = new Promise(async (resolve7, reject) => {
10306
10371
  while (attempts < retries2) {
10307
10372
  try {
10308
10373
  const messages2 = await processUiMessagesFlow({
@@ -10316,7 +10381,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
10316
10381
  tools,
10317
10382
  config
10318
10383
  });
10319
- resolve6(messages2);
10384
+ resolve7(messages2);
10320
10385
  break;
10321
10386
  } catch (error) {
10322
10387
  console.error(
@@ -10327,7 +10392,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
10327
10392
  if (attempts >= retries2) {
10328
10393
  reject(new Error(error instanceof Error ? error.message : String(error)));
10329
10394
  }
10330
- await new Promise((resolve7) => setTimeout((resolve8) => resolve8(true), 2e3));
10395
+ await new Promise((resolve8) => setTimeout((resolve9) => resolve9(true), 2e3));
10331
10396
  }
10332
10397
  }
10333
10398
  });
@@ -10802,7 +10867,7 @@ var pollJobResult = async ({
10802
10867
  attempts++;
10803
10868
  const job = await import_bullmq3.Job.fromId(queue.queue, jobId);
10804
10869
  if (!job) {
10805
- await new Promise((resolve6) => setTimeout((resolve7) => resolve7(true), 2e3));
10870
+ await new Promise((resolve7) => setTimeout((resolve8) => resolve8(true), 2e3));
10806
10871
  continue;
10807
10872
  }
10808
10873
  const elapsedTime = Date.now() - startTime;
@@ -10832,7 +10897,7 @@ var pollJobResult = async ({
10832
10897
  console.log(`[EXULU] eval function ${job.id} result: ${result}`);
10833
10898
  break;
10834
10899
  }
10835
- await new Promise((resolve6) => setTimeout(() => resolve6(true), 2e3));
10900
+ await new Promise((resolve7) => setTimeout(() => resolve7(true), 2e3));
10836
10901
  }
10837
10902
  return result;
10838
10903
  };
@@ -10932,7 +10997,7 @@ var processUiMessagesFlow = async ({
10932
10997
  label: agent.name,
10933
10998
  trigger: "agent"
10934
10999
  };
10935
- messageHistory = await new Promise(async (resolve6, reject) => {
11000
+ messageHistory = await new Promise(async (resolve7, reject) => {
10936
11001
  const startTime = Date.now();
10937
11002
  try {
10938
11003
  const result = await provider.generateStream({
@@ -11010,7 +11075,7 @@ var processUiMessagesFlow = async ({
11010
11075
  })
11011
11076
  ] : []
11012
11077
  ]);
11013
- resolve6({
11078
+ resolve7({
11014
11079
  messages,
11015
11080
  metadata: {
11016
11081
  tokens: {
@@ -11066,6 +11131,373 @@ function getAverage(arr) {
11066
11131
  // src/graphql/schemas/index.ts
11067
11132
  init_entitlements();
11068
11133
  var import_fs = require("fs");
11134
+
11135
+ // src/exulu/transcription/service.ts
11136
+ init_cjs_shims();
11137
+ init_singleton();
11138
+ init_client();
11139
+ init_uppy();
11140
+
11141
+ // src/exulu/transcription/client.ts
11142
+ init_cjs_shims();
11143
+ var TranscriptionServerUnavailable = class extends Error {
11144
+ constructor(message) {
11145
+ super(message);
11146
+ this.name = "TranscriptionServerUnavailable";
11147
+ }
11148
+ };
11149
+ var getBaseUrl = () => {
11150
+ const url = process.env.TRANSCRIPTION_SERVER;
11151
+ if (!url) {
11152
+ throw new TranscriptionServerUnavailable(
11153
+ "TRANSCRIPTION_SERVER env var is not set. Start a whisper server with `npx @exulu/backend exulu-start-whisper` and point TRANSCRIPTION_SERVER at it."
11154
+ );
11155
+ }
11156
+ return url.replace(/\/$/, "");
11157
+ };
11158
+ var request = async (path3, init = {}) => {
11159
+ const url = `${getBaseUrl()}${path3}`;
11160
+ let res;
11161
+ try {
11162
+ res = await fetch(url, init);
11163
+ } catch (err) {
11164
+ throw new TranscriptionServerUnavailable(
11165
+ `Unable to reach whisper server at ${url}: ${err.message}`
11166
+ );
11167
+ }
11168
+ if (res.status === 404) {
11169
+ const err = new Error(`whisper server returned 404 for ${path3}`);
11170
+ err.code = "JOB_NOT_FOUND";
11171
+ throw err;
11172
+ }
11173
+ if (!res.ok) {
11174
+ throw new Error(
11175
+ `whisper server returned ${res.status} for ${path3}: ${await res.text()}`
11176
+ );
11177
+ }
11178
+ return await res.json();
11179
+ };
11180
+ var transcriptionClient = {
11181
+ submitJob: (opts) => request("/jobs", {
11182
+ method: "POST",
11183
+ headers: { "content-type": "application/json" },
11184
+ body: JSON.stringify(opts)
11185
+ }),
11186
+ getJob: (jobId) => request(`/jobs/${jobId}`),
11187
+ cancelJob: (jobId) => request(`/jobs/${jobId}`, {
11188
+ method: "DELETE"
11189
+ }),
11190
+ health: () => request("/healthz"),
11191
+ isConfigured: () => Boolean(process.env.TRANSCRIPTION_SERVER)
11192
+ };
11193
+
11194
+ // src/exulu/transcription/transcript-text.ts
11195
+ init_cjs_shims();
11196
+ var renderTranscript = (segments, speakers) => {
11197
+ if (!segments || segments.length === 0) return "";
11198
+ const blocks = [];
11199
+ for (const seg of segments) {
11200
+ const text = (seg.text ?? "").trim();
11201
+ if (!text) continue;
11202
+ const label = speakers[seg.speaker] ?? seg.speaker ?? "unknown";
11203
+ const last = blocks[blocks.length - 1];
11204
+ if (last && last.speaker === label) {
11205
+ last.text = `${last.text} ${text}`.trim();
11206
+ } else {
11207
+ blocks.push({ speaker: label, text });
11208
+ }
11209
+ }
11210
+ return blocks.map((b) => `${b.speaker}: ${b.text}`).join("\n");
11211
+ };
11212
+
11213
+ // src/exulu/transcription/service.ts
11214
+ var TABLE = "transcription_jobs";
11215
+ var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
11216
+ var parseJsonField = (v) => {
11217
+ if (v == null) return null;
11218
+ if (typeof v === "string") {
11219
+ try {
11220
+ return JSON.parse(v);
11221
+ } catch {
11222
+ return null;
11223
+ }
11224
+ }
11225
+ return v;
11226
+ };
11227
+ var presignAudio = async (s3Key) => {
11228
+ const app = exuluApp.get();
11229
+ const config = app._config ?? app.config;
11230
+ const configuredBucket = config?.fileUploads?.s3Bucket;
11231
+ if (!configuredBucket) {
11232
+ throw new Error("File uploads are not configured (s3Bucket missing).");
11233
+ }
11234
+ const firstSlash = s3Key.indexOf("/");
11235
+ const bucket = firstSlash > 0 ? s3Key.slice(0, firstSlash) : configuredBucket;
11236
+ const objectKey = firstSlash > 0 ? s3Key.slice(firstSlash + 1) : s3Key;
11237
+ return getPresignedUrl(bucket, objectKey, config);
11238
+ };
11239
+ var transcriptionService = {
11240
+ /**
11241
+ * Create a transcription job row and dispatch it to the whisper server.
11242
+ * Throws TranscriptionServerUnavailable if the feature is off.
11243
+ */
11244
+ async startJob(input) {
11245
+ if (!transcriptionClient.isConfigured()) {
11246
+ throw new TranscriptionServerUnavailable(
11247
+ "TRANSCRIPTION_SERVER is not set. Start a whisper server with `npx @exulu/backend exulu-start-whisper` and point TRANSCRIPTION_SERVER at it."
11248
+ );
11249
+ }
11250
+ const { db: db2 } = await postgresClient();
11251
+ const now = /* @__PURE__ */ new Date();
11252
+ const [inserted] = await db2(TABLE).insert({
11253
+ audio_s3key: input.s3Key,
11254
+ title: input.title ?? input.filename,
11255
+ status: "queued",
11256
+ project_id: input.project_id ?? null,
11257
+ target_rights_mode: input.target_rights_mode ?? "private",
11258
+ target_rbac_users: input.target_rbac_users ? JSON.stringify(input.target_rbac_users) : null,
11259
+ target_rbac_roles: input.target_rbac_roles ? JSON.stringify(input.target_rbac_roles) : null,
11260
+ rights_mode: "private",
11261
+ created_by: input.userId,
11262
+ createdAt: now,
11263
+ updatedAt: now
11264
+ }).returning("*");
11265
+ const row = this._rowFromDb(inserted);
11266
+ try {
11267
+ const audioUrl = await presignAudio(input.s3Key);
11268
+ const submitted = await transcriptionClient.submitJob({
11269
+ audio_url: audioUrl,
11270
+ language: input.language ?? void 0,
11271
+ num_speakers: input.num_speakers ?? void 0,
11272
+ hotwords: input.hotwords
11273
+ });
11274
+ const [updated] = await db2(TABLE).where({ id: row.id }).update({
11275
+ whisper_job_id: submitted.job_id,
11276
+ status: "transcribing",
11277
+ updatedAt: /* @__PURE__ */ new Date()
11278
+ }).returning("*");
11279
+ return this._rowFromDb(updated);
11280
+ } catch (err) {
11281
+ const [failed] = await db2(TABLE).where({ id: row.id }).update({
11282
+ status: "failed",
11283
+ error: err.message,
11284
+ updatedAt: /* @__PURE__ */ new Date()
11285
+ }).returning("*");
11286
+ log2(`Failed to dispatch job ${row.id}: ${err.message}`);
11287
+ return this._rowFromDb(failed);
11288
+ }
11289
+ },
11290
+ /**
11291
+ * Reconcile every transcribing row against the whisper server. Called from
11292
+ * the polling loop on a fixed interval. Caps how many rows we touch per
11293
+ * tick so a backlog can't starve the event loop.
11294
+ */
11295
+ async pollOnce(maxPerTick = 50) {
11296
+ if (!transcriptionClient.isConfigured()) return;
11297
+ const { db: db2 } = await postgresClient();
11298
+ const rows = await db2(TABLE).where({ status: "transcribing" }).whereNotNull("whisper_job_id").limit(maxPerTick);
11299
+ for (const dbRow of rows) {
11300
+ const row = this._rowFromDb(dbRow);
11301
+ if (!row.whisper_job_id) continue;
11302
+ try {
11303
+ const job = await transcriptionClient.getJob(row.whisper_job_id);
11304
+ await this._applyJobUpdate(row, job);
11305
+ } catch (err) {
11306
+ const code = err.code;
11307
+ if (code === "JOB_NOT_FOUND") {
11308
+ await db2(TABLE).where({ id: row.id }).update({
11309
+ status: "failed",
11310
+ error: "lost on server restart",
11311
+ updatedAt: /* @__PURE__ */ new Date()
11312
+ });
11313
+ } else if (err instanceof TranscriptionServerUnavailable) {
11314
+ log2(`Whisper server unreachable while polling ${row.id}; will retry`);
11315
+ } else {
11316
+ log2(`Error polling job ${row.id}: ${err.message}`);
11317
+ }
11318
+ }
11319
+ }
11320
+ },
11321
+ async _applyJobUpdate(row, job) {
11322
+ const { db: db2 } = await postgresClient();
11323
+ if ((job.status === "queued" || job.status === "running") && job.duration_seconds != null && row.duration_seconds !== job.duration_seconds) {
11324
+ await db2(TABLE).where({ id: row.id }).update({
11325
+ duration_seconds: job.duration_seconds,
11326
+ updatedAt: /* @__PURE__ */ new Date()
11327
+ });
11328
+ }
11329
+ if (job.status === "running" || job.status === "queued") return;
11330
+ if (job.status === "completed") {
11331
+ await db2(TABLE).where({ id: row.id }).update({
11332
+ status: "awaiting_review",
11333
+ raw_segments: JSON.stringify(job.segments ?? []),
11334
+ language: job.language ?? null,
11335
+ duration_seconds: job.duration_seconds ?? null,
11336
+ updatedAt: /* @__PURE__ */ new Date()
11337
+ });
11338
+ return;
11339
+ }
11340
+ if (job.status === "failed") {
11341
+ await db2(TABLE).where({ id: row.id }).update({
11342
+ status: "failed",
11343
+ error: job.error ?? "transcription failed",
11344
+ updatedAt: /* @__PURE__ */ new Date()
11345
+ });
11346
+ return;
11347
+ }
11348
+ if (job.status === "cancelled") {
11349
+ await db2(TABLE).where({ id: row.id }).update({
11350
+ status: "cancelled",
11351
+ updatedAt: /* @__PURE__ */ new Date()
11352
+ });
11353
+ }
11354
+ },
11355
+ async cancelJob(id) {
11356
+ const { db: db2 } = await postgresClient();
11357
+ const dbRow = await db2(TABLE).where({ id }).first();
11358
+ if (!dbRow) throw new Error(`transcription_job ${id} not found`);
11359
+ const row = this._rowFromDb(dbRow);
11360
+ if (row.whisper_job_id && transcriptionClient.isConfigured()) {
11361
+ try {
11362
+ await transcriptionClient.cancelJob(row.whisper_job_id);
11363
+ } catch (err) {
11364
+ const code = err.code;
11365
+ if (code !== "JOB_NOT_FOUND") {
11366
+ log2(`Best-effort cancel of whisper job failed: ${err.message}`);
11367
+ }
11368
+ }
11369
+ }
11370
+ const [updated] = await db2(TABLE).where({ id }).update({ status: "cancelled", updatedAt: /* @__PURE__ */ new Date() }).returning("*");
11371
+ return this._rowFromDb(updated);
11372
+ },
11373
+ /**
11374
+ * User clicked Save in the review panel.
11375
+ *
11376
+ * - From 'awaiting_review': render the speaker-labeled transcript, create a
11377
+ * new ExuluContext item, apply RBAC + optional project linkage, mark the
11378
+ * job saved.
11379
+ * - From 'saved': re-render the transcript with the (possibly updated)
11380
+ * speaker map and upsert the existing context item by id. Used by the
11381
+ * Completed-section re-edit flow.
11382
+ */
11383
+ async finalize(id, input) {
11384
+ const { db: db2 } = await postgresClient();
11385
+ const dbRow = await db2(TABLE).where({ id }).first();
11386
+ if (!dbRow) throw new Error(`transcription_job ${id} not found`);
11387
+ const row = this._rowFromDb(dbRow);
11388
+ if (row.status !== "awaiting_review" && row.status !== "saved") {
11389
+ throw new Error(
11390
+ `transcription_job ${id} is in status '${row.status}'; can only finalize from 'awaiting_review' or 'saved'`
11391
+ );
11392
+ }
11393
+ if (!row.raw_segments) {
11394
+ throw new Error(`transcription_job ${id} has no raw_segments to finalize`);
11395
+ }
11396
+ const app = exuluApp.get();
11397
+ const context = app.context("transcriptions");
11398
+ if (!context) {
11399
+ throw new Error("Built-in transcriptions context not registered");
11400
+ }
11401
+ const config = app._config ?? app.config;
11402
+ const transcriptText = renderTranscript(row.raw_segments, input.speakers);
11403
+ const rightsMode = input.target_rights_mode ?? row.target_rights_mode ?? "private";
11404
+ const isReSave = row.status === "saved" && !!row.saved_item_id;
11405
+ const itemInput = {
11406
+ // Carrying the id on re-save makes context.createItem upsert in place.
11407
+ ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
11408
+ name: input.title ?? row.title ?? "Transcript",
11409
+ transcript_text: transcriptText,
11410
+ audio_s3key: row.audio_s3key,
11411
+ language: row.language ?? void 0,
11412
+ duration_seconds: row.duration_seconds ?? void 0,
11413
+ speakers: input.speakers,
11414
+ raw_segments: row.raw_segments,
11415
+ rights_mode: rightsMode,
11416
+ created_by: row.created_by
11417
+ };
11418
+ let item;
11419
+ try {
11420
+ const result = await context.createItem(
11421
+ itemInput,
11422
+ config,
11423
+ row.created_by,
11424
+ void 0,
11425
+ isReSave
11426
+ // upsert when re-saving
11427
+ );
11428
+ item = result.item;
11429
+ } catch (err) {
11430
+ await db2(TABLE).where({ id }).update({
11431
+ speakers: JSON.stringify(input.speakers),
11432
+ error: `Failed to save: ${err.message}`,
11433
+ updatedAt: /* @__PURE__ */ new Date()
11434
+ });
11435
+ throw err;
11436
+ }
11437
+ const itemId = item.id ?? row.saved_item_id ?? "";
11438
+ const users = input.target_rbac_users ?? row.target_rbac_users ?? [];
11439
+ const roles = input.target_rbac_roles ?? row.target_rbac_roles ?? [];
11440
+ if ((users.length || roles.length) && rightsMode !== "private") {
11441
+ try {
11442
+ await handleRBACUpdate(
11443
+ db2,
11444
+ "transcriptions",
11445
+ itemId,
11446
+ { users, roles },
11447
+ []
11448
+ );
11449
+ } catch (err) {
11450
+ log2(`RBAC update failed for item ${itemId}: ${err.message}`);
11451
+ }
11452
+ }
11453
+ const projectId = input.project_id ?? row.project_id ?? null;
11454
+ let projectWarning = null;
11455
+ if (projectId && !isReSave) {
11456
+ try {
11457
+ const project = await db2("projects").where({ id: projectId }).first();
11458
+ if (!project) {
11459
+ projectWarning = `project ${projectId} not found`;
11460
+ } else {
11461
+ const existing = parseJsonField(project.project_items) ?? [];
11462
+ const globalId = `transcriptions/${itemId}`;
11463
+ if (!existing.includes(globalId)) {
11464
+ existing.push(globalId);
11465
+ await db2("projects").where({ id: projectId }).update({
11466
+ project_items: JSON.stringify(existing),
11467
+ updatedAt: /* @__PURE__ */ new Date()
11468
+ });
11469
+ }
11470
+ }
11471
+ } catch (err) {
11472
+ projectWarning = err.message;
11473
+ }
11474
+ }
11475
+ const [updated] = await db2(TABLE).where({ id }).update({
11476
+ status: "saved",
11477
+ saved_item_id: itemId,
11478
+ title: input.title ?? row.title ?? null,
11479
+ speakers: JSON.stringify(input.speakers),
11480
+ error: projectWarning ? `Saved, but could not attach to project: ${projectWarning}` : null,
11481
+ updatedAt: /* @__PURE__ */ new Date()
11482
+ }).returning("*");
11483
+ return { item, row: this._rowFromDb(updated) };
11484
+ },
11485
+ _rowFromDb(dbRow) {
11486
+ return {
11487
+ ...dbRow,
11488
+ raw_segments: parseJsonField(dbRow.raw_segments),
11489
+ speakers: parseJsonField(dbRow.speakers),
11490
+ target_rbac_users: parseJsonField(
11491
+ dbRow.target_rbac_users
11492
+ ),
11493
+ target_rbac_roles: parseJsonField(
11494
+ dbRow.target_rbac_roles
11495
+ )
11496
+ };
11497
+ }
11498
+ };
11499
+
11500
+ // src/graphql/schemas/index.ts
11069
11501
  function createExuluContextsTypeDefs(table) {
11070
11502
  const enumDefs = table.fields.filter((field) => field.type === "enum" && field.enumValues).map((field) => {
11071
11503
  if (!field.enumValues) {
@@ -11483,6 +11915,39 @@ type PageInfo {
11483
11915
  mutationDefs += `
11484
11916
  deleteJob(queue: QueueEnum!, id: String!): JobActionReturnPayload
11485
11917
  `;
11918
+ mutationDefs += `
11919
+ transcriptionJobStart(input: TranscriptionJobStartInput!): transcription_job
11920
+ transcriptionJobFinalize(id: ID!, input: TranscriptionJobFinalizeInput!): TranscriptionJobFinalizeResult
11921
+ transcriptionJobCancel(id: ID!): transcription_job
11922
+ `;
11923
+ modelDefs += `
11924
+ input TranscriptionJobStartInput {
11925
+ audio_s3key: String!
11926
+ filename: String!
11927
+ title: String
11928
+ language: String
11929
+ num_speakers: Int
11930
+ hotwords: [String!]
11931
+ project_id: ID
11932
+ target_rights_mode: String
11933
+ target_rbac_users: [RBACUserInput!]
11934
+ target_rbac_roles: [RBACRoleInput!]
11935
+ }
11936
+
11937
+ input TranscriptionJobFinalizeInput {
11938
+ title: String
11939
+ speakers: JSON!
11940
+ project_id: ID
11941
+ target_rights_mode: String
11942
+ target_rbac_users: [RBACUserInput!]
11943
+ target_rbac_roles: [RBACRoleInput!]
11944
+ }
11945
+
11946
+ type TranscriptionJobFinalizeResult {
11947
+ job: transcription_job!
11948
+ item_id: ID!
11949
+ }
11950
+ `;
11486
11951
  typeDefs += `
11487
11952
  tools(search: String, category: String, limit: Int, page: Int): ToolPaginationResult
11488
11953
  toolCategories: [String!]!
@@ -11837,7 +12302,7 @@ type LiteLLMModel {
11837
12302
  } = await validateWorkflowPayload(jobData, providers);
11838
12303
  const retries = 3;
11839
12304
  let attempts = 0;
11840
- const promise = new Promise(async (resolve6, reject) => {
12305
+ const promise = new Promise(async (resolve7, reject) => {
11841
12306
  while (attempts < retries) {
11842
12307
  try {
11843
12308
  const messages2 = await processUiMessagesFlow({
@@ -11852,7 +12317,7 @@ type LiteLLMModel {
11852
12317
  config,
11853
12318
  variables: args.variables
11854
12319
  });
11855
- resolve6(messages2);
12320
+ resolve7(messages2);
11856
12321
  break;
11857
12322
  } catch (error) {
11858
12323
  console.error(
@@ -11866,7 +12331,7 @@ type LiteLLMModel {
11866
12331
  if (attempts >= retries) {
11867
12332
  reject(error instanceof Error ? error : new Error(String(error)));
11868
12333
  }
11869
- await new Promise((resolve7) => setTimeout((resolve8) => resolve8(true), 2e3));
12334
+ await new Promise((resolve8) => setTimeout((resolve9) => resolve9(true), 2e3));
11870
12335
  }
11871
12336
  }
11872
12337
  });
@@ -12048,6 +12513,54 @@ type LiteLLMModel {
12048
12513
  await config2.queue.remove(args.id);
12049
12514
  return { success: true };
12050
12515
  };
12516
+ const assertOwnsTranscriptionJob = async (id, context) => {
12517
+ const { db: db2, user } = context;
12518
+ if (!user) throw new Error("Authentication required");
12519
+ if (user.super_admin === true) return;
12520
+ const row = await db2.from("transcription_jobs").select(["created_by", "rights_mode"]).where({ id }).first();
12521
+ if (!row) throw new Error(`transcription_job ${id} not found`);
12522
+ if (row.rights_mode === "public") return;
12523
+ if (row.created_by === user.id) return;
12524
+ throw new Error("Not authorized to act on this transcription job");
12525
+ };
12526
+ resolvers.Mutation["transcriptionJobStart"] = async (_, args, context) => {
12527
+ const { user } = context;
12528
+ if (!user) throw new Error("Authentication required");
12529
+ if (!transcriptionClient.isConfigured()) {
12530
+ throw new Error(
12531
+ "TRANSCRIPTION_DISABLED: TRANSCRIPTION_SERVER not set on this server. Ask the operator to start a whisper server with `npx @exulu/backend exulu-start-whisper`."
12532
+ );
12533
+ }
12534
+ return transcriptionService.startJob({
12535
+ userId: user.id,
12536
+ s3Key: args.input.audio_s3key,
12537
+ filename: args.input.filename,
12538
+ title: args.input.title,
12539
+ language: args.input.language ?? void 0,
12540
+ num_speakers: args.input.num_speakers ?? void 0,
12541
+ hotwords: args.input.hotwords ?? void 0,
12542
+ project_id: args.input.project_id ?? null,
12543
+ target_rights_mode: args.input.target_rights_mode ?? null,
12544
+ target_rbac_users: args.input.target_rbac_users ?? void 0,
12545
+ target_rbac_roles: args.input.target_rbac_roles ?? void 0
12546
+ });
12547
+ };
12548
+ resolvers.Mutation["transcriptionJobFinalize"] = async (_, args, context) => {
12549
+ await assertOwnsTranscriptionJob(args.id, context);
12550
+ const { item, row } = await transcriptionService.finalize(args.id, {
12551
+ title: args.input.title,
12552
+ speakers: args.input.speakers,
12553
+ project_id: args.input.project_id ?? null,
12554
+ target_rights_mode: args.input.target_rights_mode ?? null,
12555
+ target_rbac_users: args.input.target_rbac_users ?? void 0,
12556
+ target_rbac_roles: args.input.target_rbac_roles ?? void 0
12557
+ });
12558
+ return { job: row, item_id: item.id };
12559
+ };
12560
+ resolvers.Mutation["transcriptionJobCancel"] = async (_, args, context) => {
12561
+ await assertOwnsTranscriptionJob(args.id, context);
12562
+ return transcriptionService.cancelJob(args.id);
12563
+ };
12051
12564
  resolvers.Query["evals"] = async (_, args, context, info) => {
12052
12565
  const requestedFields = getRequestedFields(info);
12053
12566
  return {
@@ -12119,10 +12632,10 @@ type LiteLLMModel {
12119
12632
  contexts.map(async (context2) => {
12120
12633
  let processor = null;
12121
12634
  if (context2.processor) {
12122
- processor = await new Promise(async (resolve6, reject) => {
12635
+ processor = await new Promise(async (resolve7, reject) => {
12123
12636
  const config2 = context2.processor?.config;
12124
12637
  const queue = await config2?.queue;
12125
- resolve6({
12638
+ resolve7({
12126
12639
  name: context2.processor.name,
12127
12640
  description: context2.processor.description,
12128
12641
  queue: queue?.queue?.name || void 0,
@@ -12203,10 +12716,10 @@ type LiteLLMModel {
12203
12716
  }
12204
12717
  let processor = null;
12205
12718
  if (data.processor) {
12206
- processor = await new Promise(async (resolve6, reject) => {
12719
+ processor = await new Promise(async (resolve7, reject) => {
12207
12720
  const config2 = data.processor?.config;
12208
12721
  const queue = await config2?.queue;
12209
- resolve6({
12722
+ resolve7({
12210
12723
  name: data.processor.name,
12211
12724
  description: data.processor.description,
12212
12725
  queue: queue?.queue?.name || void 0,
@@ -12900,7 +13413,7 @@ var import_utils5 = require("@apollo/utils.keyvaluecache");
12900
13413
  var import_body_parser = __toESM(require("body-parser"), 1);
12901
13414
  var import_crypto_js8 = __toESM(require("crypto-js"), 1);
12902
13415
  var import_openai = __toESM(require("openai"), 1);
12903
- var import_fs3 = __toESM(require("fs"), 1);
13416
+ var import_fs4 = __toESM(require("fs"), 1);
12904
13417
  var import_node_crypto5 = require("crypto");
12905
13418
  var import_api2 = require("@opentelemetry/api");
12906
13419
  init_check_record_access();
@@ -13256,6 +13769,9 @@ var ExuluProvider = class {
13256
13769
  If the user does not explicitly provide the current date, for examle when saying ' this weekend', you should assume
13257
13770
  they are talking with the current date in mind as a reference.`;
13258
13771
  let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
13772
+ if (user?.personal_system_prompt?.trim()) {
13773
+ system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
13774
+ }
13259
13775
  system += "\n\n" + genericContext;
13260
13776
  if (memoryContext) {
13261
13777
  system += "\n\n" + memoryContext;
@@ -13355,7 +13871,10 @@ When a tool execution is not approved by the user, do not retry it unless explic
13355
13871
  agent,
13356
13872
  memoryItems
13357
13873
  ),
13358
- stopWhen: [(0, import_ai9.stepCountIs)(maxStepCount || 5)]
13874
+ // Stop after the image_generation tool fires — the widget IS the
13875
+ // assistant's response, no follow-up text turn is wanted (same
13876
+ // reasoning as question_ask: the UI artifact is the message).
13877
+ stopWhen: [(0, import_ai9.stepCountIs)(maxStepCount || 5), (0, import_ai9.hasToolCall)("image_generation")]
13359
13878
  // make configurable
13360
13879
  });
13361
13880
  console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
@@ -13436,7 +13955,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
13436
13955
  agent,
13437
13956
  memoryItems
13438
13957
  ),
13439
- stopWhen: [(0, import_ai9.stepCountIs)(maxStepCount || 5)]
13958
+ stopWhen: [(0, import_ai9.stepCountIs)(maxStepCount || 5), (0, import_ai9.hasToolCall)("image_generation")]
13440
13959
  });
13441
13960
  if (statistics) {
13442
13961
  await Promise.all([
@@ -13661,6 +14180,9 @@ ${extractedText}
13661
14180
  messages = await this.processFilePartsInMessages(messages);
13662
14181
  const genericContext = "IMPORTANT: \n\n The current date is " + (/* @__PURE__ */ new Date()).toLocaleDateString() + " and the current time is " + (/* @__PURE__ */ new Date()).toLocaleTimeString() + ". If the user does not explicitly provide the current date, for examle when saying ' this weekend', you should assume they are talking with the current date in mind as a reference.";
13663
14182
  let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
14183
+ if (user?.personal_system_prompt?.trim()) {
14184
+ system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
14185
+ }
13664
14186
  system += "\n\n" + genericContext;
13665
14187
  const includesContextSearchTool = currentTools?.some(
13666
14188
  (tool7) => tool7.name.toLowerCase().includes("context_search") || tool7.id.includes("context_search") || tool7.type === "context"
@@ -13817,7 +14339,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
13817
14339
  },
13818
14340
  // provide more loops for skills because they are more complex to execute
13819
14341
  // todo allow configuring this per skill
13820
- stopWhen: [(0, import_ai9.stepCountIs)(maxStepCount || currentSkills?.length ? 10 : 5)]
14342
+ stopWhen: [(0, import_ai9.stepCountIs)(maxStepCount || currentSkills?.length ? 10 : 5), (0, import_ai9.hasToolCall)("image_generation")]
13821
14343
  });
13822
14344
  return {
13823
14345
  stream: result,
@@ -14027,76 +14549,543 @@ async function synthesizeSpeech(args) {
14027
14549
  return Buffer.from(arrayBuf);
14028
14550
  }
14029
14551
 
14030
- // src/exulu/routes.ts
14031
- init_tags();
14032
- var import_multer = __toESM(require("multer"), 1);
14033
-
14034
- // src/utils/check-provider-rate-limit.ts
14552
+ // src/exulu/image-generation.ts
14035
14553
  init_cjs_shims();
14036
- var checkProviderRateLimit = async (provider) => {
14037
- if (provider.rateLimit) {
14038
- console.log("[EXULU] rate limiting provider.", provider.rateLimit);
14039
- const limit = await providerRateLimiter(
14040
- provider.rateLimit.name || provider.id,
14041
- provider.rateLimit.rate_limit.time,
14042
- provider.rateLimit.rate_limit.limit,
14043
- 1
14044
- );
14045
- if (!limit.status) {
14046
- throw new Error("Rate limit exceeded.");
14047
- }
14554
+ var ImageGenerationError = class extends Error {
14555
+ constructor(upstreamStatus, message) {
14556
+ super(message);
14557
+ this.upstreamStatus = upstreamStatus;
14558
+ this.name = "ImageGenerationError";
14048
14559
  }
14049
14560
  };
14050
- var providerRateLimiter = async (key2, windowSeconds, limit, points) => {
14051
- try {
14052
- const { client: client2 } = await redisClient();
14053
- if (!client2) {
14054
- console.warn("[EXULU] Rate limiting disabled - Redis not available");
14055
- return {
14056
- status: true,
14057
- retryAfter: null
14058
- };
14059
- }
14060
- const redisKey = `exulu/${key2}`;
14061
- const current = await client2.incrBy(redisKey, points);
14062
- if (current === points) {
14063
- await client2.expire(redisKey, windowSeconds);
14064
- }
14065
- if (current > limit) {
14066
- const ttl = await client2.ttl(redisKey);
14067
- return {
14068
- status: false,
14069
- retryAfter: ttl
14070
- };
14561
+ var resolveProxyConfig = () => {
14562
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
14563
+ const port = process.env.LITELLM_PORT ?? "4000";
14564
+ const masterKey = process.env.LITELLM_MASTER_KEY;
14565
+ if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
14566
+ return { host, port, masterKey };
14567
+ };
14568
+ var normalizeDataEntries = async (data) => {
14569
+ const out = [];
14570
+ for (const entry of data) {
14571
+ let buffer;
14572
+ let contentType = "image/png";
14573
+ let extension = "png";
14574
+ if (entry.b64_json) {
14575
+ buffer = Buffer.from(entry.b64_json, "base64");
14576
+ } else if (entry.url) {
14577
+ const upstream = await fetch(entry.url);
14578
+ if (!upstream.ok) {
14579
+ throw new ImageGenerationError(
14580
+ upstream.status,
14581
+ `Failed to download generated image from ${entry.url}: ${upstream.status} ${upstream.statusText}`
14582
+ );
14583
+ }
14584
+ const ct = upstream.headers.get("content-type");
14585
+ if (ct && ct.startsWith("image/")) {
14586
+ contentType = ct;
14587
+ const inferred = ct.split("/")[1]?.split(";")[0]?.trim();
14588
+ if (inferred) extension = inferred === "jpeg" ? "jpg" : inferred;
14589
+ }
14590
+ buffer = Buffer.from(await upstream.arrayBuffer());
14591
+ } else {
14592
+ throw new ImageGenerationError(
14593
+ 0,
14594
+ "LiteLLM image response entry contained neither b64_json nor url."
14595
+ );
14071
14596
  }
14072
- return {
14073
- status: true,
14074
- retryAfter: null
14075
- };
14076
- } catch (error) {
14077
- console.error("[EXULU] Rate limiting error:", error);
14078
- return {
14079
- status: true,
14080
- retryAfter: null
14081
- };
14597
+ out.push({ buffer, contentType, extension, revisedPrompt: entry.revised_prompt });
14082
14598
  }
14599
+ return out;
14083
14600
  };
14084
-
14085
- // src/utils/check-api-key-scope.ts
14086
- init_cjs_shims();
14087
- function checkApiKeyScope(user, agentId) {
14088
- if (!user || user.type !== "api") return { allowed: true };
14089
- if (!user.scope_mode || user.scope_mode === "admin") return { allowed: true };
14090
- if (user.scope_mode === "agents") {
14091
- const ids = Array.isArray(user.agent_ids) ? user.agent_ids : [];
14092
- if (!ids.includes(agentId)) {
14093
- return {
14094
- allowed: false,
14095
- reason: `API key is not scoped to agent ${agentId}.`,
14096
- code: 403
14097
- };
14098
- }
14099
- return { allowed: true };
14601
+ async function generateImage(args) {
14602
+ if (!args.model) throw new Error("model is required");
14603
+ if (!args.prompt) throw new Error("prompt is required");
14604
+ const cfg = resolveProxyConfig();
14605
+ const body = {
14606
+ model: args.model,
14607
+ prompt: args.prompt
14608
+ };
14609
+ if (args.size) body.size = args.size;
14610
+ if (args.quality) body.quality = args.quality;
14611
+ if (args.n) body.n = args.n;
14612
+ const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/generations`, {
14613
+ method: "POST",
14614
+ headers: {
14615
+ Authorization: `Bearer ${cfg.masterKey}`,
14616
+ "Content-Type": "application/json"
14617
+ },
14618
+ body: JSON.stringify(body),
14619
+ signal: args.signal
14620
+ });
14621
+ if (!res.ok) {
14622
+ const text = await res.text().catch(() => "");
14623
+ throw new ImageGenerationError(
14624
+ res.status,
14625
+ `LiteLLM image generation failed (status ${res.status}): ${text}`.trim()
14626
+ );
14627
+ }
14628
+ const json = await res.json();
14629
+ if (!json?.data || json.data.length === 0) {
14630
+ throw new ImageGenerationError(
14631
+ res.status,
14632
+ "LiteLLM returned no image data in the response."
14633
+ );
14634
+ }
14635
+ return normalizeDataEntries(json.data);
14636
+ }
14637
+ async function editImage(args) {
14638
+ if (!args.model) throw new Error("model is required");
14639
+ if (!args.prompt) throw new Error("prompt is required");
14640
+ if (!args.references || args.references.length === 0) {
14641
+ throw new Error("at least one reference image is required");
14642
+ }
14643
+ const cfg = resolveProxyConfig();
14644
+ const form = new FormData();
14645
+ form.append("model", args.model);
14646
+ form.append("prompt", args.prompt);
14647
+ if (args.n != null) form.append("n", String(args.n));
14648
+ if (args.size) form.append("size", args.size);
14649
+ if (args.quality) form.append("quality", args.quality);
14650
+ form.append("response_format", "b64_json");
14651
+ for (const ref of args.references) {
14652
+ form.append(
14653
+ "image",
14654
+ new Blob([ref.buffer], { type: ref.mimetype ?? "image/png" }),
14655
+ ref.filename
14656
+ );
14657
+ }
14658
+ if (args.mask) {
14659
+ form.append(
14660
+ "mask",
14661
+ new Blob([args.mask.buffer], { type: args.mask.mimetype ?? "image/png" }),
14662
+ args.mask.filename
14663
+ );
14664
+ }
14665
+ const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/edits`, {
14666
+ method: "POST",
14667
+ headers: { Authorization: `Bearer ${cfg.masterKey}` },
14668
+ body: form,
14669
+ signal: args.signal
14670
+ });
14671
+ if (!res.ok) {
14672
+ const text = await res.text().catch(() => "");
14673
+ throw new ImageGenerationError(
14674
+ res.status,
14675
+ `LiteLLM image edit failed (status ${res.status}): ${text}`.trim()
14676
+ );
14677
+ }
14678
+ const json = await res.json();
14679
+ if (!json?.data || json.data.length === 0) {
14680
+ throw new ImageGenerationError(
14681
+ res.status,
14682
+ "LiteLLM returned no image data in the edit response."
14683
+ );
14684
+ }
14685
+ return normalizeDataEntries(json.data);
14686
+ }
14687
+
14688
+ // src/exulu/litellm/parse-image-models.ts
14689
+ init_cjs_shims();
14690
+ var import_node_fs5 = require("fs");
14691
+ var stripComment = (line) => {
14692
+ const idx = line.indexOf("#");
14693
+ return idx >= 0 ? line.slice(0, idx) : line;
14694
+ };
14695
+ var parseInlineArray = (raw) => {
14696
+ const m = raw.trim().match(/^\[(.*)\]$/);
14697
+ if (!m) return void 0;
14698
+ const inner = m[1] ?? "";
14699
+ if (!inner.trim()) return [];
14700
+ return inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
14701
+ };
14702
+ var parseBool = (raw) => {
14703
+ const v = raw.trim().toLowerCase();
14704
+ if (v === "true" || v === "yes") return true;
14705
+ if (v === "false" || v === "no") return false;
14706
+ return void 0;
14707
+ };
14708
+ var parseInt10 = (raw) => {
14709
+ const n = Number(raw.trim());
14710
+ return Number.isInteger(n) ? n : void 0;
14711
+ };
14712
+ var parseImageGenerationModels = (configPath) => {
14713
+ if (!(0, import_node_fs5.existsSync)(configPath)) return [];
14714
+ const text = (0, import_node_fs5.readFileSync)(configPath, "utf8");
14715
+ const lines = text.split("\n");
14716
+ const entries = [];
14717
+ let current;
14718
+ for (const rawLine of lines) {
14719
+ const noComment = stripComment(rawLine);
14720
+ if (!noComment.trim()) continue;
14721
+ const indent = (rawLine.match(/^\s*/)?.[0] ?? "").length;
14722
+ const modelNameMatch = noComment.match(
14723
+ /^\s*-\s*model_name\s*:\s*["']?([^"'\s#]+)["']?\s*$/
14724
+ );
14725
+ if (modelNameMatch) {
14726
+ if (current) entries.push(current);
14727
+ current = { model_name: modelNameMatch[1], indent };
14728
+ continue;
14729
+ }
14730
+ if (!current) continue;
14731
+ if (indent <= current.indent && !/^\s*-\s/.test(rawLine)) {
14732
+ entries.push(current);
14733
+ current = void 0;
14734
+ continue;
14735
+ }
14736
+ const kvMatch = noComment.match(/^\s*(\w+)\s*:\s*(.+?)\s*$/);
14737
+ if (!kvMatch) continue;
14738
+ const key2 = kvMatch[1] ?? "";
14739
+ const rawValue = kvMatch[2] ?? "";
14740
+ switch (key2) {
14741
+ case "type": {
14742
+ current.type = rawValue.replace(/^["']|["']$/g, "").trim();
14743
+ break;
14744
+ }
14745
+ case "sizes": {
14746
+ current.sizes = parseInlineArray(rawValue);
14747
+ break;
14748
+ }
14749
+ case "qualities": {
14750
+ current.qualities = parseInlineArray(rawValue);
14751
+ break;
14752
+ }
14753
+ case "supports_edit": {
14754
+ current.supports_edit = parseBool(rawValue);
14755
+ break;
14756
+ }
14757
+ case "max_n": {
14758
+ current.max_n = parseInt10(rawValue);
14759
+ break;
14760
+ }
14761
+ }
14762
+ }
14763
+ if (current) entries.push(current);
14764
+ const imageEntries = entries.filter((e) => e.type === "image_generation");
14765
+ const errors = [];
14766
+ const validated = [];
14767
+ for (const e of imageEntries) {
14768
+ const modelErrs = [];
14769
+ if (!Array.isArray(e.sizes) || e.sizes.length === 0) {
14770
+ modelErrs.push(
14771
+ 'model_info.sizes must be a non-empty inline YAML array of strings, e.g. `sizes: ["1024x1024", "1024x1536"]`'
14772
+ );
14773
+ }
14774
+ if (!Array.isArray(e.qualities) || e.qualities.length === 0) {
14775
+ modelErrs.push(
14776
+ 'model_info.qualities must be a non-empty inline YAML array of strings, e.g. `qualities: ["auto", "high"]`'
14777
+ );
14778
+ }
14779
+ if (typeof e.supports_edit !== "boolean") {
14780
+ modelErrs.push(
14781
+ "model_info.supports_edit must be a boolean (true/false)"
14782
+ );
14783
+ }
14784
+ if (typeof e.max_n !== "number" || !Number.isInteger(e.max_n) || e.max_n < 1) {
14785
+ modelErrs.push("model_info.max_n must be an integer \u2265 1");
14786
+ }
14787
+ if (modelErrs.length > 0) {
14788
+ errors.push(
14789
+ ` - "${e.model_name}":
14790
+ - ${modelErrs.join("\n - ")}`
14791
+ );
14792
+ continue;
14793
+ }
14794
+ validated.push({
14795
+ model_name: e.model_name,
14796
+ sizes: e.sizes,
14797
+ qualities: e.qualities,
14798
+ supports_edit: e.supports_edit,
14799
+ max_n: e.max_n
14800
+ });
14801
+ }
14802
+ if (errors.length > 0) {
14803
+ throw new Error(
14804
+ `[EXULU] config.litellm.yaml has image-generation models with missing or invalid model_info keys. Fix and restart Exulu:
14805
+ ${errors.join("\n")}
14806
+ See docs/superpowers/specs/2026-05-31-in-chat-image-generation-design.md for the required schema.`
14807
+ );
14808
+ }
14809
+ return validated;
14810
+ };
14811
+
14812
+ // src/exulu/routes.ts
14813
+ var import_node_path5 = require("path");
14814
+
14815
+ // src/utils/python-setup.ts
14816
+ init_cjs_shims();
14817
+ var import_child_process = require("child_process");
14818
+ var import_util = require("util");
14819
+ var import_path = require("path");
14820
+ var import_fs3 = require("fs");
14821
+ var import_url = require("url");
14822
+ var execAsync4 = (0, import_util.promisify)(import_child_process.exec);
14823
+ function getPackageRoot() {
14824
+ const currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
14825
+ let currentDir = (0, import_path.dirname)(currentFile);
14826
+ let attempts = 0;
14827
+ const maxAttempts = 10;
14828
+ while (attempts < maxAttempts) {
14829
+ const packageJsonPath = (0, import_path.join)(currentDir, "package.json");
14830
+ if ((0, import_fs3.existsSync)(packageJsonPath)) {
14831
+ try {
14832
+ const packageJson = JSON.parse((0, import_fs3.readFileSync)(packageJsonPath, "utf-8"));
14833
+ if (packageJson.name === "@exulu/backend") {
14834
+ return currentDir;
14835
+ }
14836
+ } catch {
14837
+ }
14838
+ }
14839
+ const parentDir = (0, import_path.resolve)(currentDir, "..");
14840
+ if (parentDir === currentDir) {
14841
+ break;
14842
+ }
14843
+ currentDir = parentDir;
14844
+ attempts++;
14845
+ }
14846
+ const fallback = (0, import_path.resolve)((0, import_path.dirname)((0, import_url.fileURLToPath)(importMetaUrl)), "../..");
14847
+ return fallback;
14848
+ }
14849
+ function getSetupScriptPath(packageRoot) {
14850
+ return (0, import_path.resolve)(packageRoot, "ee/python/setup.sh");
14851
+ }
14852
+ function getVenvPath(packageRoot) {
14853
+ return (0, import_path.resolve)(packageRoot, "ee/python/.venv");
14854
+ }
14855
+ function isPythonEnvironmentSetup(packageRoot) {
14856
+ const root = packageRoot ?? getPackageRoot();
14857
+ const venvPath = getVenvPath(root);
14858
+ const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
14859
+ return (0, import_fs3.existsSync)(venvPath) && (0, import_fs3.existsSync)(pythonPath);
14860
+ }
14861
+ async function setupPythonEnvironment(options = {}) {
14862
+ const {
14863
+ packageRoot = getPackageRoot(),
14864
+ force = false,
14865
+ verbose = false,
14866
+ timeout = 6e5
14867
+ // 10 minutes
14868
+ } = options;
14869
+ if (!force && isPythonEnvironmentSetup(packageRoot)) {
14870
+ if (verbose) {
14871
+ console.log("\u2713 Python environment already set up");
14872
+ }
14873
+ return {
14874
+ success: true,
14875
+ message: "Python environment already exists",
14876
+ alreadyExists: true
14877
+ };
14878
+ }
14879
+ const setupScriptPath = getSetupScriptPath(packageRoot);
14880
+ if (!(0, import_fs3.existsSync)(setupScriptPath)) {
14881
+ return {
14882
+ success: false,
14883
+ message: `Setup script not found at: ${setupScriptPath}`,
14884
+ alreadyExists: false
14885
+ };
14886
+ }
14887
+ try {
14888
+ if (verbose) {
14889
+ console.log("Setting up Python environment...");
14890
+ }
14891
+ const { stdout, stderr } = await execAsync4(`bash "${setupScriptPath}"`, {
14892
+ cwd: packageRoot,
14893
+ timeout,
14894
+ env: {
14895
+ ...process.env,
14896
+ // Ensure script can write to the directory
14897
+ PYTHONDONTWRITEBYTECODE: "1"
14898
+ },
14899
+ maxBuffer: 10 * 1024 * 1024
14900
+ // 10MB buffer
14901
+ });
14902
+ const output = stdout + stderr;
14903
+ const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
14904
+ const pythonVersion = versionMatch ? versionMatch[1] : void 0;
14905
+ if (verbose) {
14906
+ console.log(output);
14907
+ }
14908
+ return {
14909
+ success: true,
14910
+ message: "Python environment set up successfully",
14911
+ alreadyExists: false,
14912
+ pythonVersion,
14913
+ output
14914
+ };
14915
+ } catch (error) {
14916
+ const errorOutput = error.stdout + error.stderr;
14917
+ return {
14918
+ success: false,
14919
+ message: `Setup failed: ${error.message}`,
14920
+ alreadyExists: false,
14921
+ output: errorOutput
14922
+ };
14923
+ }
14924
+ }
14925
+ function getPythonSetupInstructions() {
14926
+ return `
14927
+ Python environment not set up. Please run one of the following commands:
14928
+
14929
+ Option 1 (Automatic):
14930
+ import { setupPythonEnvironment } from '@exulu/backend';
14931
+ await setupPythonEnvironment();
14932
+
14933
+ Option 2 (Manual - for package consumers):
14934
+ npx @exulu/backend setup-python
14935
+
14936
+ Option 3 (Manual - for contributors):
14937
+ npm run python:setup
14938
+
14939
+ These commands will automatically create a Python virtual environment (.venv)
14940
+ in the @exulu/backend package and install all required dependencies.
14941
+
14942
+ Requirements:
14943
+ - Python 3.10 or higher must be installed
14944
+ - pip must be available
14945
+ - venv module must be available (for creating virtual environments)
14946
+
14947
+ If Python dependencies are not installed, install them first, then run one of the commands above:
14948
+ - macOS: brew install python@3.12
14949
+ - Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
14950
+ - Alpine Linux: apk add python3 py3-pip python3-dev
14951
+ - Windows: Download from https://www.python.org/downloads/
14952
+
14953
+ Note: In Docker containers, ensure you install all three components:
14954
+ Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
14955
+ Alpine: apk add python3 py3-pip python3-dev
14956
+ `.trim();
14957
+ }
14958
+ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
14959
+ const root = packageRoot ?? getPackageRoot();
14960
+ const venvPath = getVenvPath(root);
14961
+ const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
14962
+ if (!(0, import_fs3.existsSync)(venvPath)) {
14963
+ return {
14964
+ valid: false,
14965
+ message: getPythonSetupInstructions()
14966
+ };
14967
+ }
14968
+ if (!(0, import_fs3.existsSync)(pythonPath)) {
14969
+ return {
14970
+ valid: false,
14971
+ message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
14972
+ };
14973
+ }
14974
+ try {
14975
+ await execAsync4(`"${pythonPath}" --version`, { cwd: root });
14976
+ } catch {
14977
+ return {
14978
+ valid: false,
14979
+ message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
14980
+ };
14981
+ }
14982
+ if (checkPackages) {
14983
+ const criticalPackages = ["docling", "transformers"];
14984
+ const missingPackages = [];
14985
+ for (const pkg of criticalPackages) {
14986
+ try {
14987
+ await execAsync4(`"${pythonPath}" -c "import ${pkg}"`, {
14988
+ cwd: root,
14989
+ timeout: 1e4
14990
+ // 10 second timeout per import check
14991
+ });
14992
+ } catch {
14993
+ missingPackages.push(pkg);
14994
+ }
14995
+ }
14996
+ if (missingPackages.length > 0) {
14997
+ return {
14998
+ valid: false,
14999
+ message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
15000
+
15001
+ This usually happens when:
15002
+ 1. The .venv folder was copied but dependencies were not installed
15003
+ 2. The package was installed via npm but setup script was not run
15004
+
15005
+ Please run:
15006
+ await setupPythonEnvironment({ force: true })
15007
+
15008
+ Or manually run the setup script:
15009
+ bash ` + getSetupScriptPath(root)
15010
+ };
15011
+ }
15012
+ }
15013
+ return {
15014
+ valid: true,
15015
+ message: "Python environment is valid"
15016
+ };
15017
+ }
15018
+
15019
+ // src/exulu/routes.ts
15020
+ init_tags();
15021
+ var import_multer = __toESM(require("multer"), 1);
15022
+
15023
+ // src/utils/check-provider-rate-limit.ts
15024
+ init_cjs_shims();
15025
+ var checkProviderRateLimit = async (provider) => {
15026
+ if (provider.rateLimit) {
15027
+ console.log("[EXULU] rate limiting provider.", provider.rateLimit);
15028
+ const limit = await providerRateLimiter(
15029
+ provider.rateLimit.name || provider.id,
15030
+ provider.rateLimit.rate_limit.time,
15031
+ provider.rateLimit.rate_limit.limit,
15032
+ 1
15033
+ );
15034
+ if (!limit.status) {
15035
+ throw new Error("Rate limit exceeded.");
15036
+ }
15037
+ }
15038
+ };
15039
+ var providerRateLimiter = async (key2, windowSeconds, limit, points) => {
15040
+ try {
15041
+ const { client: client2 } = await redisClient();
15042
+ if (!client2) {
15043
+ console.warn("[EXULU] Rate limiting disabled - Redis not available");
15044
+ return {
15045
+ status: true,
15046
+ retryAfter: null
15047
+ };
15048
+ }
15049
+ const redisKey = `exulu/${key2}`;
15050
+ const current = await client2.incrBy(redisKey, points);
15051
+ if (current === points) {
15052
+ await client2.expire(redisKey, windowSeconds);
15053
+ }
15054
+ if (current > limit) {
15055
+ const ttl = await client2.ttl(redisKey);
15056
+ return {
15057
+ status: false,
15058
+ retryAfter: ttl
15059
+ };
15060
+ }
15061
+ return {
15062
+ status: true,
15063
+ retryAfter: null
15064
+ };
15065
+ } catch (error) {
15066
+ console.error("[EXULU] Rate limiting error:", error);
15067
+ return {
15068
+ status: true,
15069
+ retryAfter: null
15070
+ };
15071
+ }
15072
+ };
15073
+
15074
+ // src/utils/check-api-key-scope.ts
15075
+ init_cjs_shims();
15076
+ function checkApiKeyScope(user, agentId) {
15077
+ if (!user || user.type !== "api") return { allowed: true };
15078
+ if (!user.scope_mode || user.scope_mode === "admin") return { allowed: true };
15079
+ if (user.scope_mode === "agents") {
15080
+ const ids = Array.isArray(user.agent_ids) ? user.agent_ids : [];
15081
+ if (!ids.includes(agentId)) {
15082
+ return {
15083
+ allowed: false,
15084
+ reason: `API key is not scoped to agent ${agentId}.`,
15085
+ code: 403
15086
+ };
15087
+ }
15088
+ return { allowed: true };
14100
15089
  }
14101
15090
  return { allowed: false, reason: "Unknown scope_mode.", code: 401 };
14102
15091
  }
@@ -14703,7 +15692,7 @@ var REQUEST_SIZE_LIMIT = "50mb";
14703
15692
  var getExuluVersionNumber = async () => {
14704
15693
  try {
14705
15694
  const path3 = process.cwd();
14706
- const packageJson = import_fs3.default.readFileSync(path3 + "/package.json", "utf8");
15695
+ const packageJson = import_fs4.default.readFileSync(path3 + "/package.json", "utf8");
14707
15696
  const packageData = JSON.parse(packageJson);
14708
15697
  const exuluVersion = packageData.dependencies["@exulu/backend"];
14709
15698
  console.log(`[EXULU] Installed exulu-backend version: ${exuluVersion}`);
@@ -14737,7 +15726,8 @@ var {
14737
15726
  contextPresetsSchema: contextPresetsSchema2,
14738
15727
  embedderSettingsSchema: embedderSettingsSchema2,
14739
15728
  promptFavoritesSchema: promptFavoritesSchema2,
14740
- statisticsSchema: statisticsSchema2
15729
+ statisticsSchema: statisticsSchema2,
15730
+ transcriptionJobsSchema: transcriptionJobsSchema2
14741
15731
  } = coreSchemas.get();
14742
15732
  var createExpressRoutes = async (app, providers, tools, contexts, config, evals, tracer, queues2, rerankers) => {
14743
15733
  let corsOptions = {
@@ -14795,7 +15785,8 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
14795
15785
  variablesSchema2(),
14796
15786
  workflowTemplatesSchema2(),
14797
15787
  statisticsSchema2(),
14798
- rbacSchema2()
15788
+ rbacSchema2(),
15789
+ transcriptionJobsSchema2()
14799
15790
  ],
14800
15791
  contexts ?? [],
14801
15792
  providers,
@@ -15618,6 +16609,487 @@ ${customInstructions}` : agent.instructions;
15618
16609
  }
15619
16610
  }
15620
16611
  );
16612
+ const imageModelsByName = (() => {
16613
+ if (!isLiteLLMEnabled() || !config?.fileUploads) return /* @__PURE__ */ new Map();
16614
+ try {
16615
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path5.resolve)(getPackageRoot(), "./config.litellm.yaml");
16616
+ const models2 = parseImageGenerationModels(configPath);
16617
+ return new Map(models2.map((m) => [m.model_name, m]));
16618
+ } catch (err) {
16619
+ console.error(
16620
+ "[EXULU] Skipping /images/* routes due to config.litellm.yaml error:",
16621
+ err.message
16622
+ );
16623
+ return /* @__PURE__ */ new Map();
16624
+ }
16625
+ })();
16626
+ const imageRoutesEnabled = isLiteLLMEnabled() && !!config?.fileUploads?.s3region && !!config?.fileUploads?.s3key && !!config?.fileUploads?.s3secret && !!config?.fileUploads?.s3Bucket && imageModelsByName.size > 0;
16627
+ const respond503ImagesNotEnabled = (res) => {
16628
+ res.status(503).json({
16629
+ detail: "Image generation is not enabled on this deployment. Requires EXULU_USE_LITELLM=true, S3 fileUploads configuration, and at least one model in config.litellm.yaml with model_info.type=image_generation."
16630
+ });
16631
+ };
16632
+ const loadAuthedSession = async (req, res, sessionId, rights) => {
16633
+ const authResult = await requestValidators.authenticate(req);
16634
+ if (!authResult.user?.id) {
16635
+ res.status(authResult.code || 401).json({ detail: authResult.message });
16636
+ return null;
16637
+ }
16638
+ const { db: db2 } = await postgresClient();
16639
+ const session = await db2.from("agent_sessions").where({ id: sessionId }).first();
16640
+ if (!session) {
16641
+ res.status(404).json({ detail: `Session ${sessionId} not found.` });
16642
+ return null;
16643
+ }
16644
+ const sessionRbac = await RBACResolver(
16645
+ db2,
16646
+ "agent_sessions",
16647
+ session.id,
16648
+ session.rights_mode || "private"
16649
+ );
16650
+ const allowed = await checkRecordAccess(
16651
+ { ...session, RBAC: sessionRbac },
16652
+ rights,
16653
+ authResult.user
16654
+ );
16655
+ if (!allowed) {
16656
+ res.status(403).json({ detail: `You don't have ${rights} access to this session.` });
16657
+ return null;
16658
+ }
16659
+ return { user: authResult.user, session, db: db2 };
16660
+ };
16661
+ const loadStyle = async (db2, styleId, user, res) => {
16662
+ if (!styleId) return { markdown: null, id: null };
16663
+ const row = await db2.from("platform_configurations").where({ id: styleId }).first();
16664
+ if (!row) {
16665
+ res.status(404).json({ detail: `Style ${styleId} not found.` });
16666
+ return "error";
16667
+ }
16668
+ const rbac = await RBACResolver(
16669
+ db2,
16670
+ "platform_configurations",
16671
+ row.id,
16672
+ row.rights_mode || "private"
16673
+ );
16674
+ const allowed = await checkRecordAccess(
16675
+ { ...row, RBAC: rbac },
16676
+ "read",
16677
+ user
16678
+ );
16679
+ if (!allowed) {
16680
+ res.status(403).json({ detail: "You don't have access to that style." });
16681
+ return "error";
16682
+ }
16683
+ const value = typeof row.config_value === "string" ? (() => {
16684
+ try {
16685
+ return JSON.parse(row.config_value);
16686
+ } catch {
16687
+ return null;
16688
+ }
16689
+ })() : row.config_value;
16690
+ return { markdown: value?.markdown ?? null, id: row.id };
16691
+ };
16692
+ const validateGenerationParams = (body, res) => {
16693
+ const { model: modelName, prompt, n, size, quality } = body || {};
16694
+ const model = typeof modelName === "string" ? imageModelsByName.get(modelName) : void 0;
16695
+ if (!model) {
16696
+ res.status(400).json({
16697
+ detail: `Unknown image-generation model "${modelName}". Available: ${[...imageModelsByName.keys()].join(", ")}.`
16698
+ });
16699
+ return null;
16700
+ }
16701
+ if (typeof prompt !== "string" || prompt.trim().length === 0) {
16702
+ res.status(400).json({ detail: "prompt must be a non-empty string." });
16703
+ return null;
16704
+ }
16705
+ const requestedN = typeof n === "number" ? n : 1;
16706
+ if (!Number.isInteger(requestedN) || requestedN < 1 || requestedN > model.max_n) {
16707
+ res.status(400).json({
16708
+ detail: `n must be an integer between 1 and ${model.max_n} for model ${model.model_name}.`
16709
+ });
16710
+ return null;
16711
+ }
16712
+ if (size && !model.sizes.includes(size)) {
16713
+ res.status(400).json({
16714
+ detail: `size "${size}" is not supported by ${model.model_name}. Allowed: ${model.sizes.join(", ")}.`
16715
+ });
16716
+ return null;
16717
+ }
16718
+ if (quality && !model.qualities.includes(quality)) {
16719
+ res.status(400).json({
16720
+ detail: `quality "${quality}" is not supported by ${model.model_name}. Allowed: ${model.qualities.join(", ")}.`
16721
+ });
16722
+ return null;
16723
+ }
16724
+ return { model, prompt, n: requestedN, size, quality };
16725
+ };
16726
+ const uploadGeneratedImages = async (images, sessionId, toolCallId, userId) => {
16727
+ if (!config?.fileUploads) {
16728
+ throw new Error("File uploads not configured.");
16729
+ }
16730
+ const keys = [];
16731
+ const revisedPrompts = [];
16732
+ for (const img of images) {
16733
+ const filename = `${(0, import_node_crypto5.randomUUID)()}.${img.extension}`;
16734
+ const key2 = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
16735
+ const fullKey = await uploadFile(
16736
+ img.buffer,
16737
+ key2,
16738
+ config,
16739
+ { contentType: img.contentType },
16740
+ userId
16741
+ );
16742
+ keys.push(fullKey);
16743
+ revisedPrompts.push(img.revisedPrompt ?? null);
16744
+ }
16745
+ const presignedUrls = await Promise.all(
16746
+ keys.map((fullKey) => {
16747
+ const slash = fullKey.indexOf("/");
16748
+ const bucket = slash > 0 ? fullKey.slice(0, slash) : config.fileUploads.s3Bucket;
16749
+ const objectKey = slash > 0 ? fullKey.slice(slash + 1) : fullKey;
16750
+ return getPresignedUrl(bucket, objectKey, config);
16751
+ })
16752
+ );
16753
+ return { keys, revisedPrompts, presignedUrls };
16754
+ };
16755
+ app.post("/images/generate", async (req, res) => {
16756
+ if (!imageRoutesEnabled) return respond503ImagesNotEnabled(res);
16757
+ const { sessionId, toolCallId, styleId } = req.body || {};
16758
+ if (typeof sessionId !== "string" || typeof toolCallId !== "string") {
16759
+ res.status(400).json({ detail: "sessionId and toolCallId are required." });
16760
+ return;
16761
+ }
16762
+ const authed = await loadAuthedSession(req, res, sessionId, "write");
16763
+ if (!authed) return;
16764
+ const params = validateGenerationParams(req.body, res);
16765
+ if (!params) return;
16766
+ const style = await loadStyle(authed.db, styleId, authed.user, res);
16767
+ if (style === "error") return;
16768
+ const finalPrompt = style.markdown ? `${params.prompt}
16769
+
16770
+ ${style.markdown}` : params.prompt;
16771
+ try {
16772
+ await Promise.race([
16773
+ waitForLiteLLMReady(),
16774
+ new Promise(
16775
+ (_, reject) => setTimeout(() => reject(new Error("LiteLLM not ready")), 5e3)
16776
+ )
16777
+ ]);
16778
+ } catch {
16779
+ res.status(503).json({ detail: "Image service is not ready. Try again shortly." });
16780
+ return;
16781
+ }
16782
+ const abortController = new AbortController();
16783
+ req.on("close", () => abortController.abort());
16784
+ try {
16785
+ const images = await generateImage({
16786
+ model: params.model.model_name,
16787
+ prompt: finalPrompt,
16788
+ n: params.n,
16789
+ size: params.size,
16790
+ quality: params.quality,
16791
+ signal: abortController.signal
16792
+ });
16793
+ const { keys, revisedPrompts, presignedUrls } = await uploadGeneratedImages(
16794
+ images,
16795
+ sessionId,
16796
+ toolCallId,
16797
+ authed.user.id
16798
+ );
16799
+ const [row] = await authed.db("image_generations").insert({
16800
+ session_id: sessionId,
16801
+ tool_call_id: toolCallId,
16802
+ user_id: authed.user.id,
16803
+ operation: "generate",
16804
+ model: params.model.model_name,
16805
+ prompt: params.prompt,
16806
+ applied_style_id: style.id,
16807
+ applied_style_markdown: style.markdown,
16808
+ size: params.size,
16809
+ quality: params.quality,
16810
+ n: params.n,
16811
+ image_keys: JSON.stringify(keys),
16812
+ revised_prompts: JSON.stringify(revisedPrompts),
16813
+ selected: false
16814
+ }).returning("*");
16815
+ res.status(200).json({
16816
+ generationId: row.id,
16817
+ images: keys.map((key2, i) => ({
16818
+ key: key2,
16819
+ presignedUrl: presignedUrls[i],
16820
+ revisedPrompt: revisedPrompts[i]
16821
+ }))
16822
+ });
16823
+ } catch (err) {
16824
+ if (abortController.signal.aborted) return;
16825
+ if (err instanceof ImageGenerationError) {
16826
+ const code = err.upstreamStatus >= 500 ? 502 : err.upstreamStatus;
16827
+ res.status(code).json({ detail: err.message });
16828
+ return;
16829
+ }
16830
+ console.error("[EXULU] /images/generate failed", err);
16831
+ res.status(500).json({
16832
+ detail: err instanceof Error ? err.message : "Image generation failed."
16833
+ });
16834
+ }
16835
+ });
16836
+ app.post("/images/edit", async (req, res) => {
16837
+ if (!imageRoutesEnabled) return respond503ImagesNotEnabled(res);
16838
+ const { sessionId, toolCallId, styleId, referenceImageKeys, maskKey } = req.body || {};
16839
+ if (typeof sessionId !== "string" || typeof toolCallId !== "string") {
16840
+ res.status(400).json({ detail: "sessionId and toolCallId are required." });
16841
+ return;
16842
+ }
16843
+ if (!Array.isArray(referenceImageKeys) || referenceImageKeys.length === 0) {
16844
+ res.status(400).json({ detail: "referenceImageKeys must be a non-empty array." });
16845
+ return;
16846
+ }
16847
+ const authed = await loadAuthedSession(req, res, sessionId, "write");
16848
+ if (!authed) return;
16849
+ const params = validateGenerationParams(req.body, res);
16850
+ if (!params) return;
16851
+ if (!params.model.supports_edit) {
16852
+ res.status(400).json({
16853
+ detail: `Model ${params.model.model_name} does not support image editing.`
16854
+ });
16855
+ return;
16856
+ }
16857
+ const userPrefix = `user_${authed.user.id}/`;
16858
+ const sessionPrefix = `sessions/${sessionId}/`;
16859
+ const ownsKey = (k) => k.includes(userPrefix) || k.includes(sessionPrefix);
16860
+ if (!referenceImageKeys.every((k) => typeof k === "string" && ownsKey(k))) {
16861
+ res.status(403).json({ detail: "One or more reference image keys are not accessible." });
16862
+ return;
16863
+ }
16864
+ if (maskKey && (typeof maskKey !== "string" || !ownsKey(maskKey))) {
16865
+ res.status(403).json({ detail: "Mask image is not accessible." });
16866
+ return;
16867
+ }
16868
+ const style = await loadStyle(authed.db, styleId, authed.user, res);
16869
+ if (style === "error") return;
16870
+ const finalPrompt = style.markdown ? `${params.prompt}
16871
+
16872
+ ${style.markdown}` : params.prompt;
16873
+ try {
16874
+ await Promise.race([
16875
+ waitForLiteLLMReady(),
16876
+ new Promise(
16877
+ (_, reject) => setTimeout(() => reject(new Error("LiteLLM not ready")), 5e3)
16878
+ )
16879
+ ]);
16880
+ } catch {
16881
+ res.status(503).json({ detail: "Image service is not ready. Try again shortly." });
16882
+ return;
16883
+ }
16884
+ const fetchRef = async (fullKey) => {
16885
+ const slash = fullKey.indexOf("/");
16886
+ const objectKey = slash > 0 ? fullKey.slice(slash + 1) : fullKey;
16887
+ const buf = await getS3ObjectBytes(objectKey, config);
16888
+ const filename = fullKey.split("/").pop() ?? "image.png";
16889
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "png";
16890
+ const mimetype = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`;
16891
+ return { buffer: buf, filename, mimetype };
16892
+ };
16893
+ const abortController = new AbortController();
16894
+ req.on("close", () => abortController.abort());
16895
+ try {
16896
+ const references = await Promise.all(referenceImageKeys.map(fetchRef));
16897
+ const mask = maskKey ? await fetchRef(maskKey) : void 0;
16898
+ const images = await editImage({
16899
+ model: params.model.model_name,
16900
+ prompt: finalPrompt,
16901
+ references,
16902
+ mask,
16903
+ n: params.n,
16904
+ size: params.size,
16905
+ quality: params.quality,
16906
+ signal: abortController.signal
16907
+ });
16908
+ const { keys, revisedPrompts, presignedUrls } = await uploadGeneratedImages(
16909
+ images,
16910
+ sessionId,
16911
+ toolCallId,
16912
+ authed.user.id
16913
+ );
16914
+ const [row] = await authed.db("image_generations").insert({
16915
+ session_id: sessionId,
16916
+ tool_call_id: toolCallId,
16917
+ user_id: authed.user.id,
16918
+ operation: "edit",
16919
+ model: params.model.model_name,
16920
+ prompt: params.prompt,
16921
+ applied_style_id: style.id,
16922
+ applied_style_markdown: style.markdown,
16923
+ size: params.size,
16924
+ quality: params.quality,
16925
+ n: params.n,
16926
+ reference_image_keys: JSON.stringify(referenceImageKeys),
16927
+ mask_image_key: maskKey,
16928
+ image_keys: JSON.stringify(keys),
16929
+ revised_prompts: JSON.stringify(revisedPrompts),
16930
+ selected: false
16931
+ }).returning("*");
16932
+ res.status(200).json({
16933
+ generationId: row.id,
16934
+ images: keys.map((key2, i) => ({
16935
+ key: key2,
16936
+ presignedUrl: presignedUrls[i],
16937
+ revisedPrompt: revisedPrompts[i]
16938
+ }))
16939
+ });
16940
+ } catch (err) {
16941
+ if (abortController.signal.aborted) return;
16942
+ if (err instanceof ImageGenerationError) {
16943
+ const code = err.upstreamStatus >= 500 ? 502 : err.upstreamStatus;
16944
+ res.status(code).json({ detail: err.message });
16945
+ return;
16946
+ }
16947
+ console.error("[EXULU] /images/edit failed", err);
16948
+ res.status(500).json({
16949
+ detail: err instanceof Error ? err.message : "Image edit failed."
16950
+ });
16951
+ }
16952
+ });
16953
+ app.post("/images/select", async (req, res) => {
16954
+ if (!imageRoutesEnabled) return respond503ImagesNotEnabled(res);
16955
+ const { sessionId, toolCallId, selections } = req.body || {};
16956
+ if (typeof sessionId !== "string" || typeof toolCallId !== "string") {
16957
+ res.status(400).json({ detail: "sessionId and toolCallId are required." });
16958
+ return;
16959
+ }
16960
+ if (!Array.isArray(selections) || selections.length === 0) {
16961
+ res.status(400).json({ detail: "selections must be a non-empty array." });
16962
+ return;
16963
+ }
16964
+ const authed = await loadAuthedSession(req, res, sessionId, "write");
16965
+ if (!authed) return;
16966
+ const rows = await authed.db("image_generations").where({ session_id: sessionId, tool_call_id: toolCallId }).select("*");
16967
+ const rowsById = new Map(rows.map((r) => [r.id, r]));
16968
+ const selectedDetails = [];
16969
+ const rowsToMarkSelected = /* @__PURE__ */ new Set();
16970
+ for (const sel of selections) {
16971
+ if (typeof sel?.generationId !== "string" || typeof sel?.imageKey !== "string") {
16972
+ res.status(400).json({ detail: "Each selection needs generationId + imageKey." });
16973
+ return;
16974
+ }
16975
+ const row = rowsById.get(sel.generationId);
16976
+ if (!row) {
16977
+ res.status(404).json({ detail: `Generation ${sel.generationId} not found in this tool call.` });
16978
+ return;
16979
+ }
16980
+ const keys = Array.isArray(row.image_keys) ? row.image_keys : (() => {
16981
+ try {
16982
+ return JSON.parse(row.image_keys);
16983
+ } catch {
16984
+ return [];
16985
+ }
16986
+ })();
16987
+ if (!keys.includes(sel.imageKey)) {
16988
+ res.status(400).json({ detail: `imageKey not part of generation ${sel.generationId}.` });
16989
+ return;
16990
+ }
16991
+ const slash = sel.imageKey.indexOf("/");
16992
+ const bucket = slash > 0 ? sel.imageKey.slice(0, slash) : config.fileUploads.s3Bucket;
16993
+ const objectKey = slash > 0 ? sel.imageKey.slice(slash + 1) : sel.imageKey;
16994
+ const presignedUrl = await getPresignedUrl(bucket, objectKey, config);
16995
+ let styleName = null;
16996
+ if (row.applied_style_id) {
16997
+ const styleRow = await authed.db("platform_configurations").where({ id: row.applied_style_id }).first();
16998
+ const parsed = styleRow?.config_value && typeof styleRow.config_value === "string" ? (() => {
16999
+ try {
17000
+ return JSON.parse(styleRow.config_value);
17001
+ } catch {
17002
+ return null;
17003
+ }
17004
+ })() : styleRow?.config_value;
17005
+ styleName = parsed?.name ?? null;
17006
+ }
17007
+ selectedDetails.push({
17008
+ key: sel.imageKey,
17009
+ presignedUrl,
17010
+ prompt: row.prompt,
17011
+ model: row.model,
17012
+ styleName
17013
+ });
17014
+ rowsToMarkSelected.add(row.id);
17015
+ }
17016
+ await authed.db("image_generations").whereIn("id", [...rowsToMarkSelected]).update({ selected: true });
17017
+ const lines = selectedDetails.map(
17018
+ (d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
17019
+ );
17020
+ const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
17021
+ const messageId = (0, import_node_crypto5.randomUUID)();
17022
+ const uiMessage = {
17023
+ id: messageId,
17024
+ role: "system",
17025
+ parts: [{ type: "text", text: messageText }]
17026
+ };
17027
+ await authed.db("agent_messages").insert({
17028
+ content: JSON.stringify(uiMessage),
17029
+ message_id: messageId,
17030
+ session: sessionId,
17031
+ user: authed.user.id
17032
+ });
17033
+ res.status(200).json({ ok: true, systemMessage: uiMessage, selectedImages: selectedDetails });
17034
+ });
17035
+ app.get("/images/history", async (req, res) => {
17036
+ if (!imageRoutesEnabled) return respond503ImagesNotEnabled(res);
17037
+ const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : "";
17038
+ const toolCallId = typeof req.query.toolCallId === "string" ? req.query.toolCallId : "";
17039
+ if (!sessionId || !toolCallId) {
17040
+ res.status(400).json({ detail: "sessionId and toolCallId query params are required." });
17041
+ return;
17042
+ }
17043
+ const authed = await loadAuthedSession(req, res, sessionId, "read");
17044
+ if (!authed) return;
17045
+ const rows = await authed.db("image_generations").where({ session_id: sessionId, tool_call_id: toolCallId }).orderBy("createdAt", "asc").select("*");
17046
+ const parseList = (v) => {
17047
+ if (!v) return [];
17048
+ if (Array.isArray(v)) return v;
17049
+ try {
17050
+ return JSON.parse(v);
17051
+ } catch {
17052
+ return [];
17053
+ }
17054
+ };
17055
+ const sign = async (fullKey) => {
17056
+ const slash = fullKey.indexOf("/");
17057
+ const bucket = slash > 0 ? fullKey.slice(0, slash) : config.fileUploads.s3Bucket;
17058
+ const objectKey = slash > 0 ? fullKey.slice(slash + 1) : fullKey;
17059
+ return getPresignedUrl(bucket, objectKey, config);
17060
+ };
17061
+ const history = await Promise.all(rows.map(async (r) => {
17062
+ const keys = parseList(r.image_keys);
17063
+ const refs = parseList(r.reference_image_keys);
17064
+ const revisedPrompts = parseList(r.revised_prompts);
17065
+ const [imageUrls, referenceUrls] = await Promise.all([
17066
+ Promise.all(keys.map(sign)),
17067
+ Promise.all(refs.map(sign))
17068
+ ]);
17069
+ return {
17070
+ generationId: r.id,
17071
+ createdAt: r.createdAt,
17072
+ operation: r.operation,
17073
+ model: r.model,
17074
+ prompt: r.prompt,
17075
+ appliedStyleId: r.applied_style_id ?? null,
17076
+ appliedStyleMarkdown: r.applied_style_markdown ?? null,
17077
+ size: r.size ?? null,
17078
+ quality: r.quality ?? null,
17079
+ n: r.n ?? 1,
17080
+ selected: !!r.selected,
17081
+ error: r.error ?? null,
17082
+ maskImageKey: r.mask_image_key ?? null,
17083
+ images: keys.map((key2, i) => ({
17084
+ key: key2,
17085
+ presignedUrl: imageUrls[i],
17086
+ revisedPrompt: revisedPrompts[i] ?? null
17087
+ })),
17088
+ references: refs.map((key2, i) => ({ key: key2, presignedUrl: referenceUrls[i] }))
17089
+ };
17090
+ }));
17091
+ res.status(200).json({ history });
17092
+ });
15621
17093
  app.use("/litellm/:project", async (req, res) => {
15622
17094
  if (!isLiteLLMEnabled()) {
15623
17095
  res.status(503).json({
@@ -18681,7 +20153,7 @@ var internetSearchTool = new ExuluTool({
18681
20153
  } catch (error) {
18682
20154
  if (error instanceof import_perplexity_ai.default.RateLimitError && attempt < maxRetries - 1) {
18683
20155
  const delay = Math.pow(2, attempt) * 1e3 + Math.random() * 1e3;
18684
- await new Promise((resolve6) => setTimeout(resolve6, delay));
20156
+ await new Promise((resolve7) => setTimeout(resolve7, delay));
18685
20157
  continue;
18686
20158
  }
18687
20159
  throw error;
@@ -18798,7 +20270,7 @@ init_supervisor();
18798
20270
  init_uppy();
18799
20271
  var import_node_crypto8 = require("crypto");
18800
20272
  var import_promises3 = require("fs/promises");
18801
- var import_node_path5 = require("path");
20273
+ var import_node_path6 = require("path");
18802
20274
  var import_zod20 = require("zod");
18803
20275
  var SANDBOX_ROOT = "/tmp/exulu-sessions";
18804
20276
  var parseSandboxPath = (input) => {
@@ -18970,7 +20442,7 @@ var transcribeTool = new ExuluTool({
18970
20442
  });
18971
20443
  let sandboxLocalPath;
18972
20444
  if (sessionID) {
18973
- sandboxLocalPath = (0, import_node_path5.join)(
20445
+ sandboxLocalPath = (0, import_node_path6.join)(
18974
20446
  SANDBOX_ROOT,
18975
20447
  sessionID,
18976
20448
  "transcripts",
@@ -18980,27 +20452,136 @@ var transcribeTool = new ExuluTool({
18980
20452
  sandboxLocalPath
18981
20453
  });
18982
20454
  try {
18983
- await (0, import_promises3.mkdir)((0, import_node_path5.dirname)(sandboxLocalPath), { recursive: true });
20455
+ await (0, import_promises3.mkdir)((0, import_node_path6.dirname)(sandboxLocalPath), { recursive: true });
18984
20456
  await (0, import_promises3.writeFile)(sandboxLocalPath, transcriptBuffer);
18985
20457
  } catch (err) {
18986
20458
  console.error(
18987
20459
  `[EXULU] Failed to write transcript to sandbox dir ${sandboxLocalPath}; S3 copy is unaffected.`,
18988
20460
  err
18989
20461
  );
18990
- sandboxLocalPath = void 0;
20462
+ sandboxLocalPath = void 0;
20463
+ }
20464
+ }
20465
+ console.log("[EXULU] Transcribed audio successfully", {
20466
+ text,
20467
+ url,
20468
+ sandboxLocalPath,
20469
+ length: text.length
20470
+ });
20471
+ return {
20472
+ result: sandboxLocalPath ? `Transcript stored at: ${url} (also available in the session sandbox at ${sandboxLocalPath}, ${text.length} characters).` : `${url}`
20473
+ };
20474
+ }
20475
+ });
20476
+
20477
+ // src/templates/tools/image-generation.ts
20478
+ init_cjs_shims();
20479
+ init_tool();
20480
+ init_supervisor();
20481
+ init_client();
20482
+ init_check_record_access();
20483
+ var import_zod21 = require("zod");
20484
+ var _cachedImageModels;
20485
+ var setCachedImageModels = (models2) => {
20486
+ _cachedImageModels = models2;
20487
+ };
20488
+ var buildDefaults = (models2) => {
20489
+ const m = models2[0];
20490
+ if (!m) {
20491
+ return { model: "", size: "1024x1024", quality: "auto", n: 1 };
20492
+ }
20493
+ return {
20494
+ model: m.model_name,
20495
+ size: m.sizes.includes("1024x1024") ? "1024x1024" : m.sizes[0],
20496
+ quality: m.qualities.includes("auto") ? "auto" : m.qualities[0],
20497
+ n: 1
20498
+ };
20499
+ };
20500
+ var loadAvailableStyles = async (user) => {
20501
+ if (!user?.id) return [];
20502
+ const { db: db2 } = await postgresClient();
20503
+ const rows = await db2.from("platform_configurations").where("config_key", "like", "image_generation_style:%").select("*");
20504
+ const visible = [];
20505
+ for (const row of rows) {
20506
+ const rbac = await RBACResolver(
20507
+ db2,
20508
+ "platform_configurations",
20509
+ row.id,
20510
+ row.rights_mode || "private"
20511
+ );
20512
+ const hasAccess = await checkRecordAccess(
20513
+ { ...row, RBAC: rbac },
20514
+ "read",
20515
+ user
20516
+ );
20517
+ if (!hasAccess) continue;
20518
+ const value = typeof row.config_value === "string" ? safeJsonParse(row.config_value) : row.config_value;
20519
+ visible.push({
20520
+ id: row.id,
20521
+ name: value?.name ?? row.config_key.replace(/^image_generation_style:/, ""),
20522
+ description: row.description ?? null,
20523
+ owner: String(row.created_by) === String(user.id) ? "user" : "shared"
20524
+ });
20525
+ }
20526
+ return visible;
20527
+ };
20528
+ var safeJsonParse = (s) => {
20529
+ try {
20530
+ return JSON.parse(s);
20531
+ } catch {
20532
+ return null;
20533
+ }
20534
+ };
20535
+ var createImageGenerationWidgetTool = (models2) => {
20536
+ setCachedImageModels(models2);
20537
+ return new ExuluTool({
20538
+ id: "image_generation",
20539
+ name: "image_generation",
20540
+ description: "Open an in-chat image generation widget pre-filled with your prompt. The user picks the model, size, quality and count, optionally attaches reference images for editing, applies a saved style, generates one or more candidates, and selects the final image(s) to share back into the conversation. Use this whenever the user asks to create or edit an image.",
20541
+ needsApproval: false,
20542
+ type: "function",
20543
+ config: [],
20544
+ inputSchema: import_zod21.z.object({
20545
+ prompt: import_zod21.z.string().describe(
20546
+ "Initial image prompt. The user can edit it before generating."
20547
+ )
20548
+ }),
20549
+ execute: async ({ prompt, user, sessionID }, options) => {
20550
+ if (!isLiteLLMEnabled()) {
20551
+ throw new Error(
20552
+ "Image generation is not enabled on this deployment (EXULU_USE_LITELLM is not 'true')."
20553
+ );
20554
+ }
20555
+ if (!_cachedImageModels || _cachedImageModels.length === 0) {
20556
+ throw new Error(
20557
+ "No image-generation models are registered in config.litellm.yaml."
20558
+ );
18991
20559
  }
20560
+ const toolCallId = options?.toolCallId;
20561
+ const styles = await loadAvailableStyles(user);
20562
+ return {
20563
+ result: JSON.stringify({
20564
+ type: "image_generation_widget",
20565
+ toolCallId,
20566
+ sessionId: sessionID,
20567
+ initialPrompt: prompt,
20568
+ models: _cachedImageModels.map((m) => ({
20569
+ name: m.model_name,
20570
+ sizes: m.sizes,
20571
+ qualities: m.qualities,
20572
+ supportsEdit: m.supports_edit,
20573
+ maxN: m.max_n
20574
+ })),
20575
+ styles,
20576
+ defaults: buildDefaults(_cachedImageModels)
20577
+ })
20578
+ };
18992
20579
  }
18993
- console.log("[EXULU] Transcribed audio successfully", {
18994
- text,
18995
- url,
18996
- sandboxLocalPath,
18997
- length: text.length
18998
- });
18999
- return {
19000
- result: sandboxLocalPath ? `Transcript stored at: ${url} (also available in the session sandbox at ${sandboxLocalPath}, ${text.length} characters).` : `${url}`
19001
- };
19002
- }
19003
- });
20580
+ });
20581
+ };
20582
+
20583
+ // src/exulu/app/index.ts
20584
+ var import_node_path7 = require("path");
19004
20585
 
19005
20586
  // src/validators/postgres-name.ts
19006
20587
  init_cjs_shims();
@@ -19018,209 +20599,69 @@ init_entitlements();
19018
20599
  init_system_dependencies();
19019
20600
  init_supervisor();
19020
20601
 
19021
- // src/utils/python-setup.ts
20602
+ // src/templates/contexts/index.ts
19022
20603
  init_cjs_shims();
19023
- var import_child_process = require("child_process");
19024
- var import_util = require("util");
19025
- var import_path = require("path");
19026
- var import_fs4 = require("fs");
19027
- var import_url = require("url");
19028
- var execAsync4 = (0, import_util.promisify)(import_child_process.exec);
19029
- function getPackageRoot() {
19030
- const currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
19031
- let currentDir = (0, import_path.dirname)(currentFile);
19032
- let attempts = 0;
19033
- const maxAttempts = 10;
19034
- while (attempts < maxAttempts) {
19035
- const packageJsonPath = (0, import_path.join)(currentDir, "package.json");
19036
- if ((0, import_fs4.existsSync)(packageJsonPath)) {
19037
- try {
19038
- const packageJson = JSON.parse((0, import_fs4.readFileSync)(packageJsonPath, "utf-8"));
19039
- if (packageJson.name === "@exulu/backend") {
19040
- return currentDir;
19041
- }
19042
- } catch {
19043
- }
19044
- }
19045
- const parentDir = (0, import_path.resolve)(currentDir, "..");
19046
- if (parentDir === currentDir) {
19047
- break;
19048
- }
19049
- currentDir = parentDir;
19050
- attempts++;
19051
- }
19052
- const fallback = (0, import_path.resolve)((0, import_path.dirname)((0, import_url.fileURLToPath)(importMetaUrl)), "../..");
19053
- return fallback;
19054
- }
19055
- function getSetupScriptPath(packageRoot) {
19056
- return (0, import_path.resolve)(packageRoot, "ee/python/setup.sh");
19057
- }
19058
- function getVenvPath(packageRoot) {
19059
- return (0, import_path.resolve)(packageRoot, "ee/python/.venv");
19060
- }
19061
- function isPythonEnvironmentSetup(packageRoot) {
19062
- const root = packageRoot ?? getPackageRoot();
19063
- const venvPath = getVenvPath(root);
19064
- const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
19065
- return (0, import_fs4.existsSync)(venvPath) && (0, import_fs4.existsSync)(pythonPath);
19066
- }
19067
- async function setupPythonEnvironment(options = {}) {
19068
- const {
19069
- packageRoot = getPackageRoot(),
19070
- force = false,
19071
- verbose = false,
19072
- timeout = 6e5
19073
- // 10 minutes
19074
- } = options;
19075
- if (!force && isPythonEnvironmentSetup(packageRoot)) {
19076
- if (verbose) {
19077
- console.log("\u2713 Python environment already set up");
19078
- }
19079
- return {
19080
- success: true,
19081
- message: "Python environment already exists",
19082
- alreadyExists: true
19083
- };
19084
- }
19085
- const setupScriptPath = getSetupScriptPath(packageRoot);
19086
- if (!(0, import_fs4.existsSync)(setupScriptPath)) {
19087
- return {
19088
- success: false,
19089
- message: `Setup script not found at: ${setupScriptPath}`,
19090
- alreadyExists: false
19091
- };
19092
- }
19093
- try {
19094
- if (verbose) {
19095
- console.log("Setting up Python environment...");
19096
- }
19097
- const { stdout, stderr } = await execAsync4(`bash "${setupScriptPath}"`, {
19098
- cwd: packageRoot,
19099
- timeout,
19100
- env: {
19101
- ...process.env,
19102
- // Ensure script can write to the directory
19103
- PYTHONDONTWRITEBYTECODE: "1"
19104
- },
19105
- maxBuffer: 10 * 1024 * 1024
19106
- // 10MB buffer
19107
- });
19108
- const output = stdout + stderr;
19109
- const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
19110
- const pythonVersion = versionMatch ? versionMatch[1] : void 0;
19111
- if (verbose) {
19112
- console.log(output);
19113
- }
19114
- return {
19115
- success: true,
19116
- message: "Python environment set up successfully",
19117
- alreadyExists: false,
19118
- pythonVersion,
19119
- output
19120
- };
19121
- } catch (error) {
19122
- const errorOutput = error.stdout + error.stderr;
19123
- return {
19124
- success: false,
19125
- message: `Setup failed: ${error.message}`,
19126
- alreadyExists: false,
19127
- output: errorOutput
19128
- };
19129
- }
19130
- }
19131
- function getPythonSetupInstructions() {
19132
- return `
19133
- Python environment not set up. Please run one of the following commands:
19134
-
19135
- Option 1 (Automatic):
19136
- import { setupPythonEnvironment } from '@exulu/backend';
19137
- await setupPythonEnvironment();
19138
-
19139
- Option 2 (Manual - for package consumers):
19140
- npx @exulu/backend setup-python
19141
-
19142
- Option 3 (Manual - for contributors):
19143
- npm run python:setup
19144
-
19145
- These commands will automatically create a Python virtual environment (.venv)
19146
- in the @exulu/backend package and install all required dependencies.
19147
-
19148
- Requirements:
19149
- - Python 3.10 or higher must be installed
19150
- - pip must be available
19151
- - venv module must be available (for creating virtual environments)
19152
-
19153
- If Python dependencies are not installed, install them first, then run one of the commands above:
19154
- - macOS: brew install python@3.12
19155
- - Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
19156
- - Alpine Linux: apk add python3 py3-pip python3-dev
19157
- - Windows: Download from https://www.python.org/downloads/
19158
20604
 
19159
- Note: In Docker containers, ensure you install all three components:
19160
- Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
19161
- Alpine: apk add python3 py3-pip python3-dev
19162
- `.trim();
19163
- }
19164
- async function validatePythonEnvironment(packageRoot, checkPackages = true) {
19165
- const root = packageRoot ?? getPackageRoot();
19166
- const venvPath = getVenvPath(root);
19167
- const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
19168
- if (!(0, import_fs4.existsSync)(venvPath)) {
19169
- return {
19170
- valid: false,
19171
- message: getPythonSetupInstructions()
19172
- };
19173
- }
19174
- if (!(0, import_fs4.existsSync)(pythonPath)) {
19175
- return {
19176
- valid: false,
19177
- message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
19178
- };
19179
- }
19180
- try {
19181
- await execAsync4(`"${pythonPath}" --version`, { cwd: root });
19182
- } catch {
19183
- return {
19184
- valid: false,
19185
- message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
19186
- };
20605
+ // src/templates/contexts/transcriptions.ts
20606
+ init_cjs_shims();
20607
+ init_context();
20608
+ var transcriptionsContext = new ExuluContext2({
20609
+ id: "transcriptions",
20610
+ name: "Transcriptions",
20611
+ description: "Diarized audio transcripts",
20612
+ fields: [
20613
+ { name: "transcript_text", type: "longText", editable: true },
20614
+ { name: "audio", type: "file" },
20615
+ { name: "language", type: "text" },
20616
+ { name: "duration_seconds", type: "number" },
20617
+ { name: "speakers", type: "json" },
20618
+ { name: "raw_segments", type: "json", editable: false }
20619
+ ],
20620
+ sources: [],
20621
+ active: true,
20622
+ configuration: {
20623
+ calculateVectors: "onInsert",
20624
+ defaultRightsMode: "private"
19187
20625
  }
19188
- if (checkPackages) {
19189
- const criticalPackages = ["docling", "transformers"];
19190
- const missingPackages = [];
19191
- for (const pkg of criticalPackages) {
19192
- try {
19193
- await execAsync4(`"${pythonPath}" -c "import ${pkg}"`, {
19194
- cwd: root,
19195
- timeout: 1e4
19196
- // 10 second timeout per import check
19197
- });
19198
- } catch {
19199
- missingPackages.push(pkg);
19200
- }
19201
- }
19202
- if (missingPackages.length > 0) {
19203
- return {
19204
- valid: false,
19205
- message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
19206
-
19207
- This usually happens when:
19208
- 1. The .venv folder was copied but dependencies were not installed
19209
- 2. The package was installed via npm but setup script was not run
20626
+ });
19210
20627
 
19211
- Please run:
19212
- await setupPythonEnvironment({ force: true })
20628
+ // src/templates/contexts/index.ts
20629
+ var builtInContexts = {
20630
+ transcriptions: transcriptionsContext
20631
+ };
19213
20632
 
19214
- Or manually run the setup script:
19215
- bash ` + getSetupScriptPath(root)
19216
- };
20633
+ // src/exulu/transcription/polling-loop.ts
20634
+ init_cjs_shims();
20635
+ var POLL_INTERVAL_MS = 5e3;
20636
+ var MAX_PER_TICK = 50;
20637
+ var timer = null;
20638
+ var stopped = false;
20639
+ var tick = async () => {
20640
+ if (stopped) return;
20641
+ try {
20642
+ await transcriptionService.pollOnce(MAX_PER_TICK);
20643
+ } catch (err) {
20644
+ console.error(`[EXULU-TRANSCRIPTION] polling tick failed: ${err.message}`);
20645
+ } finally {
20646
+ if (!stopped) {
20647
+ timer = setTimeout(tick, POLL_INTERVAL_MS);
19217
20648
  }
19218
20649
  }
19219
- return {
19220
- valid: true,
19221
- message: "Python environment is valid"
20650
+ };
20651
+ var startTranscriptionPollingLoop = () => {
20652
+ if (timer) return;
20653
+ stopped = false;
20654
+ timer = setTimeout(tick, POLL_INTERVAL_MS);
20655
+ const stop = () => {
20656
+ stopped = true;
20657
+ if (timer) {
20658
+ clearTimeout(timer);
20659
+ timer = null;
20660
+ }
19222
20661
  };
19223
- }
20662
+ process.on("SIGINT", stop);
20663
+ process.on("SIGTERM", stop);
20664
+ };
19224
20665
 
19225
20666
  // src/exulu/app/index.ts
19226
20667
  var isDev = process.env.NODE_ENV !== "production";
@@ -19286,8 +20727,14 @@ var ExuluApp = class {
19286
20727
  rerankers
19287
20728
  }) => {
19288
20729
  this._evals = redisServer.host?.length && redisServer.port?.length ? [...getDefaultEvals(), ...evals ?? []] : [];
20730
+ if (contexts && "transcriptions" in contexts) {
20731
+ console.warn(
20732
+ "[EXULU] User-defined 'transcriptions' context overridden by built-in. Rename your context to avoid the collision."
20733
+ );
20734
+ }
19289
20735
  this._contexts = {
19290
- ...contexts
20736
+ ...contexts,
20737
+ ...builtInContexts
19291
20738
  };
19292
20739
  this._rerankers = [...rerankers ?? []];
19293
20740
  this._agents = [...agents ?? []];
@@ -19318,13 +20765,26 @@ var ExuluApp = class {
19318
20765
  if (process.env.TRANSCRIPTION_MODEL && config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
19319
20766
  transcriptionTools.push(transcribeTool);
19320
20767
  }
20768
+ const imageGenerationTools = [];
20769
+ const s3Configured = !!config?.fileUploads && !!config.fileUploads.s3region && !!config.fileUploads.s3key && !!config.fileUploads.s3secret && !!config.fileUploads.s3Bucket;
20770
+ if (isLiteLLMEnabled() && s3Configured) {
20771
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path7.resolve)(getPackageRoot(), "./config.litellm.yaml");
20772
+ const imageModels = parseImageGenerationModels(configPath);
20773
+ if (imageModels.length > 0) {
20774
+ console.log(
20775
+ `[EXULU] Registering image_generation widget tool with ${imageModels.length} model(s): ${imageModels.map((m) => m.model_name).join(", ")}`
20776
+ );
20777
+ imageGenerationTools.push(createImageGenerationWidgetTool(imageModels));
20778
+ }
20779
+ }
19321
20780
  this._tools = [
19322
20781
  ...tools ?? [],
19323
20782
  ...todoTools,
19324
20783
  ...questionTools,
19325
20784
  ...perplexityTools,
19326
20785
  emailTool,
19327
- ...transcriptionTools
20786
+ ...transcriptionTools,
20787
+ ...imageGenerationTools
19328
20788
  // Because agents are stored in the database, we add those as tools
19329
20789
  // at request time, not during ExuluApp initialization. We add them
19330
20790
  // in the grahql tools resolver.
@@ -19423,6 +20883,23 @@ var ExuluApp = class {
19423
20883
  );
19424
20884
  }
19425
20885
  }
20886
+ if (process.env.TRANSCRIPTION_SERVER) {
20887
+ try {
20888
+ const health = await transcriptionClient.health();
20889
+ console.log(
20890
+ `[EXULU] Transcription: enabled (server=${process.env.TRANSCRIPTION_SERVER}, device=${health.device}, GPU=${health.gpu.available ? "enabled" : "disabled"}, diarization=${health.diarization ? "enabled" : "disabled"})`
20891
+ );
20892
+ startTranscriptionPollingLoop();
20893
+ } catch (err) {
20894
+ console.warn(
20895
+ `[EXULU] TRANSCRIPTION_SERVER set but unreachable: ${err.message}. Transcriptions will fail until the server is up.`
20896
+ );
20897
+ }
20898
+ } else {
20899
+ console.log(
20900
+ "[EXULU] Transcription: disabled (TRANSCRIPTION_SERVER not set). Start a whisper server with `npx @exulu/backend exulu-start-whisper`."
20901
+ );
20902
+ }
19426
20903
  return this._expressApp;
19427
20904
  }
19428
20905
  };
@@ -21240,7 +22717,9 @@ var {
21240
22717
  promptLibrarySchema: promptLibrarySchema3,
21241
22718
  contextPresetsSchema: contextPresetsSchema3,
21242
22719
  embedderSettingsSchema: embedderSettingsSchema3,
21243
- promptFavoritesSchema: promptFavoritesSchema3
22720
+ promptFavoritesSchema: promptFavoritesSchema3,
22721
+ transcriptionJobsSchema: transcriptionJobsSchema3,
22722
+ imageGenerationsSchema: imageGenerationsSchema2
21244
22723
  } = coreSchemas.get();
21245
22724
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
21246
22725
  for (const field of fields) {
@@ -21280,6 +22759,8 @@ var up = async function(knex) {
21280
22759
  contextPresetsSchema3(),
21281
22760
  embedderSettingsSchema3(),
21282
22761
  promptFavoritesSchema3(),
22762
+ transcriptionJobsSchema3(),
22763
+ imageGenerationsSchema2(),
21283
22764
  rbacSchema3(),
21284
22765
  agentsSchema3(),
21285
22766
  feedbackSchema3(),
@@ -21490,17 +22971,17 @@ init_cjs_shims();
21490
22971
 
21491
22972
  // src/exulu/litellm/db-init.ts
21492
22973
  init_cjs_shims();
21493
- var import_node_fs6 = require("fs");
21494
- var import_node_path6 = require("path");
22974
+ var import_node_fs7 = require("fs");
22975
+ var import_node_path8 = require("path");
21495
22976
  var import_node_child_process5 = require("child_process");
21496
22977
  var import_pg = require("pg");
21497
22978
 
21498
22979
  // src/exulu/litellm/db-setup-check.ts
21499
22980
  init_cjs_shims();
21500
- var import_node_fs5 = require("fs");
22981
+ var import_node_fs6 = require("fs");
21501
22982
  var readLiteLLMDatabaseUrl = (configPath) => {
21502
- if (!(0, import_node_fs5.existsSync)(configPath)) return void 0;
21503
- const text = (0, import_node_fs5.readFileSync)(configPath, "utf8");
22983
+ if (!(0, import_node_fs6.existsSync)(configPath)) return void 0;
22984
+ const text = (0, import_node_fs6.readFileSync)(configPath, "utf8");
21504
22985
  const match = text.match(
21505
22986
  /^\s*database_url:\s*["']?([^"'\n#]+?)["']?\s*(#.*)?$/m
21506
22987
  );
@@ -21559,9 +23040,9 @@ ${WARNING_BANNER}`);
21559
23040
  console.warn(`${WARNING_BANNER}
21560
23041
  `);
21561
23042
  };
21562
- var log2 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
23043
+ var log3 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
21563
23044
  var initLiteLLMDatabase = async (packageRoot) => {
21564
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path6.resolve)(packageRoot, "./config.litellm.yaml");
23045
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path8.resolve)(packageRoot, "./config.litellm.yaml");
21565
23046
  const safety = checkLiteLLMDatabaseSafety(configPath);
21566
23047
  if (safety.ok && safety.reason === "no-litellm-db-mode") return;
21567
23048
  if (!safety.ok && safety.reason === "unparseable-url") {
@@ -21593,7 +23074,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21593
23074
  return;
21594
23075
  }
21595
23076
  const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
21596
- log2(
23077
+ log3(
21597
23078
  `LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
21598
23079
  );
21599
23080
  const ensureDatabaseExists2 = async () => {
@@ -21621,7 +23102,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21621
23102
  return false;
21622
23103
  }
21623
23104
  url.pathname = "/postgres";
21624
- log2(`Target database "${targetDbName}" does not exist; creating it\u2026`);
23105
+ log3(`Target database "${targetDbName}" does not exist; creating it\u2026`);
21625
23106
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
21626
23107
  warn([
21627
23108
  `Refusing to auto-create database "${targetDbName}" \u2014 name`,
@@ -21634,7 +23115,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21634
23115
  try {
21635
23116
  await admin.connect();
21636
23117
  await admin.query(`CREATE DATABASE "${targetDbName}"`);
21637
- log2(`\u2713 Created database "${targetDbName}".`);
23118
+ log3(`\u2713 Created database "${targetDbName}".`);
21638
23119
  return true;
21639
23120
  } catch (createErr) {
21640
23121
  warn([
@@ -21656,7 +23137,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21656
23137
  }
21657
23138
  };
21658
23139
  if (!await ensureDatabaseExists2()) return;
21659
- log2("Checking that the target database is safe to push into\u2026");
23140
+ log3("Checking that the target database is safe to push into\u2026");
21660
23141
  const client2 = new import_pg.Client({ connectionString: litellmUrl });
21661
23142
  let foreignTables = [];
21662
23143
  try {
@@ -21699,14 +23180,14 @@ var initLiteLLMDatabase = async (packageRoot) => {
21699
23180
  ]);
21700
23181
  return;
21701
23182
  }
21702
- const venvBin = (0, import_node_path6.resolve)(packageRoot, "ee/python/.venv/bin");
21703
- const prismaCli = (0, import_node_path6.resolve)(venvBin, "prisma");
21704
- const litellmProxyDir = (0, import_node_path6.resolve)(
23183
+ const venvBin = (0, import_node_path8.resolve)(packageRoot, "ee/python/.venv/bin");
23184
+ const prismaCli = (0, import_node_path8.resolve)(venvBin, "prisma");
23185
+ const litellmProxyDir = (0, import_node_path8.resolve)(
21705
23186
  packageRoot,
21706
23187
  "ee/python/.venv/lib/python3.12/site-packages/litellm/proxy"
21707
23188
  );
21708
- const schemaPath = (0, import_node_path6.resolve)(litellmProxyDir, "schema.prisma");
21709
- if (!(0, import_node_fs6.existsSync)(prismaCli)) {
23189
+ const schemaPath = (0, import_node_path8.resolve)(litellmProxyDir, "schema.prisma");
23190
+ if (!(0, import_node_fs7.existsSync)(prismaCli)) {
21710
23191
  warn([
21711
23192
  `Prisma CLI not found at ${prismaCli}.`,
21712
23193
  `Run \`npm run python:setup\` to create the venv and install prisma.`,
@@ -21714,14 +23195,14 @@ var initLiteLLMDatabase = async (packageRoot) => {
21714
23195
  ]);
21715
23196
  return;
21716
23197
  }
21717
- if (!(0, import_node_fs6.existsSync)(schemaPath)) {
23198
+ if (!(0, import_node_fs7.existsSync)(schemaPath)) {
21718
23199
  warn([
21719
23200
  `LiteLLM Prisma schema not found at ${schemaPath}.`,
21720
23201
  `Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
21721
23202
  ]);
21722
23203
  return;
21723
23204
  }
21724
- log2("Running `prisma db push` against LiteLLM's schema\u2026");
23205
+ log3("Running `prisma db push` against LiteLLM's schema\u2026");
21725
23206
  const result = (0, import_node_child_process5.spawnSync)(prismaCli, ["db", "push", "--skip-generate"], {
21726
23207
  cwd: litellmProxyDir,
21727
23208
  env: {
@@ -21751,7 +23232,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21751
23232
  ]);
21752
23233
  return;
21753
23234
  }
21754
- log2("\u2713 LiteLLM database ready.");
23235
+ log3("\u2713 LiteLLM database ready.");
21755
23236
  };
21756
23237
 
21757
23238
  // src/postgres/init-litellm-db.ts
@@ -22295,7 +23776,7 @@ init_cjs_shims();
22295
23776
  var fs5 = __toESM(require("fs"), 1);
22296
23777
  var path2 = __toESM(require("path"), 1);
22297
23778
  var import_ai14 = require("ai");
22298
- var import_zod21 = require("zod");
23779
+ var import_zod22 = require("zod");
22299
23780
  var import_p_limit = __toESM(require("p-limit"), 1);
22300
23781
  var import_crypto = require("crypto");
22301
23782
  init_with_retry();
@@ -22650,15 +24131,15 @@ If the page contains a flow-chart, schematic, technical drawing or control board
22650
24131
  const result = await (0, import_ai14.generateText)({
22651
24132
  model,
22652
24133
  output: import_ai14.Output.object({
22653
- schema: import_zod21.z.object({
22654
- needs_correction: import_zod21.z.boolean(),
22655
- corrected_text: import_zod21.z.string().nullable(),
22656
- current_page_table: import_zod21.z.object({
22657
- headers: import_zod21.z.array(import_zod21.z.string()),
22658
- is_continuation: import_zod21.z.boolean()
24134
+ schema: import_zod22.z.object({
24135
+ needs_correction: import_zod22.z.boolean(),
24136
+ corrected_text: import_zod22.z.string().nullable(),
24137
+ current_page_table: import_zod22.z.object({
24138
+ headers: import_zod22.z.array(import_zod22.z.string()),
24139
+ is_continuation: import_zod22.z.boolean()
22659
24140
  }).nullable(),
22660
- confidence: import_zod21.z.enum(["high", "medium", "low"]),
22661
- reasoning: import_zod21.z.string()
24141
+ confidence: import_zod22.z.enum(["high", "medium", "low"]),
24142
+ reasoning: import_zod22.z.string()
22662
24143
  })
22663
24144
  }),
22664
24145
  messages: [
@@ -22738,7 +24219,7 @@ async function validateWithVLM(document2, model, verbose = false, concurrency =
22738
24219
  let correctedCount = 0;
22739
24220
  const validationTasks = document2.map(
22740
24221
  (page) => limit(async () => {
22741
- await new Promise((resolve6) => setImmediate(resolve6));
24222
+ await new Promise((resolve7) => setImmediate(resolve7));
22742
24223
  const imagePath = page.image;
22743
24224
  if (!imagePath) {
22744
24225
  console.warn(`[EXULU] Page ${page.page}: No image found, skipping validation`);
@@ -22929,7 +24410,7 @@ ${setupResult.output || ""}`);
22929
24410
  if (!MISTRAL_API_KEY) {
22930
24411
  throw new Error('[EXULU] MISTRAL_API_KEY is not set, please set it in the environment variable via process.env or via an Exulu variable named "MISTRAL_API_KEY".');
22931
24412
  }
22932
- await new Promise((resolve6) => setTimeout(resolve6, Math.floor(Math.random() * 4e3) + 1e3));
24413
+ await new Promise((resolve7) => setTimeout(resolve7, Math.floor(Math.random() * 4e3) + 1e3));
22933
24414
  const base64Pdf = buffer.toString("base64");
22934
24415
  const client2 = new import_mistralai.Mistral({ apiKey: MISTRAL_API_KEY });
22935
24416
  const ocrResponse = await withRetry(async () => {
@@ -23025,8 +24506,8 @@ ${setupResult.output || ""}`);
23025
24506
  markdownStream.write("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
23026
24507
  }
23027
24508
  }
23028
- await new Promise((resolve6, reject) => {
23029
- markdownStream.end(() => resolve6());
24509
+ await new Promise((resolve7, reject) => {
24510
+ markdownStream.end(() => resolve7());
23030
24511
  markdownStream.on("error", reject);
23031
24512
  });
23032
24513
  console.log(`[EXULU] Validated output saved to: ${paths.json}`);