@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.cjs CHANGED
@@ -160,9 +160,9 @@ async function postgresClient() {
160
160
  // Log pool events to help debug connection issues
161
161
  afterCreate: (conn, done) => {
162
162
  console.log("[EXULU] New database connection created");
163
- conn.query("SET statement_timeout = 1800000", (err) => {
163
+ conn.query("SET statement_timeout = 1800000; SET hnsw.ef_search = 20", (err) => {
164
164
  if (err) {
165
- console.error("[EXULU] Error setting statement_timeout:", err);
165
+ console.error("[EXULU] Error setting connection parameters:", err);
166
166
  }
167
167
  done(err, conn);
168
168
  });
@@ -1351,6 +1351,18 @@ var init_sanitize_name = __esm({
1351
1351
  }
1352
1352
  });
1353
1353
 
1354
+ // src/exulu/table-names.ts
1355
+ var getTableName, getChunksTableName;
1356
+ var init_table_names = __esm({
1357
+ "src/exulu/table-names.ts"() {
1358
+ "use strict";
1359
+ init_cjs_shims();
1360
+ init_sanitize_name();
1361
+ getTableName = (id) => sanitizeName(id) + "_items";
1362
+ getChunksTableName = (id) => sanitizeName(id) + "_chunks";
1363
+ }
1364
+ });
1365
+
1354
1366
  // ee/tokenizer.ts
1355
1367
  var import_lite, import_load, import_registry, import_model_to_encoding, ExuluTokenizer;
1356
1368
  var init_tokenizer = __esm({
@@ -2052,7 +2064,7 @@ var init_chunker = __esm({
2052
2064
  });
2053
2065
 
2054
2066
  // src/exulu/litellm/supervisor.ts
2055
- var import_node_child_process, import_node_fs3, import_node_path2, LITELLM_UI_PATH, MAX_CRASHES, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, READY_TIMEOUT_MS, WAIT_TIMEOUT_MS, READY_POLL_INTERVAL_MS, SHUTDOWN_GRACE_MS, internal, isLiteLLMEnabled, resolveConfig, log, pollHealth, spawnLiteLLM, supervise, _packageRoot, _clientMode, setLiteLLMPackageRoot, enableLiteLLMClientMode, startLiteLLMSupervisor, waitForLiteLLMReady, stopLiteLLM, shutdownHandlersRegistered, registerShutdownHandlers, getSupervisorState;
2067
+ var import_node_child_process, import_node_fs3, import_node_path2, LITELLM_UI_PATH, MAX_CRASHES, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, READY_TIMEOUT_MS, WAIT_TIMEOUT_MS, READY_POLL_INTERVAL_MS, SHUTDOWN_GRACE_MS, internal, isLiteLLMEnabled, resolveConfig, log2, pollHealth, spawnLiteLLM, supervise, _packageRoot, _clientMode, setLiteLLMPackageRoot, enableLiteLLMClientMode, startLiteLLMSupervisor, waitForLiteLLMReady, stopLiteLLM, shutdownHandlersRegistered, registerShutdownHandlers, getSupervisorState;
2056
2068
  var init_supervisor = __esm({
2057
2069
  "src/exulu/litellm/supervisor.ts"() {
2058
2070
  "use strict";
@@ -2087,7 +2099,7 @@ var init_supervisor = __esm({
2087
2099
  const litellmBin = (0, import_node_path2.resolve)(venvBin, "litellm");
2088
2100
  return { host, port, masterKey, configPath, venvBin, venvPython, litellmBin };
2089
2101
  };
2090
- log = (line) => console.log(`[EXULU-LITELLM] ${line}`);
2102
+ log2 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
2091
2103
  pollHealth = async (host, port) => {
2092
2104
  const url = `http://${host}:${port}/health/liveliness`;
2093
2105
  const deadline = Date.now() + READY_TIMEOUT_MS;
@@ -2104,7 +2116,7 @@ var init_supervisor = __esm({
2104
2116
  );
2105
2117
  };
2106
2118
  spawnLiteLLM = (cfg) => {
2107
- log(
2119
+ log2(
2108
2120
  `Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
2109
2121
  );
2110
2122
  const { DEBUG: _debug, ...rest } = process.env;
@@ -2131,10 +2143,10 @@ var init_supervisor = __esm({
2131
2143
  }
2132
2144
  );
2133
2145
  child.stdout?.on("data", (chunk) => {
2134
- chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(l));
2146
+ chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log2(l));
2135
2147
  });
2136
2148
  child.stderr?.on("data", (chunk) => {
2137
- chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(`stderr: ${l}`));
2149
+ chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log2(`stderr: ${l}`));
2138
2150
  });
2139
2151
  return child;
2140
2152
  };
@@ -2151,7 +2163,7 @@ var init_supervisor = __esm({
2151
2163
  exitPromise.then((code2) => ({ exited: code2 }))
2152
2164
  ]);
2153
2165
  } catch (err) {
2154
- log(`Readiness probe failed: ${err.message}`);
2166
+ log2(`Readiness probe failed: ${err.message}`);
2155
2167
  try {
2156
2168
  internal.child?.kill("SIGTERM");
2157
2169
  } catch {
@@ -2161,22 +2173,22 @@ var init_supervisor = __esm({
2161
2173
  internal.state = "ready";
2162
2174
  internal.crashCount = 0;
2163
2175
  internal.backoffMs = INITIAL_BACKOFF_MS;
2164
- log("LiteLLM is ready.");
2176
+ log2("LiteLLM is ready.");
2165
2177
  }
2166
2178
  const code = await exitPromise;
2167
2179
  internal.state = "respawning";
2168
2180
  internal.child = void 0;
2169
2181
  if (internal.shutdownRequested) {
2170
- log("Child exited during shutdown; supervisor stopping.");
2182
+ log2("Child exited during shutdown; supervisor stopping.");
2171
2183
  internal.state = "stopped";
2172
2184
  return;
2173
2185
  }
2174
2186
  internal.crashCount += 1;
2175
- log(
2187
+ log2(
2176
2188
  `LiteLLM exited (code=${code}). Crash ${internal.crashCount}/${MAX_CRASHES}. Respawning in ${internal.backoffMs}ms.`
2177
2189
  );
2178
2190
  if (internal.crashCount >= MAX_CRASHES) {
2179
- log(
2191
+ log2(
2180
2192
  "LiteLLM keeps crashing \u2014 fix the config and restart Exulu. Giving up on respawn."
2181
2193
  );
2182
2194
  internal.state = "given_up";
@@ -2212,14 +2224,14 @@ var init_supervisor = __esm({
2212
2224
  }
2213
2225
  const cfg = resolveConfig(packageRoot);
2214
2226
  if (!(0, import_node_fs3.existsSync)(cfg.configPath)) {
2215
- log(
2227
+ log2(
2216
2228
  `LiteLLM config not found at ${cfg.configPath}. Copy ee/python/.litellm/config.yaml.example to that path, edit it, and restart Exulu. LiteLLM will NOT be started until then.`
2217
2229
  );
2218
2230
  internal.state = "given_up";
2219
2231
  return;
2220
2232
  }
2221
2233
  if (!(0, import_node_fs3.existsSync)(cfg.litellmBin)) {
2222
- log(
2234
+ log2(
2223
2235
  `LiteLLM binary not found at ${cfg.litellmBin}. The Python venv may not be set up. Run setupPythonEnvironment() from @exulu/backend, then restart.`
2224
2236
  );
2225
2237
  internal.state = "given_up";
@@ -2735,13 +2747,31 @@ function durationToDays(duration) {
2735
2747
  return n;
2736
2748
  }
2737
2749
  }
2750
+ function subtractDuration(date, duration) {
2751
+ const m = /^\s*(\d+(?:\.\d+)?)\s*(mo|[a-z]+)?\s*$/i.exec(String(duration ?? ""));
2752
+ const unit = m ? (m[2] ?? "d").toLowerCase() : "d";
2753
+ const n = m ? parseFloat(m[1]) : NaN;
2754
+ if (unit === "mo" && Number.isFinite(n) && n > 0) {
2755
+ const result = new Date(date);
2756
+ result.setUTCMonth(result.getUTCMonth() - Math.round(n));
2757
+ return result;
2758
+ }
2759
+ return new Date(date.getTime() - durationToDays(duration) * DAY_MS);
2760
+ }
2738
2761
  function windowStartYmd(reset_at, duration) {
2739
- const days = durationToDays(duration);
2762
+ const now = Date.now();
2740
2763
  const reset = reset_at ? new Date(reset_at) : null;
2741
- const periodStart = reset && !Number.isNaN(reset.getTime()) ? new Date(reset.getTime() - days * DAY_MS) : null;
2742
- const trailingStart = new Date(Date.now() - days * DAY_MS);
2743
- const chosen = periodStart && periodStart > trailingStart ? periodStart : trailingStart;
2744
- return ymd(chosen);
2764
+ const resetMs = reset && !Number.isNaN(reset.getTime()) ? reset.getTime() : null;
2765
+ const trailingStart = subtractDuration(new Date(now), duration);
2766
+ if (resetMs !== null) {
2767
+ if (resetMs > now) {
2768
+ const periodStart = subtractDuration(reset, duration);
2769
+ return ymd(periodStart > trailingStart ? periodStart : trailingStart);
2770
+ } else {
2771
+ return ymd(reset > trailingStart ? reset : trailingStart);
2772
+ }
2773
+ }
2774
+ return ymd(trailingStart);
2745
2775
  }
2746
2776
  async function enrichSpendFromActivity(map) {
2747
2777
  const names = Object.keys(map);
@@ -2752,7 +2782,10 @@ async function enrichSpendFromActivity(map) {
2752
2782
  windows[name] = windowStartYmd(ti.budget_reset_at, ti.budget_duration);
2753
2783
  }
2754
2784
  try {
2755
- const spendByTag = await getTagSpendByWindow(windows, ymd(/* @__PURE__ */ new Date()));
2785
+ const spendByTag = await getTagSpendByWindow(
2786
+ windows,
2787
+ ymd(new Date(Date.now() + DAY_MS))
2788
+ );
2756
2789
  for (const name of names) {
2757
2790
  const spend = spendByTag[name];
2758
2791
  if (typeof spend === "number" && Number.isFinite(spend)) {
@@ -2876,6 +2909,7 @@ async function getUserBudgetView(userId) {
2876
2909
  readCache.set(tag, { expiry: Date.now() + READ_TTL_MS, view: null });
2877
2910
  return null;
2878
2911
  }
2912
+ await provisionDefaultUserBudget(userId);
2879
2913
  const info = await tagInfo([tag]);
2880
2914
  const ti = info[tag];
2881
2915
  if (ti?.max_budget != null) {
@@ -3847,7 +3881,7 @@ var init_entitlements = __esm({
3847
3881
  });
3848
3882
 
3849
3883
  // src/postgres/core-schema.ts
3850
- var agentMessagesSchema, agentSessionsSchema, skillsSchema, variablesSchema, projectsSchema, agentsSchema, modelsSchema, usersSchema, platformConfigurationsSchema, entityTypeSettingsSchema, promptLibrarySchema, promptFavoritesSchema, transcriptionJobsSchema, imageGenerationsSchema, oauthTokensSchema, contextPresetsSchema, addCoreFields, coreSchemas;
3884
+ var agentMessagesSchema, agentSessionsSchema, skillsSchema, variablesSchema, projectsSchema, agentsSchema, modelsSchema, usersSchema, platformConfigurationsSchema, entityTypeSettingsSchema, promptLibrarySchema, promptFavoritesSchema, transcriptionJobsSchema, imageGenerationsSchema, oauthTokensSchema, sharedArtifactsSchema, contextPresetsSchema, addCoreFields, coreSchemas;
3851
3885
  var init_core_schema = __esm({
3852
3886
  "src/postgres/core-schema.ts"() {
3853
3887
  "use strict";
@@ -4112,6 +4146,11 @@ var init_core_schema = __esm({
4112
4146
  {
4113
4147
  name: "animation_responding",
4114
4148
  type: "text"
4149
+ },
4150
+ {
4151
+ name: "sandbox_enabled",
4152
+ type: "boolean",
4153
+ default: false
4115
4154
  }
4116
4155
  ]
4117
4156
  };
@@ -4517,6 +4556,26 @@ var init_core_schema = __esm({
4517
4556
  // null = non-expiring
4518
4557
  ]
4519
4558
  };
4559
+ sharedArtifactsSchema = {
4560
+ type: "shared_artifacts",
4561
+ name: {
4562
+ plural: "shared_artifacts",
4563
+ singular: "shared_artifact"
4564
+ },
4565
+ // RBAC drives the "regular" auth_mode: rights_mode + the rbac table scope
4566
+ // who may view. public/password modes ignore rights_mode.
4567
+ RBAC: true,
4568
+ fields: [
4569
+ { name: "name", type: "text", index: true, unique: true, required: true },
4570
+ { name: "s3key", type: "text", required: true },
4571
+ { name: "auth_mode", type: "text", default: "regular" },
4572
+ { name: "password_hash", type: "text", required: false },
4573
+ // bcrypt; password mode only
4574
+ { name: "expires_at", type: "date", required: false },
4575
+ // null = no expiry
4576
+ { name: "content_type", type: "text", required: false }
4577
+ ]
4578
+ };
4520
4579
  contextPresetsSchema = {
4521
4580
  type: "context_presets",
4522
4581
  name: {
@@ -4609,6 +4668,7 @@ var init_core_schema = __esm({
4609
4668
  promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
4610
4669
  contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
4611
4670
  oauthTokensSchema: () => addCoreFields(oauthTokensSchema),
4671
+ sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
4612
4672
  transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
4613
4673
  imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
4614
4674
  };
@@ -5105,7 +5165,12 @@ var init_resolve_model = __esm({
5105
5165
  // supports — including Vertex Gemini, which translates it into
5106
5166
  // responseSchema/responseMimeType — so enabling this matches the actual
5107
5167
  // proxy contract.
5108
- supportsStructuredOutputs: true
5168
+ supportsStructuredOutputs: true,
5169
+ // Request token usage on STREAMED responses. Without this the openai-compatible
5170
+ // provider omits `stream_options: { include_usage: true }`, so LiteLLM returns no
5171
+ // usage for streaming calls — which zeroes out the per-request token metrics and
5172
+ // the message-footer token count (both read the AI SDK `totalUsage`/finish-part usage).
5173
+ includeUsage: true
5109
5174
  });
5110
5175
  };
5111
5176
  }
@@ -5768,7 +5833,8 @@ var init_vector_search = __esm({
5768
5833
  trigger,
5769
5834
  cutoffs,
5770
5835
  expand,
5771
- entityFilter
5836
+ entityFilter,
5837
+ queryEmbedding
5772
5838
  }) => {
5773
5839
  const table = convertContextToTableDefinition(context);
5774
5840
  console.log("[EXULU] Called vector search.", {
@@ -5850,36 +5916,53 @@ var init_vector_search = __esm({
5850
5916
  let vector = [];
5851
5917
  let vectorStr = "";
5852
5918
  let vectorExpr = "";
5919
+ const _tBody = Date.now();
5920
+ let _preMs = 0, _statMs = 0, _resolveMs = 0, _embedMs = 0;
5921
+ let _embedSource = "none";
5853
5922
  if (query) {
5923
+ const _tp = Date.now();
5854
5924
  const { processed: stemmedQuery } = preprocessQuery(query, {
5855
5925
  enableStemming: true,
5856
5926
  detectLanguage: true
5857
5927
  });
5928
+ _preMs = Date.now() - _tp;
5858
5929
  console.log("[EXULU] Stemmed query:", stemmedQuery);
5859
5930
  if (stemmedQuery) {
5860
5931
  query = stemmedQuery;
5861
5932
  }
5862
- await updateStatistic({
5863
- name: "count",
5864
- label: table.name.singular,
5865
- type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
5866
- trigger,
5867
- count: 1,
5868
- user: user?.id,
5869
- role
5870
- });
5871
- const resolved = await resolveEmbedder({
5872
- model: embedder.model,
5873
- contextId: context.id,
5874
- contextName: context.name,
5875
- user,
5876
- roleId: role
5877
- });
5878
- const [queryVector] = await resolved.embed([query], { inputType: "query" });
5879
- if (!queryVector?.length) {
5880
- throw new Error("No vector generated for query.");
5933
+ if (queryEmbedding && queryEmbedding.length) {
5934
+ vector = queryEmbedding;
5935
+ _embedSource = "reused";
5936
+ } else {
5937
+ const _ts = Date.now();
5938
+ await updateStatistic({
5939
+ name: "count",
5940
+ label: table.name.singular,
5941
+ type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
5942
+ trigger,
5943
+ count: 1,
5944
+ user: user?.id,
5945
+ role
5946
+ });
5947
+ _statMs = Date.now() - _ts;
5948
+ const _tr = Date.now();
5949
+ const resolved = await resolveEmbedder({
5950
+ model: embedder.model,
5951
+ contextId: context.id,
5952
+ contextName: context.name,
5953
+ user,
5954
+ roleId: role
5955
+ });
5956
+ _resolveMs = Date.now() - _tr;
5957
+ const _te = Date.now();
5958
+ const [queryVector] = await resolved.embed([query], { inputType: "query" });
5959
+ _embedMs = Date.now() - _te;
5960
+ if (!queryVector?.length) {
5961
+ throw new Error("No vector generated for query.");
5962
+ }
5963
+ vector = queryVector;
5964
+ _embedSource = "computed";
5881
5965
  }
5882
- vector = queryVector;
5883
5966
  vectorStr = `ARRAY[${vector.join(",")}]`;
5884
5967
  vectorExpr = `${vectorStr}::vector`;
5885
5968
  }
@@ -5906,6 +5989,7 @@ var init_vector_search = __esm({
5906
5989
  const languages = configuration.languages?.length ? configuration.languages : ["english"];
5907
5990
  console.log("[EXULU] Vector search params:", { method, query, cutoffs, languages });
5908
5991
  let resultChunks = [];
5992
+ const _tSql = Date.now();
5909
5993
  switch (method) {
5910
5994
  case "tsvector":
5911
5995
  chunksQuery.limit(limit * 2);
@@ -6019,6 +6103,11 @@ var init_vector_search = __esm({
6019
6103
  ]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
6020
6104
  resultChunks = await hybridQuery;
6021
6105
  }
6106
+ if (process.env.EXULU_VS_TIMING) {
6107
+ console.log(
6108
+ `[EXULU-VS] ctx=${context.id} method=${method} embed=${_embedSource} pre=${_preMs}ms stat=${_statMs}ms resolve=${_resolveMs}ms embedApi=${_embedMs}ms sql=${Date.now() - _tSql}ms total=${Date.now() - _tBody}ms`
6109
+ );
6110
+ }
6022
6111
  console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
6023
6112
  let results = resultChunks.map((chunk) => ({
6024
6113
  chunk_content: chunk.content,
@@ -6480,13 +6569,14 @@ var init_decorator = __esm({
6480
6569
  });
6481
6570
 
6482
6571
  // src/exulu/context.ts
6483
- var import_knex5, getTableName, getChunksTableName, ExuluContext2;
6572
+ var import_knex5, ExuluContext2;
6484
6573
  var init_context = __esm({
6485
6574
  "src/exulu/context.ts"() {
6486
6575
  "use strict";
6487
6576
  init_cjs_shims();
6488
6577
  init_storage();
6489
6578
  init_sanitize_name();
6579
+ init_table_names();
6490
6580
  import_knex5 = __toESM(require("pgvector/knex"), 1);
6491
6581
  init_chunker();
6492
6582
  init_resolve_embedder();
@@ -6497,15 +6587,10 @@ var init_context = __esm({
6497
6587
  init_vector_search();
6498
6588
  init_convert_context_to_table_definition();
6499
6589
  init_apply_filters();
6590
+ init_access_control();
6500
6591
  init_map_types();
6501
6592
  init_decorator();
6502
6593
  init_entities();
6503
- getTableName = (id) => {
6504
- return sanitizeName(id) + "_items";
6505
- };
6506
- getChunksTableName = (id) => {
6507
- return sanitizeName(id) + "_chunks";
6508
- };
6509
6594
  ExuluContext2 = class {
6510
6595
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
6511
6596
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -6999,12 +7084,18 @@ var init_context = __esm({
6999
7084
  };
7000
7085
  getItems = async ({
7001
7086
  filters,
7002
- fields
7087
+ fields,
7088
+ user,
7089
+ role
7003
7090
  }) => {
7004
7091
  const { db: db2 } = await postgresClient();
7005
- let query = db2.from(getTableName(this.id)).select(fields || "*");
7006
7092
  const tableDefinition = convertContextToTableDefinition(this);
7093
+ let query = db2.from(getTableName(this.id)).select(fields || "*");
7007
7094
  query = applyFilters(query, filters || [], tableDefinition);
7095
+ if (user) {
7096
+ const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
7097
+ query = applyAccessControl(tableDefinition, query, acUser);
7098
+ }
7008
7099
  const items = await query;
7009
7100
  return items;
7010
7101
  };
@@ -7365,11 +7456,9 @@ async function resolveReranker(input) {
7365
7456
  rerank_score: r.relevance_score ?? 0
7366
7457
  }));
7367
7458
  reranked.sort((a, b) => b.rerank_score - a.rerank_score);
7368
- import_fs.default.writeFileSync("reranked.json", JSON.stringify(reranked, null, 2));
7369
7459
  return reranked;
7370
7460
  } catch (err) {
7371
7461
  console.error("[EXULU] Error reranking:", err);
7372
- import_fs.default.writeFileSync("reranked.json", JSON.stringify(err, null, 2));
7373
7462
  return [];
7374
7463
  }
7375
7464
  };
@@ -7383,7 +7472,7 @@ var init_resolve_reranker = __esm({
7383
7472
  init_supervisor();
7384
7473
  init_budget_service();
7385
7474
  init_tags();
7386
- import_fs = __toESM(require("fs"), 1);
7475
+ import_fs = require("fs");
7387
7476
  ResolveRerankerError = class extends Error {
7388
7477
  constructor(code, message) {
7389
7478
  super(message);
@@ -7984,9 +8073,21 @@ var init_memory_tool = __esm({
7984
8073
  case "longText":
7985
8074
  case "shortText":
7986
8075
  case "code":
7987
- case "enum":
7988
8076
  fields[field.name] = import_zod4.z.string().describe("The " + field.name + " of the item to create");
7989
8077
  break;
8078
+ case "enum":
8079
+ if (field.enumValues && field.enumValues.length > 0) {
8080
+ const enumValues = field.enumValues;
8081
+ fields[field.name] = import_zod4.z.preprocess(
8082
+ (v) => typeof v === "string" ? v.toUpperCase() : v,
8083
+ import_zod4.z.enum(enumValues)
8084
+ ).describe(
8085
+ "The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
8086
+ );
8087
+ } else {
8088
+ fields[field.name] = import_zod4.z.string().describe("The " + field.name + " of the item to create");
8089
+ }
8090
+ break;
7990
8091
  case "json":
7991
8092
  fields[field.name] = import_zod4.z.string({}).describe(
7992
8093
  "The " + field.name + " of the item to create, it should be a valid JSON string."
@@ -8012,6 +8113,9 @@ var init_memory_tool = __esm({
8012
8113
  break;
8013
8114
  }
8014
8115
  }
8116
+ fields["visibility"] = import_zod4.z.enum(["private", "public"]).optional().describe(
8117
+ "Whether this memory is private to the user or shared (public). Ask the user if unknown."
8118
+ );
8015
8119
  const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
8016
8120
  return new ExuluTool({
8017
8121
  id: toolName,
@@ -8021,14 +8125,36 @@ var init_memory_tool = __esm({
8021
8125
  type: "function",
8022
8126
  inputSchema: import_zod4.z.object(fields),
8023
8127
  config: [],
8024
- execute: async ({ name, description, surroundingContext, mode, information, exuluConfig, user }) => {
8128
+ execute: async (params) => {
8129
+ const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
8025
8130
  let result = { result: "" };
8131
+ if (!visibility) {
8132
+ return {
8133
+ result: `Before saving this memory, ask the user whether it should be PRIVATE (visible only to them) or PUBLIC (shared with the team), then call \`${toolName}\` again with \`visibility\` set.`
8134
+ };
8135
+ }
8026
8136
  try {
8137
+ const extraFields = {};
8138
+ for (const field of context.fields ?? []) {
8139
+ if (field.type === "enum" && field.enumValues && field.enumValues.length > 0) {
8140
+ const raw = params[field.name];
8141
+ if (raw !== void 0 && raw !== null && raw !== "") {
8142
+ const rawStr = String(raw);
8143
+ const canonical = field.enumValues.find(
8144
+ (v) => v.toUpperCase() === rawStr.toUpperCase()
8145
+ );
8146
+ if (canonical !== void 0) {
8147
+ extraFields[field.name] = canonical;
8148
+ }
8149
+ }
8150
+ }
8151
+ }
8027
8152
  const newItem = {
8028
8153
  name,
8029
8154
  description: "Description: " + description + "\n\nSurrounding Context: " + surroundingContext,
8030
8155
  information: "Information: " + information,
8031
- rights_mode: "public"
8156
+ rights_mode: visibility === "private" ? "private" : "public",
8157
+ ...extraFields
8032
8158
  };
8033
8159
  const { item: createdItem, job: createdJob } = await context.createItem(
8034
8160
  newItem,
@@ -8841,7 +8967,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
8841
8967
  contexts = [];
8842
8968
  }
8843
8969
  let sharedSessionSandbox;
8844
- if (sessionID && exuluConfig) {
8970
+ if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
8845
8971
  try {
8846
8972
  sharedSessionSandbox = await createSessionSandbox(
8847
8973
  sessionID,
@@ -10922,11 +11048,13 @@ __export(index_exports, {
10922
11048
  ExuluProvider: () => ExuluProvider,
10923
11049
  ExuluPython: () => ExuluPython,
10924
11050
  ExuluQueues: () => queues,
11051
+ ExuluReadApi: () => ExuluReadApi,
10925
11052
  ExuluReranker: () => ExuluReranker,
10926
11053
  ExuluTool: () => ExuluTool,
10927
11054
  ExuluTrajectoryRegistry: () => trajectoryRegistry,
10928
11055
  ExuluVariables: () => ExuluVariables,
10929
- defaultChunker: () => defaultChunker
11056
+ defaultChunker: () => defaultChunker,
11057
+ postgresClient: () => postgresClient
10930
11058
  });
10931
11059
  module.exports = __toCommonJS(index_exports);
10932
11060
  init_cjs_shims();
@@ -10945,6 +11073,73 @@ var redisServer = {
10945
11073
  username: process.env.REDIS_USER || ""
10946
11074
  };
10947
11075
 
11076
+ // ee/queues/redis-startup.ts
11077
+ init_cjs_shims();
11078
+ var REDIS_STARTUP_TIMEOUT_MS = 6e4;
11079
+ var WATCHDOG_INTERVAL_MS = 1e4;
11080
+ var ERROR_LOG_THROTTLE_MS = 3e4;
11081
+ var log = (line) => console.log(`[EXULU-REDIS] ${line}`);
11082
+ var warn = (line) => console.warn(`[EXULU-REDIS] ${line}`);
11083
+ var errorLog = (line) => console.error(`[EXULU-REDIS] ${line}`);
11084
+ var redisAddress = () => `${redisServer.host || "(unset)"}:${redisServer.port || "(unset)"}`;
11085
+ var describeError = (e) => {
11086
+ const any = e;
11087
+ const head = any?.message ? String(any.message).split("\n")[0] : void 0;
11088
+ if (any?.code) return head && head !== any.code ? `${any.code} (${head})` : `${any.code}`;
11089
+ return head ?? String(e);
11090
+ };
11091
+ function logRedisErrors(source, label) {
11092
+ let count = 0;
11093
+ let lastLoggedAt = 0;
11094
+ source.on("error", (err) => {
11095
+ count += 1;
11096
+ const now = Date.now();
11097
+ if (count === 1 || now - lastLoggedAt >= ERROR_LOG_THROTTLE_MS) {
11098
+ errorLog(`${label} connection error (${redisAddress()}): ${describeError(err)}${count > 1 ? ` (x${count})` : ""}`);
11099
+ lastLoggedAt = now;
11100
+ }
11101
+ });
11102
+ }
11103
+ async function guardRedisStartup(label, run, source) {
11104
+ const addr = redisAddress();
11105
+ log(`Connecting to Redis (${addr}) for ${label}\u2026`);
11106
+ const startedAt = Date.now();
11107
+ let lastError;
11108
+ const onError = (err) => {
11109
+ lastError = err;
11110
+ };
11111
+ source?.on("error", onError);
11112
+ const watchdog = setInterval(() => {
11113
+ const secs = Math.round((Date.now() - startedAt) / 1e3);
11114
+ warn(
11115
+ `\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)`
11116
+ );
11117
+ }, WATCHDOG_INTERVAL_MS);
11118
+ watchdog.unref?.();
11119
+ let timer2;
11120
+ const timeout = new Promise((_resolve, reject) => {
11121
+ timer2 = setTimeout(() => {
11122
+ reject(
11123
+ new Error(
11124
+ `[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}.`
11125
+ )
11126
+ );
11127
+ }, REDIS_STARTUP_TIMEOUT_MS);
11128
+ });
11129
+ const runPromise = Promise.resolve().then(run);
11130
+ runPromise.catch(() => {
11131
+ });
11132
+ try {
11133
+ const result = await Promise.race([runPromise, timeout]);
11134
+ log(`Redis ready; ${label} initialized (${addr}, ${((Date.now() - startedAt) / 1e3).toFixed(1)}s).`);
11135
+ return result;
11136
+ } finally {
11137
+ clearInterval(watchdog);
11138
+ if (timer2) clearTimeout(timer2);
11139
+ source?.off?.("error", onError);
11140
+ }
11141
+ }
11142
+
10948
11143
  // src/redis/client.ts
10949
11144
  var client = {};
10950
11145
  async function redisClient() {
@@ -10962,9 +11157,11 @@ async function redisClient() {
10962
11157
  client["exulu"] = (0, import_redis.createClient)({
10963
11158
  url
10964
11159
  });
10965
- await client["exulu"].connect();
11160
+ logRedisErrors(client["exulu"], "client");
11161
+ await guardRedisStartup("client", () => client["exulu"].connect().then(() => void 0), client["exulu"]);
10966
11162
  } catch (error) {
10967
11163
  console.error(`[EXULU] error connecting to redis:`, error);
11164
+ delete client["exulu"];
10968
11165
  return { client: null };
10969
11166
  }
10970
11167
  }
@@ -10989,7 +11186,8 @@ init_client();
10989
11186
  init_auth();
10990
11187
  var requestValidators = {
10991
11188
  authenticate: async (req) => {
10992
- const apikey = req.headers["exulu-api-key"] || null;
11189
+ const rawApiKey = req.headers["exulu-api-key"] || req.headers["x-api-key"];
11190
+ const apikey = rawApiKey?.replace(/^Bearer\s+/i, "") || null;
10993
11191
  const { db: db2 } = await postgresClient();
10994
11192
  let authtoken = null;
10995
11193
  if (typeof apikey !== "string") {
@@ -11082,7 +11280,7 @@ var requestValidators = {
11082
11280
  init_statistics();
11083
11281
  init_client();
11084
11282
  var import_express5 = __toESM(require("express"), 1);
11085
- var import_server4 = require("@apollo/server");
11283
+ var import_server5 = require("@apollo/server");
11086
11284
  var import_cors = __toESM(require("cors"), 1);
11087
11285
  var import_reflect_metadata = require("reflect-metadata");
11088
11286
 
@@ -11275,6 +11473,14 @@ var ExuluQueues = class {
11275
11473
  },
11276
11474
  telemetry: new import_bullmq_otel.BullMQOtel("simple-guide")
11277
11475
  });
11476
+ logRedisErrors(newQueue, `queue "${name}"`);
11477
+ try {
11478
+ await guardRedisStartup(`queue "${name}"`, () => newQueue.waitUntilReady(), newQueue);
11479
+ } catch (err) {
11480
+ void newQueue.close().catch(() => {
11481
+ });
11482
+ throw err;
11483
+ }
11278
11484
  await newQueue.setGlobalConcurrency(queueConcurrency);
11279
11485
  this.queues.push({
11280
11486
  queue: newQueue,
@@ -13436,6 +13642,8 @@ var createWorkers = async (providers, queues2, config, contexts, evals, tools, t
13436
13642
  },
13437
13643
  maxRetriesPerRequest: null
13438
13644
  });
13645
+ logRedisErrors(redisConnection, "worker");
13646
+ await guardRedisStartup("workers", () => redisConnection.ping().then(() => void 0), redisConnection);
13439
13647
  }
13440
13648
  const workers = queues2.map((queue) => {
13441
13649
  console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
@@ -14615,7 +14823,7 @@ var renderTranscript = (segments, speakers) => {
14615
14823
 
14616
14824
  // src/exulu/transcription/service.ts
14617
14825
  var TABLE2 = "transcription_jobs";
14618
- var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
14826
+ var log3 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
14619
14827
  var parseJsonField = (v) => {
14620
14828
  if (v == null) return null;
14621
14829
  if (typeof v === "string") {
@@ -14686,7 +14894,7 @@ var transcriptionService = {
14686
14894
  error: err.message,
14687
14895
  updatedAt: /* @__PURE__ */ new Date()
14688
14896
  }).returning("*");
14689
- log2(`Failed to dispatch job ${row.id}: ${err.message}`);
14897
+ log3(`Failed to dispatch job ${row.id}: ${err.message}`);
14690
14898
  return this._rowFromDb(failed);
14691
14899
  }
14692
14900
  },
@@ -14714,9 +14922,9 @@ var transcriptionService = {
14714
14922
  updatedAt: /* @__PURE__ */ new Date()
14715
14923
  });
14716
14924
  } else if (err instanceof TranscriptionServerUnavailable) {
14717
- log2(`Whisper server unreachable while polling ${row.id}; will retry`);
14925
+ log3(`Whisper server unreachable while polling ${row.id}; will retry`);
14718
14926
  } else {
14719
- log2(`Error polling job ${row.id}: ${err.message}`);
14927
+ log3(`Error polling job ${row.id}: ${err.message}`);
14720
14928
  }
14721
14929
  }
14722
14930
  }
@@ -14766,7 +14974,7 @@ var transcriptionService = {
14766
14974
  } catch (err) {
14767
14975
  const code = err.code;
14768
14976
  if (code !== "JOB_NOT_FOUND") {
14769
- log2(`Best-effort cancel of whisper job failed: ${err.message}`);
14977
+ log3(`Best-effort cancel of whisper job failed: ${err.message}`);
14770
14978
  }
14771
14979
  }
14772
14980
  }
@@ -14852,7 +15060,7 @@ var transcriptionService = {
14852
15060
  []
14853
15061
  );
14854
15062
  } catch (err) {
14855
- log2(`RBAC update failed for item ${itemId}: ${err.message}`);
15063
+ log3(`RBAC update failed for item ${itemId}: ${err.message}`);
14856
15064
  }
14857
15065
  }
14858
15066
  const projectId = input.project_id ?? row.project_id ?? null;
@@ -15149,7 +15357,7 @@ var durationFromSegments = (segments) => {
15149
15357
  // src/exulu/recall/service.ts
15150
15358
  var TABLE3 = "transcription_jobs";
15151
15359
  var DEFAULT_BOT_NAME = "Exulu Notetaker";
15152
- var log3 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
15360
+ var log4 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
15153
15361
  var parseJson = (v) => {
15154
15362
  if (v == null) return null;
15155
15363
  if (typeof v === "string") {
@@ -15246,7 +15454,7 @@ var recallService = {
15246
15454
  error: err.message,
15247
15455
  updatedAt: /* @__PURE__ */ new Date()
15248
15456
  }).returning("*");
15249
- log3(`createBot failed for job ${inserted.id}: ${err.message}`);
15457
+ log4(`createBot failed for job ${inserted.id}: ${err.message}`);
15250
15458
  return this._row(failed);
15251
15459
  }
15252
15460
  },
@@ -15260,7 +15468,7 @@ var recallService = {
15260
15468
  const name = event?.event ?? "";
15261
15469
  const job = await this._findJob(ids.botId, ids.recordingId, ids.transcriptId);
15262
15470
  if (!job) {
15263
- log3(`No job for event ${name} (bot=${ids.botId}); ignoring.`);
15471
+ log4(`No job for event ${name} (bot=${ids.botId}); ignoring.`);
15264
15472
  return;
15265
15473
  }
15266
15474
  if (name.startsWith("bot.")) {
@@ -15285,12 +15493,12 @@ var recallService = {
15285
15493
  await this._fail(job.id, ids.subCode || ids.code || "transcript failed");
15286
15494
  return;
15287
15495
  default:
15288
- log3(`Unhandled event ${name} for job ${job.id}`);
15496
+ log4(`Unhandled event ${name} for job ${job.id}`);
15289
15497
  }
15290
15498
  },
15291
15499
  async _onRecordingDone(jobId, recordingId) {
15292
15500
  if (!recordingId) {
15293
- log3(`recording.done for job ${jobId} had no recording id`);
15501
+ log4(`recording.done for job ${jobId} had no recording id`);
15294
15502
  return;
15295
15503
  }
15296
15504
  const { db: db2 } = await postgresClient();
@@ -15300,7 +15508,7 @@ var recallService = {
15300
15508
  updatedAt: /* @__PURE__ */ new Date()
15301
15509
  });
15302
15510
  if (!claimed) {
15303
- log3(`recording.done for job ${jobId} already handled; skipping.`);
15511
+ log4(`recording.done for job ${jobId} already handled; skipping.`);
15304
15512
  return;
15305
15513
  }
15306
15514
  try {
@@ -15320,7 +15528,7 @@ var recallService = {
15320
15528
  if (!dbRow) return;
15321
15529
  const job = this._row(dbRow);
15322
15530
  if ((job.status === "awaiting_review" || job.status === "saved") && job.raw_segments && job.raw_segments.length > 0) {
15323
- log3(`transcript.done for job ${jobId} already processed; skipping.`);
15531
+ log4(`transcript.done for job ${jobId} already processed; skipping.`);
15324
15532
  return;
15325
15533
  }
15326
15534
  try {
@@ -15339,7 +15547,7 @@ var recallService = {
15339
15547
  const recDuration = recordingDurationSeconds(rec);
15340
15548
  if (recDuration != null) duration = recDuration;
15341
15549
  } catch (err) {
15342
- log3(`could not fetch recording duration for job ${jobId}: ${err.message}`);
15550
+ log4(`could not fetch recording duration for job ${jobId}: ${err.message}`);
15343
15551
  }
15344
15552
  }
15345
15553
  await this._update(jobId, {
@@ -15379,11 +15587,11 @@ var recallService = {
15379
15587
  const prompts = job.post_processing_prompts ?? [];
15380
15588
  if (prompts.length === 0) return [];
15381
15589
  if (job.post_processing_outputs && job.post_processing_outputs.length > 0) {
15382
- log3(`post-processing for job ${jobId} already ran; skipping.`);
15590
+ log4(`post-processing for job ${jobId} already ran; skipping.`);
15383
15591
  return job.post_processing_outputs;
15384
15592
  }
15385
15593
  if (!job.raw_segments || job.raw_segments.length === 0) {
15386
- log3(`post-processing for job ${jobId} skipped: no transcript.`);
15594
+ log4(`post-processing for job ${jobId} skipped: no transcript.`);
15387
15595
  return [];
15388
15596
  }
15389
15597
  const outputs = [];
@@ -15456,7 +15664,7 @@ ${transcriptText}`,
15456
15664
  ran_at: ranAt
15457
15665
  };
15458
15666
  } catch (err) {
15459
- log3(`post-processing prompt ${promptId} failed for job ${job.id}: ${err.message}`);
15667
+ log4(`post-processing prompt ${promptId} failed for job ${job.id}: ${err.message}`);
15460
15668
  return {
15461
15669
  prompt_id: promptId,
15462
15670
  agent_id: agentId,
@@ -19655,6 +19863,53 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
19655
19863
  return passed ? { ok: true } : { ok: false, reason: "signature mismatch" };
19656
19864
  };
19657
19865
 
19866
+ // src/exulu/shared-artifacts.ts
19867
+ init_cjs_shims();
19868
+ var import_bcryptjs4 = __toESM(require("bcryptjs"), 1);
19869
+ var normalizeS3Key = (key, bucket) => {
19870
+ const segments = key.split("/").filter((s, i) => !(i === 0 && s === "")).map((s) => decodeURIComponent(s));
19871
+ if (segments[0] === bucket) segments.shift();
19872
+ return segments.join("/");
19873
+ };
19874
+ var isHtmlKey = (key) => /\.html?$/i.test(key);
19875
+ var deriveFilename = (key) => {
19876
+ const base = key.split("/").pop() ?? key;
19877
+ return base.split("_EXULU_").pop() ?? base;
19878
+ };
19879
+ var slugifyShareName = (input) => deriveFilename(input).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
19880
+ var isExpired = (expiresAt, now) => {
19881
+ if (!expiresAt) return false;
19882
+ return new Date(expiresAt).getTime() <= now.getTime();
19883
+ };
19884
+ var validateCreateInput = (input, now) => {
19885
+ if (!input.s3key) return { ok: false, message: "s3key is required." };
19886
+ if (!input.name) return { ok: false, message: "name is required." };
19887
+ const mode = input.auth_mode;
19888
+ if (mode !== "public" && mode !== "password" && mode !== "regular") {
19889
+ return { ok: false, message: "auth_mode must be public, password, or regular." };
19890
+ }
19891
+ if (mode === "password" && !input.password) {
19892
+ return { ok: false, message: "A password is required for password mode." };
19893
+ }
19894
+ if (input.expires_at && Number.isNaN(new Date(input.expires_at).getTime())) {
19895
+ return { ok: false, message: "expires_at is not a valid date." };
19896
+ }
19897
+ if (input.expires_at && isExpired(input.expires_at, now)) {
19898
+ return { ok: false, message: "expires_at must be in the future." };
19899
+ }
19900
+ return { ok: true };
19901
+ };
19902
+ var hashSharePassword = (password) => import_bcryptjs4.default.hash(password, 10);
19903
+ var verifySharePassword = (password, hash) => import_bcryptjs4.default.compare(password, hash);
19904
+ var contentHeadersFor = (key, contentType, filename) => {
19905
+ if (isHtmlKey(key)) return { contentType: "text/html; charset=utf-8" };
19906
+ return {
19907
+ contentType: contentType || "application/octet-stream",
19908
+ disposition: `attachment; filename="${filename.replace(/"/g, "")}"`
19909
+ };
19910
+ };
19911
+ var getSharedArtifactByName = (db2, name) => db2("shared_artifacts").where({ name }).first();
19912
+
19658
19913
  // src/exulu/routes.ts
19659
19914
  var REQUEST_SIZE_LIMIT = "50mb";
19660
19915
  var getExuluVersionNumber = async () => {
@@ -19771,7 +20026,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
19771
20026
  config,
19772
20027
  evals
19773
20028
  );
19774
- const server = new import_server4.ApolloServer({
20029
+ const server = new import_server5.ApolloServer({
19775
20030
  cache: new import_utils5.InMemoryLRUCache(),
19776
20031
  schema,
19777
20032
  introspection: true
@@ -22689,6 +22944,126 @@ ${style.markdown}` : params.prompt;
22689
22944
  }
22690
22945
  res.status(204).send();
22691
22946
  });
22947
+ app.post("/shared-artifacts", async (req, res) => {
22948
+ const { db: db2 } = await postgresClient();
22949
+ const auth = await requestValidators.authenticate(req);
22950
+ if (!auth.user?.id) {
22951
+ res.status(401).json({ detail: "Authentication required." });
22952
+ return;
22953
+ }
22954
+ const now = /* @__PURE__ */ new Date();
22955
+ const valid = validateCreateInput(req.body, now);
22956
+ if (!valid.ok) {
22957
+ res.status(400).json({ detail: valid.message });
22958
+ return;
22959
+ }
22960
+ const bucket = config.fileUploads?.s3Bucket ?? "";
22961
+ const s3key = normalizeS3Key(req.body.s3key, bucket);
22962
+ const name = slugifyShareName(req.body.name);
22963
+ if (!name) {
22964
+ res.status(400).json({ detail: "name must contain url-safe characters." });
22965
+ return;
22966
+ }
22967
+ const existing = await getSharedArtifactByName(db2, name);
22968
+ if (existing) {
22969
+ res.status(409).json({ detail: "That share name is already taken." });
22970
+ return;
22971
+ }
22972
+ const auth_mode = req.body.auth_mode;
22973
+ const password_hash = auth_mode === "password" ? await hashSharePassword(req.body.password) : null;
22974
+ const rights_mode = auth_mode === "regular" ? req.body.rights_mode ?? "private" : "public";
22975
+ const [row] = await db2("shared_artifacts").insert({
22976
+ name,
22977
+ s3key,
22978
+ auth_mode,
22979
+ password_hash,
22980
+ expires_at: req.body.expires_at ?? null,
22981
+ content_type: req.body.content_type ?? null,
22982
+ rights_mode,
22983
+ created_by: auth.user.id
22984
+ }).returning("*");
22985
+ if (auth_mode === "regular" && req.body.rbac) {
22986
+ await handleRBACUpdate(db2, "shared_artifact", row.id, req.body.rbac, []);
22987
+ }
22988
+ res.status(201).json({ name: row.name });
22989
+ });
22990
+ app.get("/shared-artifacts/:name/meta", async (req, res) => {
22991
+ const { db: db2 } = await postgresClient();
22992
+ const row = await getSharedArtifactByName(db2, req.params.name ?? "");
22993
+ if (!row) {
22994
+ res.status(404).json({ detail: "Not found." });
22995
+ return;
22996
+ }
22997
+ if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
22998
+ res.status(410).json({ detail: "This link has expired." });
22999
+ return;
23000
+ }
23001
+ res.json({
23002
+ auth_mode: row.auth_mode,
23003
+ expires_at: row.expires_at,
23004
+ filename: deriveFilename(row.s3key),
23005
+ content_type: row.content_type,
23006
+ is_html: /\.html?$/i.test(row.s3key)
23007
+ });
23008
+ });
23009
+ app.get("/shared-artifacts/:name/content", async (req, res) => {
23010
+ const { db: db2 } = await postgresClient();
23011
+ const row = await getSharedArtifactByName(db2, req.params.name ?? "");
23012
+ if (!row) {
23013
+ res.status(404).json({ detail: "Not found." });
23014
+ return;
23015
+ }
23016
+ if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
23017
+ res.status(410).json({ detail: "This link has expired." });
23018
+ return;
23019
+ }
23020
+ if (row.auth_mode === "password") {
23021
+ const pw = req.headers["x-share-password"] || "";
23022
+ if (!row.password_hash || !await verifySharePassword(pw, row.password_hash)) {
23023
+ res.status(401).json({ detail: "Incorrect password." });
23024
+ return;
23025
+ }
23026
+ } else if (row.auth_mode === "regular") {
23027
+ const viewer = await requestValidators.authenticate(req);
23028
+ if (!viewer.user?.id) {
23029
+ res.status(401).json({ detail: "Authentication required." });
23030
+ return;
23031
+ }
23032
+ const rbac = await RBACResolver(
23033
+ db2,
23034
+ "shared_artifact",
23035
+ row.id,
23036
+ row.rights_mode || "private"
23037
+ );
23038
+ const ok = await checkRecordAccess({ ...row, RBAC: rbac }, "read", viewer.user);
23039
+ if (!ok) {
23040
+ res.status(403).json({ detail: "You don't have access to this artifact." });
23041
+ return;
23042
+ }
23043
+ }
23044
+ let bytes;
23045
+ try {
23046
+ bytes = await getS3ObjectBytes(row.s3key, config);
23047
+ } catch (e) {
23048
+ if (e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.$metadata?.httpStatusCode === 404) {
23049
+ res.status(404).json({ detail: "Artifact file not found." });
23050
+ return;
23051
+ }
23052
+ console.error("[EXULU] shared-artifact content read failed", e);
23053
+ res.status(500).json({ detail: "Failed to read artifact." });
23054
+ return;
23055
+ }
23056
+ const headers = contentHeadersFor(
23057
+ row.s3key,
23058
+ row.content_type,
23059
+ deriveFilename(row.s3key)
23060
+ );
23061
+ res.setHeader("Content-Type", headers.contentType);
23062
+ res.setHeader("X-Content-Type-Options", "nosniff");
23063
+ res.setHeader("Cache-Control", "private, no-store");
23064
+ if (headers.disposition) res.setHeader("Content-Disposition", headers.disposition);
23065
+ res.send(bytes);
23066
+ });
22692
23067
  app.use(import_express5.default.static("public"));
22693
23068
  await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config);
22694
23069
  return app;
@@ -26668,6 +27043,83 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
26668
27043
  // src/index.ts
26669
27044
  init_chunker();
26670
27045
  init_context();
27046
+
27047
+ // src/exulu/read-api.ts
27048
+ init_cjs_shims();
27049
+ init_table_names();
27050
+ init_types();
27051
+ init_client();
27052
+ init_access_control();
27053
+ init_convert_context_to_table_definition();
27054
+ init_resolve_embedder();
27055
+ var authorizedRead = async (context, user, role, opts = {}) => {
27056
+ if (!opts.itemIds?.length && !opts.externalIds?.length) {
27057
+ throw new Error("authorizedRead requires itemIds or externalIds to constrain the read.");
27058
+ }
27059
+ const { db: db2 } = await postgresClient();
27060
+ const table = convertContextToTableDefinition(context);
27061
+ const itemsTable = getTableName(context.id);
27062
+ const chunksTable = getChunksTableName(context.id);
27063
+ const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
27064
+ let q = db2(chunksTable + " as chunks").select([
27065
+ "chunks.id as chunk_id",
27066
+ "chunks.source as chunk_source",
27067
+ "chunks.content as chunk_content",
27068
+ "chunks.chunk_index",
27069
+ "chunks.metadata as chunk_metadata",
27070
+ db2.raw('chunks."createdAt" as chunk_created_at'),
27071
+ db2.raw('chunks."updatedAt" as chunk_updated_at'),
27072
+ "items.id as item_id",
27073
+ "items.name as item_name",
27074
+ "items.external_id as item_external_id",
27075
+ db2.raw('items."createdAt" as item_created_at'),
27076
+ db2.raw('items."updatedAt" as item_updated_at')
27077
+ ]);
27078
+ q = q.leftJoin(itemsTable + " as items", "chunks.source", "items.id");
27079
+ if (opts.itemIds?.length) q = q.whereIn("items.id", opts.itemIds);
27080
+ if (opts.externalIds?.length) q = q.whereIn("items.external_id", opts.externalIds);
27081
+ if (opts.chunkIndexRange) {
27082
+ const { from, to } = opts.chunkIndexRange;
27083
+ if (typeof from === "number") q = q.where("chunks.chunk_index", ">=", from);
27084
+ if (typeof to === "number") q = q.where("chunks.chunk_index", "<=", to);
27085
+ }
27086
+ q = applyAccessControl(table, q, acUser, "items");
27087
+ q = q.orderBy("chunks.source").orderBy("chunks.chunk_index");
27088
+ return await q;
27089
+ };
27090
+ var entitiesAvailable = async (context) => {
27091
+ if (!context.entities) return false;
27092
+ const { db: db2 } = await postgresClient();
27093
+ const table = getChunkEntitiesTableName(context.id);
27094
+ const res = await db2.raw(
27095
+ "SELECT to_regclass(?) IS NOT NULL AS exists",
27096
+ [table]
27097
+ );
27098
+ return res.rows?.[0]?.exists === true;
27099
+ };
27100
+ var embedQuery = async (context, text, opts = {}) => {
27101
+ const resolved = await resolveEmbedder({
27102
+ model: context.embedder.model,
27103
+ contextId: context.id,
27104
+ contextName: context.name,
27105
+ user: opts.user,
27106
+ roleId: opts.role
27107
+ });
27108
+ const [vector] = await resolved.embed([text], { inputType: opts.inputType ?? "query" });
27109
+ return vector ?? [];
27110
+ };
27111
+ var ExuluReadApi = {
27112
+ getTableName,
27113
+ getChunksTableName,
27114
+ getEntitiesTableName,
27115
+ getChunkEntitiesTableName,
27116
+ entitiesAvailable,
27117
+ authorizedRead,
27118
+ embedQuery
27119
+ };
27120
+
27121
+ // src/index.ts
27122
+ init_client();
26671
27123
  init_tool();
26672
27124
  init_sentence2();
26673
27125
 
@@ -26704,7 +27156,8 @@ var {
26704
27156
  promptFavoritesSchema: promptFavoritesSchema3,
26705
27157
  transcriptionJobsSchema: transcriptionJobsSchema3,
26706
27158
  imageGenerationsSchema: imageGenerationsSchema2,
26707
- oauthTokensSchema: oauthTokensSchema2
27159
+ oauthTokensSchema: oauthTokensSchema2,
27160
+ sharedArtifactsSchema: sharedArtifactsSchema2
26708
27161
  } = coreSchemas.get();
26709
27162
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
26710
27163
  for (const field of fields) {
@@ -26748,6 +27201,7 @@ var up = async function(knex) {
26748
27201
  transcriptionJobsSchema3(),
26749
27202
  imageGenerationsSchema2(),
26750
27203
  oauthTokensSchema2(),
27204
+ sharedArtifactsSchema2(),
26751
27205
  rbacSchema3(),
26752
27206
  agentsSchema3(),
26753
27207
  feedbackSchema3(),
@@ -27038,7 +27492,7 @@ var checkLiteLLMDatabaseSafety = (configPath) => {
27038
27492
 
27039
27493
  // src/exulu/litellm/db-init.ts
27040
27494
  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";
27041
- var warn = (lines) => {
27495
+ var warn2 = (lines) => {
27042
27496
  console.warn(`
27043
27497
  ${WARNING_BANNER}`);
27044
27498
  console.warn("\u26A0 [EXULU-LITELLM] CONFIGURATION WARNING");
@@ -27046,13 +27500,13 @@ ${WARNING_BANNER}`);
27046
27500
  console.warn(`${WARNING_BANNER}
27047
27501
  `);
27048
27502
  };
27049
- var log4 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
27503
+ var log5 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
27050
27504
  var initLiteLLMDatabase = async (packageRoot) => {
27051
27505
  const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path9.resolve)(process.cwd(), "./config.litellm.yaml");
27052
27506
  const safety = checkLiteLLMDatabaseSafety(configPath);
27053
27507
  if (safety.ok && safety.reason === "no-litellm-db-mode") return;
27054
27508
  if (!safety.ok && safety.reason === "unparseable-url") {
27055
- warn([
27509
+ warn2([
27056
27510
  `LiteLLM's database_url is not a valid postgres URL:`,
27057
27511
  ` ${safety.rawUrl}`,
27058
27512
  `Expected postgres://user:pass@host:port/database. Skipping setup.`
@@ -27061,7 +27515,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27061
27515
  }
27062
27516
  if (!safety.ok && safety.reason === "shared-with-exulu") {
27063
27517
  const { exuluTarget } = safety;
27064
- warn([
27518
+ warn2([
27065
27519
  `LiteLLM's database_url points to the SAME database Exulu uses:`,
27066
27520
  ` ${exuluTarget.host}:${exuluTarget.port}/${exuluTarget.database}`,
27067
27521
  ``,
@@ -27080,7 +27534,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27080
27534
  return;
27081
27535
  }
27082
27536
  const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
27083
- log4(
27537
+ log5(
27084
27538
  `LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
27085
27539
  );
27086
27540
  const ensureDatabaseExists2 = async () => {
@@ -27092,7 +27546,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27092
27546
  } catch (err) {
27093
27547
  const code = err?.code;
27094
27548
  if (code !== "3D000") {
27095
- warn([
27549
+ warn2([
27096
27550
  `Could not connect to LiteLLM's target database:`,
27097
27551
  ` ${err instanceof Error ? err.message : String(err)}`,
27098
27552
  ``,
@@ -27104,13 +27558,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
27104
27558
  const url = new URL(litellmUrl);
27105
27559
  const targetDbName = url.pathname.replace(/^\//, "");
27106
27560
  if (!targetDbName) {
27107
- warn([`LiteLLM database_url has no database name; cannot auto-create.`]);
27561
+ warn2([`LiteLLM database_url has no database name; cannot auto-create.`]);
27108
27562
  return false;
27109
27563
  }
27110
27564
  url.pathname = "/postgres";
27111
- log4(`Target database "${targetDbName}" does not exist; creating it\u2026`);
27565
+ log5(`Target database "${targetDbName}" does not exist; creating it\u2026`);
27112
27566
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
27113
- warn([
27567
+ warn2([
27114
27568
  `Refusing to auto-create database "${targetDbName}" \u2014 name`,
27115
27569
  `contains characters that would require quoting. Create it`,
27116
27570
  `manually: createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`
@@ -27121,10 +27575,10 @@ var initLiteLLMDatabase = async (packageRoot) => {
27121
27575
  try {
27122
27576
  await admin.connect();
27123
27577
  await admin.query(`CREATE DATABASE "${targetDbName}"`);
27124
- log4(`\u2713 Created database "${targetDbName}".`);
27578
+ log5(`\u2713 Created database "${targetDbName}".`);
27125
27579
  return true;
27126
27580
  } catch (createErr) {
27127
- warn([
27581
+ warn2([
27128
27582
  `Failed to auto-create database "${targetDbName}":`,
27129
27583
  ` ${createErr instanceof Error ? createErr.message : String(createErr)}`,
27130
27584
  ``,
@@ -27143,7 +27597,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27143
27597
  }
27144
27598
  };
27145
27599
  if (!await ensureDatabaseExists2()) return;
27146
- log4("Checking that the target database is safe to push into\u2026");
27600
+ log5("Checking that the target database is safe to push into\u2026");
27147
27601
  const client2 = new import_pg.Client({ connectionString: litellmUrl });
27148
27602
  let foreignTables = [];
27149
27603
  try {
@@ -27159,7 +27613,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27159
27613
  );
27160
27614
  foreignTables = res.rows.map((r) => r.table_name);
27161
27615
  } catch (err) {
27162
- warn([
27616
+ warn2([
27163
27617
  `Could not query LiteLLM's target database to verify it is safe`,
27164
27618
  `to push into:`,
27165
27619
  ` ${err instanceof Error ? err.message : String(err)}`,
@@ -27175,7 +27629,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27175
27629
  }
27176
27630
  }
27177
27631
  if (foreignTables.length > 0) {
27178
- warn([
27632
+ warn2([
27179
27633
  `LiteLLM's target database contains ${foreignTables.length} table(s) that are NOT`,
27180
27634
  `part of LiteLLM's schema:`,
27181
27635
  ...foreignTables.slice(0, 10).map((t) => ` - ${t}`),
@@ -27191,7 +27645,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27191
27645
  const venvLibDir = (0, import_node_path9.resolve)(packageRoot, "ee/python/.venv/lib");
27192
27646
  const pythonVersionDir = (0, import_node_fs9.existsSync)(venvLibDir) ? (0, import_node_fs9.readdirSync)(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
27193
27647
  if (!pythonVersionDir) {
27194
- warn([
27648
+ warn2([
27195
27649
  `Could not find a python3.* directory under ${venvLibDir}.`,
27196
27650
  `Run \`npm run python:setup\` to create the venv.`,
27197
27651
  `Skipping LiteLLM database setup.`
@@ -27205,7 +27659,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27205
27659
  );
27206
27660
  const schemaPath = (0, import_node_path9.resolve)(litellmProxyDir, "schema.prisma");
27207
27661
  if (!(0, import_node_fs9.existsSync)(prismaCli)) {
27208
- warn([
27662
+ warn2([
27209
27663
  `Prisma CLI not found at ${prismaCli}.`,
27210
27664
  `Run \`npm run python:setup\` to create the venv and install prisma.`,
27211
27665
  `Skipping LiteLLM database setup.`
@@ -27213,13 +27667,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
27213
27667
  return;
27214
27668
  }
27215
27669
  if (!(0, import_node_fs9.existsSync)(schemaPath)) {
27216
- warn([
27670
+ warn2([
27217
27671
  `LiteLLM Prisma schema not found at ${schemaPath}.`,
27218
27672
  `Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
27219
27673
  ]);
27220
27674
  return;
27221
27675
  }
27222
- log4("Running `prisma db push` against LiteLLM's schema\u2026");
27676
+ log5("Running `prisma db push` against LiteLLM's schema\u2026");
27223
27677
  const result = (0, import_node_child_process5.spawnSync)(prismaCli, ["db", "push", "--skip-generate"], {
27224
27678
  cwd: litellmProxyDir,
27225
27679
  env: {
@@ -27233,14 +27687,14 @@ var initLiteLLMDatabase = async (packageRoot) => {
27233
27687
  encoding: "utf8"
27234
27688
  });
27235
27689
  if (result.error) {
27236
- warn([
27690
+ warn2([
27237
27691
  `Failed to launch prisma: ${result.error.message}`,
27238
27692
  `Skipping LiteLLM database setup.`
27239
27693
  ]);
27240
27694
  return;
27241
27695
  }
27242
27696
  if (result.status !== 0) {
27243
- warn([
27697
+ warn2([
27244
27698
  `prisma db push exited with status ${result.status}.`,
27245
27699
  `stdout:`,
27246
27700
  ...(result.stdout || "(empty)").split("\n").map((l) => ` ${l}`),
@@ -27249,7 +27703,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27249
27703
  ]);
27250
27704
  return;
27251
27705
  }
27252
- log4("\u2713 LiteLLM database ready.");
27706
+ log5("\u2713 LiteLLM database ready.");
27253
27707
  };
27254
27708
 
27255
27709
  // src/postgres/init-litellm-db.ts
@@ -28532,14 +28986,37 @@ ${setupResult.output || ""}`);
28532
28986
  model: config.processor.model ?? "mistral-ocr",
28533
28987
  ...config.attribution
28534
28988
  });
28535
- await new Promise((resolve8) => setTimeout(resolve8, Math.floor(Math.random() * 4e3) + 1e3));
28536
- const base64Pdf = buffer.toString("base64");
28537
- const ocrResponse = await withRetry(async () => {
28538
- return await resolved.ocr({
28539
- type: "document_url",
28540
- document_url: "data:application/pdf;base64," + base64Pdf
28541
- }, { includeImageBase64: false });
28542
- }, 10);
28989
+ const maxPagesPerChunk = config.processor.maxPagesPerChunk ?? 25;
28990
+ const chunksDir = path2.join(path2.dirname(paths.json), "ocr_chunks");
28991
+ const splitResult = await executePythonScript({
28992
+ scriptPath: "ee/python/documents/processing/split_pdf.py",
28993
+ args: [paths.source, chunksDir, "--chunk-size", String(maxPagesPerChunk)],
28994
+ timeout: 5 * 60 * 1e3
28995
+ });
28996
+ const pdfChunks = JSON.parse(splitResult.stdout);
28997
+ console.log(`[EXULU] PDF split into ${pdfChunks.length} chunk(s) for OCR (max ${maxPagesPerChunk} pages each)`);
28998
+ const chunkLimit = (0, import_p_limit.default)(3);
28999
+ const chunkResults = await Promise.all(
29000
+ pdfChunks.map(
29001
+ (chunk, i) => chunkLimit(async () => {
29002
+ await new Promise((resolve8) => setImmediate(resolve8));
29003
+ await new Promise((resolve8) => setTimeout(resolve8, Math.floor(Math.random() * 1e3) + 200));
29004
+ console.log(`[EXULU] OCR chunk ${i + 1}/${pdfChunks.length}: pages ${chunk.start_page}\u2013${chunk.end_page - 1}`);
29005
+ const chunkBuffer = await fs5.promises.readFile(chunk.path);
29006
+ const chunkBase64 = chunkBuffer.toString("base64");
29007
+ const chunkResponse = await withRetry(async () => {
29008
+ return await resolved.ocr({
29009
+ type: "document_url",
29010
+ document_url: "data:application/pdf;base64," + chunkBase64
29011
+ }, { includeImageBase64: false });
29012
+ }, 10);
29013
+ return { pages: chunkResponse.pages, offset: chunk.start_page };
29014
+ })
29015
+ )
29016
+ );
29017
+ const mergedPages = chunkResults.sort((a, b) => a.offset - b.offset).flatMap(
29018
+ ({ pages, offset }) => pages.map((p) => ({ ...p, index: p.index + offset }))
29019
+ );
28543
29020
  const parser = new import_liteparse.LiteParse();
28544
29021
  const screenshots = await parser.screenshot(paths.source, void 0);
28545
29022
  await fs5.promises.mkdir(paths.images, { recursive: true });
@@ -28553,7 +29030,7 @@ ${setupResult.output || ""}`);
28553
29030
  );
28554
29031
  screenshot.imagePath = path2.join(paths.images, `${screenshot.pageNum}.png`);
28555
29032
  }
28556
- json = ocrResponse.pages.map((page) => ({
29033
+ json = mergedPages.map((page) => ({
28557
29034
  page: page.index + 1,
28558
29035
  content: page.markdown,
28559
29036
  image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
@@ -28887,9 +29364,11 @@ var ExuluPython = {
28887
29364
  ExuluProvider,
28888
29365
  ExuluPython,
28889
29366
  ExuluQueues,
29367
+ ExuluReadApi,
28890
29368
  ExuluReranker,
28891
29369
  ExuluTool,
28892
29370
  ExuluTrajectoryRegistry,
28893
29371
  ExuluVariables,
28894
- defaultChunker
29372
+ defaultChunker,
29373
+ postgresClient
28895
29374
  });