@exulu/backend 1.68.0 → 1.69.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.js CHANGED
@@ -43,8 +43,10 @@ import {
43
43
  exchangeCodeForTokens,
44
44
  exuluApp,
45
45
  getBudgetSettings,
46
+ getChunkEntitiesTableName,
46
47
  getChunksTableName,
47
48
  getEntitiesForItem,
49
+ getEntitiesTableName,
48
50
  getPresignedUrl,
49
51
  getS3ObjectBytes,
50
52
  getS3ObjectContent,
@@ -66,6 +68,7 @@ import {
66
68
  postgresClient,
67
69
  provisionDefaultUserBudget,
68
70
  reportSystemDependencies,
71
+ resolveEmbedder,
69
72
  resolveEntityModel,
70
73
  resolveLiteLLMConfigPath,
71
74
  resolveModel,
@@ -85,7 +88,7 @@ import {
85
88
  vectorSearch,
86
89
  waitForLiteLLMReady,
87
90
  withRetry
88
- } from "./chunk-VPSLTGZF.js";
91
+ } from "./chunk-PJSDLFXL.js";
89
92
  import {
90
93
  findLiteLLMModel
91
94
  } from "./chunk-YCE44CMU.js";
@@ -104,6 +107,72 @@ var redisServer = {
104
107
  username: process.env.REDIS_USER || ""
105
108
  };
106
109
 
110
+ // ee/queues/redis-startup.ts
111
+ var REDIS_STARTUP_TIMEOUT_MS = 6e4;
112
+ var WATCHDOG_INTERVAL_MS = 1e4;
113
+ var ERROR_LOG_THROTTLE_MS = 3e4;
114
+ var log = (line) => console.log(`[EXULU-REDIS] ${line}`);
115
+ var warn = (line) => console.warn(`[EXULU-REDIS] ${line}`);
116
+ var errorLog = (line) => console.error(`[EXULU-REDIS] ${line}`);
117
+ var redisAddress = () => `${redisServer.host || "(unset)"}:${redisServer.port || "(unset)"}`;
118
+ var describeError = (e) => {
119
+ const any = e;
120
+ const head = any?.message ? String(any.message).split("\n")[0] : void 0;
121
+ if (any?.code) return head && head !== any.code ? `${any.code} (${head})` : `${any.code}`;
122
+ return head ?? String(e);
123
+ };
124
+ function logRedisErrors(source, label) {
125
+ let count = 0;
126
+ let lastLoggedAt = 0;
127
+ source.on("error", (err) => {
128
+ count += 1;
129
+ const now = Date.now();
130
+ if (count === 1 || now - lastLoggedAt >= ERROR_LOG_THROTTLE_MS) {
131
+ errorLog(`${label} connection error (${redisAddress()}): ${describeError(err)}${count > 1 ? ` (x${count})` : ""}`);
132
+ lastLoggedAt = now;
133
+ }
134
+ });
135
+ }
136
+ async function guardRedisStartup(label, run, source) {
137
+ const addr = redisAddress();
138
+ log(`Connecting to Redis (${addr}) for ${label}\u2026`);
139
+ const startedAt = Date.now();
140
+ let lastError;
141
+ const onError = (err) => {
142
+ lastError = err;
143
+ };
144
+ source?.on("error", onError);
145
+ const watchdog = setInterval(() => {
146
+ const secs = Math.round((Date.now() - startedAt) / 1e3);
147
+ warn(
148
+ `\u26A0 Still waiting for Redis at ${addr} after ${secs}s \u2014 ${label} startup is blocked. Is Redis running? (aborting at ${REDIS_STARTUP_TIMEOUT_MS / 1e3}s)`
149
+ );
150
+ }, WATCHDOG_INTERVAL_MS);
151
+ watchdog.unref?.();
152
+ let timer2;
153
+ const timeout = new Promise((_resolve, reject) => {
154
+ timer2 = setTimeout(() => {
155
+ reject(
156
+ new Error(
157
+ `[EXULU-REDIS] Redis unreachable at ${addr} after ${REDIS_STARTUP_TIMEOUT_MS / 1e3}s \u2014 aborting ${label} startup. Last error: ${lastError ? describeError(lastError) : "none surfaced"}. Check REDIS_HOST/REDIS_PORT and that a Redis server is reachable at ${addr}.`
158
+ )
159
+ );
160
+ }, REDIS_STARTUP_TIMEOUT_MS);
161
+ });
162
+ const runPromise = Promise.resolve().then(run);
163
+ runPromise.catch(() => {
164
+ });
165
+ try {
166
+ const result = await Promise.race([runPromise, timeout]);
167
+ log(`Redis ready; ${label} initialized (${addr}, ${((Date.now() - startedAt) / 1e3).toFixed(1)}s).`);
168
+ return result;
169
+ } finally {
170
+ clearInterval(watchdog);
171
+ if (timer2) clearTimeout(timer2);
172
+ source?.off?.("error", onError);
173
+ }
174
+ }
175
+
107
176
  // src/redis/client.ts
108
177
  var client = {};
109
178
  async function redisClient() {
@@ -121,9 +190,11 @@ async function redisClient() {
121
190
  client["exulu"] = createClient({
122
191
  url
123
192
  });
124
- await client["exulu"].connect();
193
+ logRedisErrors(client["exulu"], "client");
194
+ await guardRedisStartup("client", () => client["exulu"].connect().then(() => void 0), client["exulu"]);
125
195
  } catch (error) {
126
196
  console.error(`[EXULU] error connecting to redis:`, error);
197
+ delete client["exulu"];
127
198
  return { client: null };
128
199
  }
129
200
  }
@@ -141,7 +212,8 @@ import "express";
141
212
  // src/validators/requests.ts
142
213
  var requestValidators = {
143
214
  authenticate: async (req) => {
144
- const apikey = req.headers["exulu-api-key"] || null;
215
+ const rawApiKey = req.headers["exulu-api-key"] || req.headers["x-api-key"];
216
+ const apikey = rawApiKey?.replace(/^Bearer\s+/i, "") || null;
145
217
  const { db } = await postgresClient();
146
218
  let authtoken = null;
147
219
  if (typeof apikey !== "string") {
@@ -405,6 +477,14 @@ var ExuluQueues = class {
405
477
  },
406
478
  telemetry: new BullMQOtel("simple-guide")
407
479
  });
480
+ logRedisErrors(newQueue, `queue "${name}"`);
481
+ try {
482
+ await guardRedisStartup(`queue "${name}"`, () => newQueue.waitUntilReady(), newQueue);
483
+ } catch (err) {
484
+ void newQueue.close().catch(() => {
485
+ });
486
+ throw err;
487
+ }
408
488
  await newQueue.setGlobalConcurrency(queueConcurrency);
409
489
  this.queues.push({
410
490
  queue: newQueue,
@@ -2490,6 +2570,8 @@ var createWorkers = async (providers, queues2, config, contexts, evals, tools, t
2490
2570
  },
2491
2571
  maxRetriesPerRequest: null
2492
2572
  });
2573
+ logRedisErrors(redisConnection, "worker");
2574
+ await guardRedisStartup("workers", () => redisConnection.ping().then(() => void 0), redisConnection);
2493
2575
  }
2494
2576
  const workers = queues2.map((queue) => {
2495
2577
  console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
@@ -3660,7 +3742,7 @@ var renderTranscript = (segments, speakers) => {
3660
3742
 
3661
3743
  // src/exulu/transcription/service.ts
3662
3744
  var TABLE = "transcription_jobs";
3663
- var log = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
3745
+ var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
3664
3746
  var parseJsonField = (v) => {
3665
3747
  if (v == null) return null;
3666
3748
  if (typeof v === "string") {
@@ -3731,7 +3813,7 @@ var transcriptionService = {
3731
3813
  error: err.message,
3732
3814
  updatedAt: /* @__PURE__ */ new Date()
3733
3815
  }).returning("*");
3734
- log(`Failed to dispatch job ${row.id}: ${err.message}`);
3816
+ log2(`Failed to dispatch job ${row.id}: ${err.message}`);
3735
3817
  return this._rowFromDb(failed);
3736
3818
  }
3737
3819
  },
@@ -3759,9 +3841,9 @@ var transcriptionService = {
3759
3841
  updatedAt: /* @__PURE__ */ new Date()
3760
3842
  });
3761
3843
  } else if (err instanceof TranscriptionServerUnavailable) {
3762
- log(`Whisper server unreachable while polling ${row.id}; will retry`);
3844
+ log2(`Whisper server unreachable while polling ${row.id}; will retry`);
3763
3845
  } else {
3764
- log(`Error polling job ${row.id}: ${err.message}`);
3846
+ log2(`Error polling job ${row.id}: ${err.message}`);
3765
3847
  }
3766
3848
  }
3767
3849
  }
@@ -3811,7 +3893,7 @@ var transcriptionService = {
3811
3893
  } catch (err) {
3812
3894
  const code = err.code;
3813
3895
  if (code !== "JOB_NOT_FOUND") {
3814
- log(`Best-effort cancel of whisper job failed: ${err.message}`);
3896
+ log2(`Best-effort cancel of whisper job failed: ${err.message}`);
3815
3897
  }
3816
3898
  }
3817
3899
  }
@@ -3897,7 +3979,7 @@ var transcriptionService = {
3897
3979
  []
3898
3980
  );
3899
3981
  } catch (err) {
3900
- log(`RBAC update failed for item ${itemId}: ${err.message}`);
3982
+ log2(`RBAC update failed for item ${itemId}: ${err.message}`);
3901
3983
  }
3902
3984
  }
3903
3985
  const projectId = input.project_id ?? row.project_id ?? null;
@@ -4185,7 +4267,7 @@ var durationFromSegments = (segments) => {
4185
4267
  // src/exulu/recall/service.ts
4186
4268
  var TABLE2 = "transcription_jobs";
4187
4269
  var DEFAULT_BOT_NAME = "Exulu Notetaker";
4188
- var log2 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
4270
+ var log3 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
4189
4271
  var parseJson = (v) => {
4190
4272
  if (v == null) return null;
4191
4273
  if (typeof v === "string") {
@@ -4282,7 +4364,7 @@ var recallService = {
4282
4364
  error: err.message,
4283
4365
  updatedAt: /* @__PURE__ */ new Date()
4284
4366
  }).returning("*");
4285
- log2(`createBot failed for job ${inserted.id}: ${err.message}`);
4367
+ log3(`createBot failed for job ${inserted.id}: ${err.message}`);
4286
4368
  return this._row(failed);
4287
4369
  }
4288
4370
  },
@@ -4296,7 +4378,7 @@ var recallService = {
4296
4378
  const name = event?.event ?? "";
4297
4379
  const job = await this._findJob(ids.botId, ids.recordingId, ids.transcriptId);
4298
4380
  if (!job) {
4299
- log2(`No job for event ${name} (bot=${ids.botId}); ignoring.`);
4381
+ log3(`No job for event ${name} (bot=${ids.botId}); ignoring.`);
4300
4382
  return;
4301
4383
  }
4302
4384
  if (name.startsWith("bot.")) {
@@ -4321,12 +4403,12 @@ var recallService = {
4321
4403
  await this._fail(job.id, ids.subCode || ids.code || "transcript failed");
4322
4404
  return;
4323
4405
  default:
4324
- log2(`Unhandled event ${name} for job ${job.id}`);
4406
+ log3(`Unhandled event ${name} for job ${job.id}`);
4325
4407
  }
4326
4408
  },
4327
4409
  async _onRecordingDone(jobId, recordingId) {
4328
4410
  if (!recordingId) {
4329
- log2(`recording.done for job ${jobId} had no recording id`);
4411
+ log3(`recording.done for job ${jobId} had no recording id`);
4330
4412
  return;
4331
4413
  }
4332
4414
  const { db } = await postgresClient();
@@ -4336,7 +4418,7 @@ var recallService = {
4336
4418
  updatedAt: /* @__PURE__ */ new Date()
4337
4419
  });
4338
4420
  if (!claimed) {
4339
- log2(`recording.done for job ${jobId} already handled; skipping.`);
4421
+ log3(`recording.done for job ${jobId} already handled; skipping.`);
4340
4422
  return;
4341
4423
  }
4342
4424
  try {
@@ -4356,7 +4438,7 @@ var recallService = {
4356
4438
  if (!dbRow) return;
4357
4439
  const job = this._row(dbRow);
4358
4440
  if ((job.status === "awaiting_review" || job.status === "saved") && job.raw_segments && job.raw_segments.length > 0) {
4359
- log2(`transcript.done for job ${jobId} already processed; skipping.`);
4441
+ log3(`transcript.done for job ${jobId} already processed; skipping.`);
4360
4442
  return;
4361
4443
  }
4362
4444
  try {
@@ -4375,7 +4457,7 @@ var recallService = {
4375
4457
  const recDuration = recordingDurationSeconds(rec);
4376
4458
  if (recDuration != null) duration = recDuration;
4377
4459
  } catch (err) {
4378
- log2(`could not fetch recording duration for job ${jobId}: ${err.message}`);
4460
+ log3(`could not fetch recording duration for job ${jobId}: ${err.message}`);
4379
4461
  }
4380
4462
  }
4381
4463
  await this._update(jobId, {
@@ -4415,11 +4497,11 @@ var recallService = {
4415
4497
  const prompts = job.post_processing_prompts ?? [];
4416
4498
  if (prompts.length === 0) return [];
4417
4499
  if (job.post_processing_outputs && job.post_processing_outputs.length > 0) {
4418
- log2(`post-processing for job ${jobId} already ran; skipping.`);
4500
+ log3(`post-processing for job ${jobId} already ran; skipping.`);
4419
4501
  return job.post_processing_outputs;
4420
4502
  }
4421
4503
  if (!job.raw_segments || job.raw_segments.length === 0) {
4422
- log2(`post-processing for job ${jobId} skipped: no transcript.`);
4504
+ log3(`post-processing for job ${jobId} skipped: no transcript.`);
4423
4505
  return [];
4424
4506
  }
4425
4507
  const outputs = [];
@@ -4492,7 +4574,7 @@ ${transcriptText}`,
4492
4574
  ran_at: ranAt
4493
4575
  };
4494
4576
  } catch (err) {
4495
- log2(`post-processing prompt ${promptId} failed for job ${job.id}: ${err.message}`);
4577
+ log3(`post-processing prompt ${promptId} failed for job ${job.id}: ${err.message}`);
4496
4578
  return {
4497
4579
  prompt_id: promptId,
4498
4580
  agent_id: agentId,
@@ -8644,6 +8726,52 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
8644
8726
  return passed ? { ok: true } : { ok: false, reason: "signature mismatch" };
8645
8727
  };
8646
8728
 
8729
+ // src/exulu/shared-artifacts.ts
8730
+ import bcrypt3 from "bcryptjs";
8731
+ var normalizeS3Key = (key, bucket) => {
8732
+ const segments = key.split("/").filter((s, i) => !(i === 0 && s === "")).map((s) => decodeURIComponent(s));
8733
+ if (segments[0] === bucket) segments.shift();
8734
+ return segments.join("/");
8735
+ };
8736
+ var isHtmlKey = (key) => /\.html?$/i.test(key);
8737
+ var deriveFilename = (key) => {
8738
+ const base = key.split("/").pop() ?? key;
8739
+ return base.split("_EXULU_").pop() ?? base;
8740
+ };
8741
+ var slugifyShareName = (input) => deriveFilename(input).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
8742
+ var isExpired = (expiresAt, now) => {
8743
+ if (!expiresAt) return false;
8744
+ return new Date(expiresAt).getTime() <= now.getTime();
8745
+ };
8746
+ var validateCreateInput = (input, now) => {
8747
+ if (!input.s3key) return { ok: false, message: "s3key is required." };
8748
+ if (!input.name) return { ok: false, message: "name is required." };
8749
+ const mode = input.auth_mode;
8750
+ if (mode !== "public" && mode !== "password" && mode !== "regular") {
8751
+ return { ok: false, message: "auth_mode must be public, password, or regular." };
8752
+ }
8753
+ if (mode === "password" && !input.password) {
8754
+ return { ok: false, message: "A password is required for password mode." };
8755
+ }
8756
+ if (input.expires_at && Number.isNaN(new Date(input.expires_at).getTime())) {
8757
+ return { ok: false, message: "expires_at is not a valid date." };
8758
+ }
8759
+ if (input.expires_at && isExpired(input.expires_at, now)) {
8760
+ return { ok: false, message: "expires_at must be in the future." };
8761
+ }
8762
+ return { ok: true };
8763
+ };
8764
+ var hashSharePassword = (password) => bcrypt3.hash(password, 10);
8765
+ var verifySharePassword = (password, hash) => bcrypt3.compare(password, hash);
8766
+ var contentHeadersFor = (key, contentType, filename) => {
8767
+ if (isHtmlKey(key)) return { contentType: "text/html; charset=utf-8" };
8768
+ return {
8769
+ contentType: contentType || "application/octet-stream",
8770
+ disposition: `attachment; filename="${filename.replace(/"/g, "")}"`
8771
+ };
8772
+ };
8773
+ var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
8774
+
8647
8775
  // src/exulu/routes.ts
8648
8776
  var REQUEST_SIZE_LIMIT = "50mb";
8649
8777
  var getExuluVersionNumber = async () => {
@@ -11678,6 +11806,126 @@ ${style.markdown}` : params.prompt;
11678
11806
  }
11679
11807
  res.status(204).send();
11680
11808
  });
11809
+ app.post("/shared-artifacts", async (req, res) => {
11810
+ const { db } = await postgresClient();
11811
+ const auth = await requestValidators.authenticate(req);
11812
+ if (!auth.user?.id) {
11813
+ res.status(401).json({ detail: "Authentication required." });
11814
+ return;
11815
+ }
11816
+ const now = /* @__PURE__ */ new Date();
11817
+ const valid = validateCreateInput(req.body, now);
11818
+ if (!valid.ok) {
11819
+ res.status(400).json({ detail: valid.message });
11820
+ return;
11821
+ }
11822
+ const bucket = config.fileUploads?.s3Bucket ?? "";
11823
+ const s3key = normalizeS3Key(req.body.s3key, bucket);
11824
+ const name = slugifyShareName(req.body.name);
11825
+ if (!name) {
11826
+ res.status(400).json({ detail: "name must contain url-safe characters." });
11827
+ return;
11828
+ }
11829
+ const existing = await getSharedArtifactByName(db, name);
11830
+ if (existing) {
11831
+ res.status(409).json({ detail: "That share name is already taken." });
11832
+ return;
11833
+ }
11834
+ const auth_mode = req.body.auth_mode;
11835
+ const password_hash = auth_mode === "password" ? await hashSharePassword(req.body.password) : null;
11836
+ const rights_mode = auth_mode === "regular" ? req.body.rights_mode ?? "private" : "public";
11837
+ const [row] = await db("shared_artifacts").insert({
11838
+ name,
11839
+ s3key,
11840
+ auth_mode,
11841
+ password_hash,
11842
+ expires_at: req.body.expires_at ?? null,
11843
+ content_type: req.body.content_type ?? null,
11844
+ rights_mode,
11845
+ created_by: auth.user.id
11846
+ }).returning("*");
11847
+ if (auth_mode === "regular" && req.body.rbac) {
11848
+ await handleRBACUpdate(db, "shared_artifact", row.id, req.body.rbac, []);
11849
+ }
11850
+ res.status(201).json({ name: row.name });
11851
+ });
11852
+ app.get("/shared-artifacts/:name/meta", async (req, res) => {
11853
+ const { db } = await postgresClient();
11854
+ const row = await getSharedArtifactByName(db, req.params.name ?? "");
11855
+ if (!row) {
11856
+ res.status(404).json({ detail: "Not found." });
11857
+ return;
11858
+ }
11859
+ if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
11860
+ res.status(410).json({ detail: "This link has expired." });
11861
+ return;
11862
+ }
11863
+ res.json({
11864
+ auth_mode: row.auth_mode,
11865
+ expires_at: row.expires_at,
11866
+ filename: deriveFilename(row.s3key),
11867
+ content_type: row.content_type,
11868
+ is_html: /\.html?$/i.test(row.s3key)
11869
+ });
11870
+ });
11871
+ app.get("/shared-artifacts/:name/content", async (req, res) => {
11872
+ const { db } = await postgresClient();
11873
+ const row = await getSharedArtifactByName(db, req.params.name ?? "");
11874
+ if (!row) {
11875
+ res.status(404).json({ detail: "Not found." });
11876
+ return;
11877
+ }
11878
+ if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
11879
+ res.status(410).json({ detail: "This link has expired." });
11880
+ return;
11881
+ }
11882
+ if (row.auth_mode === "password") {
11883
+ const pw = req.headers["x-share-password"] || "";
11884
+ if (!row.password_hash || !await verifySharePassword(pw, row.password_hash)) {
11885
+ res.status(401).json({ detail: "Incorrect password." });
11886
+ return;
11887
+ }
11888
+ } else if (row.auth_mode === "regular") {
11889
+ const viewer = await requestValidators.authenticate(req);
11890
+ if (!viewer.user?.id) {
11891
+ res.status(401).json({ detail: "Authentication required." });
11892
+ return;
11893
+ }
11894
+ const rbac = await RBACResolver(
11895
+ db,
11896
+ "shared_artifact",
11897
+ row.id,
11898
+ row.rights_mode || "private"
11899
+ );
11900
+ const ok = await checkRecordAccess({ ...row, RBAC: rbac }, "read", viewer.user);
11901
+ if (!ok) {
11902
+ res.status(403).json({ detail: "You don't have access to this artifact." });
11903
+ return;
11904
+ }
11905
+ }
11906
+ let bytes;
11907
+ try {
11908
+ bytes = await getS3ObjectBytes(row.s3key, config);
11909
+ } catch (e) {
11910
+ if (e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.$metadata?.httpStatusCode === 404) {
11911
+ res.status(404).json({ detail: "Artifact file not found." });
11912
+ return;
11913
+ }
11914
+ console.error("[EXULU] shared-artifact content read failed", e);
11915
+ res.status(500).json({ detail: "Failed to read artifact." });
11916
+ return;
11917
+ }
11918
+ const headers = contentHeadersFor(
11919
+ row.s3key,
11920
+ row.content_type,
11921
+ deriveFilename(row.s3key)
11922
+ );
11923
+ res.setHeader("Content-Type", headers.contentType);
11924
+ res.setHeader("X-Content-Type-Options", "nosniff");
11925
+ res.setHeader("Cache-Control", "private, no-store");
11926
+ if (headers.disposition) res.setHeader("Content-Disposition", headers.disposition);
11927
+ res.send(bytes);
11928
+ });
11681
11929
  app.use(express2.static("public"));
11682
11930
  await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config);
11683
11931
  return app;
@@ -15373,6 +15621,73 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
15373
15621
  }
15374
15622
  };
15375
15623
 
15624
+ // src/exulu/read-api.ts
15625
+ var authorizedRead = async (context, user, role, opts = {}) => {
15626
+ if (!opts.itemIds?.length && !opts.externalIds?.length) {
15627
+ throw new Error("authorizedRead requires itemIds or externalIds to constrain the read.");
15628
+ }
15629
+ const { db } = await postgresClient();
15630
+ const table = convertContextToTableDefinition(context);
15631
+ const itemsTable = getTableName(context.id);
15632
+ const chunksTable = getChunksTableName(context.id);
15633
+ const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
15634
+ let q = db(chunksTable + " as chunks").select([
15635
+ "chunks.id as chunk_id",
15636
+ "chunks.source as chunk_source",
15637
+ "chunks.content as chunk_content",
15638
+ "chunks.chunk_index",
15639
+ "chunks.metadata as chunk_metadata",
15640
+ db.raw('chunks."createdAt" as chunk_created_at'),
15641
+ db.raw('chunks."updatedAt" as chunk_updated_at'),
15642
+ "items.id as item_id",
15643
+ "items.name as item_name",
15644
+ "items.external_id as item_external_id",
15645
+ db.raw('items."createdAt" as item_created_at'),
15646
+ db.raw('items."updatedAt" as item_updated_at')
15647
+ ]);
15648
+ q = q.leftJoin(itemsTable + " as items", "chunks.source", "items.id");
15649
+ if (opts.itemIds?.length) q = q.whereIn("items.id", opts.itemIds);
15650
+ if (opts.externalIds?.length) q = q.whereIn("items.external_id", opts.externalIds);
15651
+ if (opts.chunkIndexRange) {
15652
+ const { from, to } = opts.chunkIndexRange;
15653
+ if (typeof from === "number") q = q.where("chunks.chunk_index", ">=", from);
15654
+ if (typeof to === "number") q = q.where("chunks.chunk_index", "<=", to);
15655
+ }
15656
+ q = applyAccessControl(table, q, acUser, "items");
15657
+ q = q.orderBy("chunks.source").orderBy("chunks.chunk_index");
15658
+ return await q;
15659
+ };
15660
+ var entitiesAvailable = async (context) => {
15661
+ if (!context.entities) return false;
15662
+ const { db } = await postgresClient();
15663
+ const table = getChunkEntitiesTableName(context.id);
15664
+ const res = await db.raw(
15665
+ "SELECT to_regclass(?) IS NOT NULL AS exists",
15666
+ [table]
15667
+ );
15668
+ return res.rows?.[0]?.exists === true;
15669
+ };
15670
+ var embedQuery = async (context, text, opts = {}) => {
15671
+ const resolved = await resolveEmbedder({
15672
+ model: context.embedder.model,
15673
+ contextId: context.id,
15674
+ contextName: context.name,
15675
+ user: opts.user,
15676
+ roleId: opts.role
15677
+ });
15678
+ const [vector] = await resolved.embed([text], { inputType: opts.inputType ?? "query" });
15679
+ return vector ?? [];
15680
+ };
15681
+ var ExuluReadApi = {
15682
+ getTableName,
15683
+ getChunksTableName,
15684
+ getEntitiesTableName,
15685
+ getChunkEntitiesTableName,
15686
+ entitiesAvailable,
15687
+ authorizedRead,
15688
+ embedQuery
15689
+ };
15690
+
15376
15691
  // src/postgres/init-exulu-db.ts
15377
15692
  var {
15378
15693
  agentsSchema: agentsSchema2,
@@ -15400,7 +15715,8 @@ var {
15400
15715
  promptFavoritesSchema: promptFavoritesSchema2,
15401
15716
  transcriptionJobsSchema: transcriptionJobsSchema2,
15402
15717
  imageGenerationsSchema,
15403
- oauthTokensSchema
15718
+ oauthTokensSchema,
15719
+ sharedArtifactsSchema
15404
15720
  } = coreSchemas.get();
15405
15721
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
15406
15722
  for (const field of fields) {
@@ -15444,6 +15760,7 @@ var up = async function(knex) {
15444
15760
  transcriptionJobsSchema2(),
15445
15761
  imageGenerationsSchema(),
15446
15762
  oauthTokensSchema(),
15763
+ sharedArtifactsSchema(),
15447
15764
  rbacSchema2(),
15448
15765
  agentsSchema2(),
15449
15766
  feedbackSchema2(),
@@ -15729,7 +16046,7 @@ var checkLiteLLMDatabaseSafety = (configPath) => {
15729
16046
 
15730
16047
  // src/exulu/litellm/db-init.ts
15731
16048
  var WARNING_BANNER = "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550";
15732
- var warn = (lines) => {
16049
+ var warn2 = (lines) => {
15733
16050
  console.warn(`
15734
16051
  ${WARNING_BANNER}`);
15735
16052
  console.warn("\u26A0 [EXULU-LITELLM] CONFIGURATION WARNING");
@@ -15737,13 +16054,13 @@ ${WARNING_BANNER}`);
15737
16054
  console.warn(`${WARNING_BANNER}
15738
16055
  `);
15739
16056
  };
15740
- var log3 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
16057
+ var log4 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
15741
16058
  var initLiteLLMDatabase = async (packageRoot) => {
15742
16059
  const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve2(process.cwd(), "./config.litellm.yaml");
15743
16060
  const safety = checkLiteLLMDatabaseSafety(configPath);
15744
16061
  if (safety.ok && safety.reason === "no-litellm-db-mode") return;
15745
16062
  if (!safety.ok && safety.reason === "unparseable-url") {
15746
- warn([
16063
+ warn2([
15747
16064
  `LiteLLM's database_url is not a valid postgres URL:`,
15748
16065
  ` ${safety.rawUrl}`,
15749
16066
  `Expected postgres://user:pass@host:port/database. Skipping setup.`
@@ -15752,7 +16069,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15752
16069
  }
15753
16070
  if (!safety.ok && safety.reason === "shared-with-exulu") {
15754
16071
  const { exuluTarget } = safety;
15755
- warn([
16072
+ warn2([
15756
16073
  `LiteLLM's database_url points to the SAME database Exulu uses:`,
15757
16074
  ` ${exuluTarget.host}:${exuluTarget.port}/${exuluTarget.database}`,
15758
16075
  ``,
@@ -15771,7 +16088,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15771
16088
  return;
15772
16089
  }
15773
16090
  const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
15774
- log3(
16091
+ log4(
15775
16092
  `LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
15776
16093
  );
15777
16094
  const ensureDatabaseExists = async () => {
@@ -15783,7 +16100,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15783
16100
  } catch (err) {
15784
16101
  const code = err?.code;
15785
16102
  if (code !== "3D000") {
15786
- warn([
16103
+ warn2([
15787
16104
  `Could not connect to LiteLLM's target database:`,
15788
16105
  ` ${err instanceof Error ? err.message : String(err)}`,
15789
16106
  ``,
@@ -15795,13 +16112,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
15795
16112
  const url = new URL(litellmUrl);
15796
16113
  const targetDbName = url.pathname.replace(/^\//, "");
15797
16114
  if (!targetDbName) {
15798
- warn([`LiteLLM database_url has no database name; cannot auto-create.`]);
16115
+ warn2([`LiteLLM database_url has no database name; cannot auto-create.`]);
15799
16116
  return false;
15800
16117
  }
15801
16118
  url.pathname = "/postgres";
15802
- log3(`Target database "${targetDbName}" does not exist; creating it\u2026`);
16119
+ log4(`Target database "${targetDbName}" does not exist; creating it\u2026`);
15803
16120
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
15804
- warn([
16121
+ warn2([
15805
16122
  `Refusing to auto-create database "${targetDbName}" \u2014 name`,
15806
16123
  `contains characters that would require quoting. Create it`,
15807
16124
  `manually: createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`
@@ -15812,10 +16129,10 @@ var initLiteLLMDatabase = async (packageRoot) => {
15812
16129
  try {
15813
16130
  await admin.connect();
15814
16131
  await admin.query(`CREATE DATABASE "${targetDbName}"`);
15815
- log3(`\u2713 Created database "${targetDbName}".`);
16132
+ log4(`\u2713 Created database "${targetDbName}".`);
15816
16133
  return true;
15817
16134
  } catch (createErr) {
15818
- warn([
16135
+ warn2([
15819
16136
  `Failed to auto-create database "${targetDbName}":`,
15820
16137
  ` ${createErr instanceof Error ? createErr.message : String(createErr)}`,
15821
16138
  ``,
@@ -15834,7 +16151,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15834
16151
  }
15835
16152
  };
15836
16153
  if (!await ensureDatabaseExists()) return;
15837
- log3("Checking that the target database is safe to push into\u2026");
16154
+ log4("Checking that the target database is safe to push into\u2026");
15838
16155
  const client2 = new Client({ connectionString: litellmUrl });
15839
16156
  let foreignTables = [];
15840
16157
  try {
@@ -15850,7 +16167,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15850
16167
  );
15851
16168
  foreignTables = res.rows.map((r) => r.table_name);
15852
16169
  } catch (err) {
15853
- warn([
16170
+ warn2([
15854
16171
  `Could not query LiteLLM's target database to verify it is safe`,
15855
16172
  `to push into:`,
15856
16173
  ` ${err instanceof Error ? err.message : String(err)}`,
@@ -15866,7 +16183,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15866
16183
  }
15867
16184
  }
15868
16185
  if (foreignTables.length > 0) {
15869
- warn([
16186
+ warn2([
15870
16187
  `LiteLLM's target database contains ${foreignTables.length} table(s) that are NOT`,
15871
16188
  `part of LiteLLM's schema:`,
15872
16189
  ...foreignTables.slice(0, 10).map((t) => ` - ${t}`),
@@ -15882,7 +16199,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15882
16199
  const venvLibDir = resolve2(packageRoot, "ee/python/.venv/lib");
15883
16200
  const pythonVersionDir = existsSync5(venvLibDir) ? readdirSync(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
15884
16201
  if (!pythonVersionDir) {
15885
- warn([
16202
+ warn2([
15886
16203
  `Could not find a python3.* directory under ${venvLibDir}.`,
15887
16204
  `Run \`npm run python:setup\` to create the venv.`,
15888
16205
  `Skipping LiteLLM database setup.`
@@ -15896,7 +16213,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15896
16213
  );
15897
16214
  const schemaPath = resolve2(litellmProxyDir, "schema.prisma");
15898
16215
  if (!existsSync5(prismaCli)) {
15899
- warn([
16216
+ warn2([
15900
16217
  `Prisma CLI not found at ${prismaCli}.`,
15901
16218
  `Run \`npm run python:setup\` to create the venv and install prisma.`,
15902
16219
  `Skipping LiteLLM database setup.`
@@ -15904,13 +16221,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
15904
16221
  return;
15905
16222
  }
15906
16223
  if (!existsSync5(schemaPath)) {
15907
- warn([
16224
+ warn2([
15908
16225
  `LiteLLM Prisma schema not found at ${schemaPath}.`,
15909
16226
  `Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
15910
16227
  ]);
15911
16228
  return;
15912
16229
  }
15913
- log3("Running `prisma db push` against LiteLLM's schema\u2026");
16230
+ log4("Running `prisma db push` against LiteLLM's schema\u2026");
15914
16231
  const result = spawnSync(prismaCli, ["db", "push", "--skip-generate"], {
15915
16232
  cwd: litellmProxyDir,
15916
16233
  env: {
@@ -15924,14 +16241,14 @@ var initLiteLLMDatabase = async (packageRoot) => {
15924
16241
  encoding: "utf8"
15925
16242
  });
15926
16243
  if (result.error) {
15927
- warn([
16244
+ warn2([
15928
16245
  `Failed to launch prisma: ${result.error.message}`,
15929
16246
  `Skipping LiteLLM database setup.`
15930
16247
  ]);
15931
16248
  return;
15932
16249
  }
15933
16250
  if (result.status !== 0) {
15934
- warn([
16251
+ warn2([
15935
16252
  `prisma db push exited with status ${result.status}.`,
15936
16253
  `stdout:`,
15937
16254
  ...(result.stdout || "(empty)").split("\n").map((l) => ` ${l}`),
@@ -15940,7 +16257,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
15940
16257
  ]);
15941
16258
  return;
15942
16259
  }
15943
- log3("\u2713 LiteLLM database ready.");
16260
+ log4("\u2713 LiteLLM database ready.");
15944
16261
  };
15945
16262
 
15946
16263
  // src/postgres/init-litellm-db.ts
@@ -17207,14 +17524,37 @@ ${setupResult.output || ""}`);
17207
17524
  model: config.processor.model ?? "mistral-ocr",
17208
17525
  ...config.attribution
17209
17526
  });
17210
- await new Promise((resolve4) => setTimeout(resolve4, Math.floor(Math.random() * 4e3) + 1e3));
17211
- const base64Pdf = buffer.toString("base64");
17212
- const ocrResponse = await withRetry(async () => {
17213
- return await resolved.ocr({
17214
- type: "document_url",
17215
- document_url: "data:application/pdf;base64," + base64Pdf
17216
- }, { includeImageBase64: false });
17217
- }, 10);
17527
+ const maxPagesPerChunk = config.processor.maxPagesPerChunk ?? 25;
17528
+ const chunksDir = path.join(path.dirname(paths.json), "ocr_chunks");
17529
+ const splitResult = await executePythonScript({
17530
+ scriptPath: "ee/python/documents/processing/split_pdf.py",
17531
+ args: [paths.source, chunksDir, "--chunk-size", String(maxPagesPerChunk)],
17532
+ timeout: 5 * 60 * 1e3
17533
+ });
17534
+ const pdfChunks = JSON.parse(splitResult.stdout);
17535
+ console.log(`[EXULU] PDF split into ${pdfChunks.length} chunk(s) for OCR (max ${maxPagesPerChunk} pages each)`);
17536
+ const chunkLimit = pLimit(3);
17537
+ const chunkResults = await Promise.all(
17538
+ pdfChunks.map(
17539
+ (chunk, i) => chunkLimit(async () => {
17540
+ await new Promise((resolve4) => setImmediate(resolve4));
17541
+ await new Promise((resolve4) => setTimeout(resolve4, Math.floor(Math.random() * 1e3) + 200));
17542
+ console.log(`[EXULU] OCR chunk ${i + 1}/${pdfChunks.length}: pages ${chunk.start_page}\u2013${chunk.end_page - 1}`);
17543
+ const chunkBuffer = await fs3.promises.readFile(chunk.path);
17544
+ const chunkBase64 = chunkBuffer.toString("base64");
17545
+ const chunkResponse = await withRetry(async () => {
17546
+ return await resolved.ocr({
17547
+ type: "document_url",
17548
+ document_url: "data:application/pdf;base64," + chunkBase64
17549
+ }, { includeImageBase64: false });
17550
+ }, 10);
17551
+ return { pages: chunkResponse.pages, offset: chunk.start_page };
17552
+ })
17553
+ )
17554
+ );
17555
+ const mergedPages = chunkResults.sort((a, b) => a.offset - b.offset).flatMap(
17556
+ ({ pages, offset }) => pages.map((p) => ({ ...p, index: p.index + offset }))
17557
+ );
17218
17558
  const parser = new LiteParse();
17219
17559
  const screenshots = await parser.screenshot(paths.source, void 0);
17220
17560
  await fs3.promises.mkdir(paths.images, { recursive: true });
@@ -17228,7 +17568,7 @@ ${setupResult.output || ""}`);
17228
17568
  );
17229
17569
  screenshot.imagePath = path.join(paths.images, `${screenshot.pageNum}.png`);
17230
17570
  }
17231
- json = ocrResponse.pages.map((page) => ({
17571
+ json = mergedPages.map((page) => ({
17232
17572
  page: page.index + 1,
17233
17573
  content: page.markdown,
17234
17574
  image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
@@ -17557,9 +17897,11 @@ export {
17557
17897
  ExuluProvider,
17558
17898
  ExuluPython,
17559
17899
  queues as ExuluQueues,
17900
+ ExuluReadApi,
17560
17901
  ExuluReranker,
17561
17902
  ExuluTool,
17562
17903
  trajectoryRegistry as ExuluTrajectoryRegistry,
17563
17904
  ExuluVariables,
17564
- defaultChunker
17905
+ defaultChunker,
17906
+ postgresClient
17565
17907
  };