@exulu/backend 1.68.0 → 1.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
@@ -2737,11 +2749,20 @@ function durationToDays(duration) {
2737
2749
  }
2738
2750
  function windowStartYmd(reset_at, duration) {
2739
2751
  const days = durationToDays(duration);
2752
+ const windowMs = days * DAY_MS;
2753
+ const now = Date.now();
2740
2754
  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);
2755
+ const resetMs = reset && !Number.isNaN(reset.getTime()) ? reset.getTime() : null;
2756
+ const trailingStart = new Date(now - windowMs);
2757
+ if (resetMs !== null) {
2758
+ if (resetMs > now) {
2759
+ const periodStart = new Date(resetMs - windowMs);
2760
+ return ymd(periodStart > trailingStart ? periodStart : trailingStart);
2761
+ } else {
2762
+ return ymd(reset > trailingStart ? reset : trailingStart);
2763
+ }
2764
+ }
2765
+ return ymd(trailingStart);
2745
2766
  }
2746
2767
  async function enrichSpendFromActivity(map) {
2747
2768
  const names = Object.keys(map);
@@ -2752,7 +2773,10 @@ async function enrichSpendFromActivity(map) {
2752
2773
  windows[name] = windowStartYmd(ti.budget_reset_at, ti.budget_duration);
2753
2774
  }
2754
2775
  try {
2755
- const spendByTag = await getTagSpendByWindow(windows, ymd(/* @__PURE__ */ new Date()));
2776
+ const spendByTag = await getTagSpendByWindow(
2777
+ windows,
2778
+ ymd(new Date(Date.now() + DAY_MS))
2779
+ );
2756
2780
  for (const name of names) {
2757
2781
  const spend = spendByTag[name];
2758
2782
  if (typeof spend === "number" && Number.isFinite(spend)) {
@@ -2876,6 +2900,7 @@ async function getUserBudgetView(userId) {
2876
2900
  readCache.set(tag, { expiry: Date.now() + READ_TTL_MS, view: null });
2877
2901
  return null;
2878
2902
  }
2903
+ await provisionDefaultUserBudget(userId);
2879
2904
  const info = await tagInfo([tag]);
2880
2905
  const ti = info[tag];
2881
2906
  if (ti?.max_budget != null) {
@@ -3847,7 +3872,7 @@ var init_entitlements = __esm({
3847
3872
  });
3848
3873
 
3849
3874
  // 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;
3875
+ var agentMessagesSchema, agentSessionsSchema, skillsSchema, variablesSchema, projectsSchema, agentsSchema, modelsSchema, usersSchema, platformConfigurationsSchema, entityTypeSettingsSchema, promptLibrarySchema, promptFavoritesSchema, transcriptionJobsSchema, imageGenerationsSchema, oauthTokensSchema, sharedArtifactsSchema, contextPresetsSchema, addCoreFields, coreSchemas;
3851
3876
  var init_core_schema = __esm({
3852
3877
  "src/postgres/core-schema.ts"() {
3853
3878
  "use strict";
@@ -4112,6 +4137,11 @@ var init_core_schema = __esm({
4112
4137
  {
4113
4138
  name: "animation_responding",
4114
4139
  type: "text"
4140
+ },
4141
+ {
4142
+ name: "sandbox_enabled",
4143
+ type: "boolean",
4144
+ default: false
4115
4145
  }
4116
4146
  ]
4117
4147
  };
@@ -4517,6 +4547,26 @@ var init_core_schema = __esm({
4517
4547
  // null = non-expiring
4518
4548
  ]
4519
4549
  };
4550
+ sharedArtifactsSchema = {
4551
+ type: "shared_artifacts",
4552
+ name: {
4553
+ plural: "shared_artifacts",
4554
+ singular: "shared_artifact"
4555
+ },
4556
+ // RBAC drives the "regular" auth_mode: rights_mode + the rbac table scope
4557
+ // who may view. public/password modes ignore rights_mode.
4558
+ RBAC: true,
4559
+ fields: [
4560
+ { name: "name", type: "text", index: true, unique: true, required: true },
4561
+ { name: "s3key", type: "text", required: true },
4562
+ { name: "auth_mode", type: "text", default: "regular" },
4563
+ { name: "password_hash", type: "text", required: false },
4564
+ // bcrypt; password mode only
4565
+ { name: "expires_at", type: "date", required: false },
4566
+ // null = no expiry
4567
+ { name: "content_type", type: "text", required: false }
4568
+ ]
4569
+ };
4520
4570
  contextPresetsSchema = {
4521
4571
  type: "context_presets",
4522
4572
  name: {
@@ -4609,6 +4659,7 @@ var init_core_schema = __esm({
4609
4659
  promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
4610
4660
  contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
4611
4661
  oauthTokensSchema: () => addCoreFields(oauthTokensSchema),
4662
+ sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
4612
4663
  transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
4613
4664
  imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
4614
4665
  };
@@ -5105,7 +5156,12 @@ var init_resolve_model = __esm({
5105
5156
  // supports — including Vertex Gemini, which translates it into
5106
5157
  // responseSchema/responseMimeType — so enabling this matches the actual
5107
5158
  // proxy contract.
5108
- supportsStructuredOutputs: true
5159
+ supportsStructuredOutputs: true,
5160
+ // Request token usage on STREAMED responses. Without this the openai-compatible
5161
+ // provider omits `stream_options: { include_usage: true }`, so LiteLLM returns no
5162
+ // usage for streaming calls — which zeroes out the per-request token metrics and
5163
+ // the message-footer token count (both read the AI SDK `totalUsage`/finish-part usage).
5164
+ includeUsage: true
5109
5165
  });
5110
5166
  };
5111
5167
  }
@@ -5768,7 +5824,8 @@ var init_vector_search = __esm({
5768
5824
  trigger,
5769
5825
  cutoffs,
5770
5826
  expand,
5771
- entityFilter
5827
+ entityFilter,
5828
+ queryEmbedding
5772
5829
  }) => {
5773
5830
  const table = convertContextToTableDefinition(context);
5774
5831
  console.log("[EXULU] Called vector search.", {
@@ -5850,36 +5907,53 @@ var init_vector_search = __esm({
5850
5907
  let vector = [];
5851
5908
  let vectorStr = "";
5852
5909
  let vectorExpr = "";
5910
+ const _tBody = Date.now();
5911
+ let _preMs = 0, _statMs = 0, _resolveMs = 0, _embedMs = 0;
5912
+ let _embedSource = "none";
5853
5913
  if (query) {
5914
+ const _tp = Date.now();
5854
5915
  const { processed: stemmedQuery } = preprocessQuery(query, {
5855
5916
  enableStemming: true,
5856
5917
  detectLanguage: true
5857
5918
  });
5919
+ _preMs = Date.now() - _tp;
5858
5920
  console.log("[EXULU] Stemmed query:", stemmedQuery);
5859
5921
  if (stemmedQuery) {
5860
5922
  query = stemmedQuery;
5861
5923
  }
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.");
5924
+ if (queryEmbedding && queryEmbedding.length) {
5925
+ vector = queryEmbedding;
5926
+ _embedSource = "reused";
5927
+ } else {
5928
+ const _ts = Date.now();
5929
+ await updateStatistic({
5930
+ name: "count",
5931
+ label: table.name.singular,
5932
+ type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
5933
+ trigger,
5934
+ count: 1,
5935
+ user: user?.id,
5936
+ role
5937
+ });
5938
+ _statMs = Date.now() - _ts;
5939
+ const _tr = Date.now();
5940
+ const resolved = await resolveEmbedder({
5941
+ model: embedder.model,
5942
+ contextId: context.id,
5943
+ contextName: context.name,
5944
+ user,
5945
+ roleId: role
5946
+ });
5947
+ _resolveMs = Date.now() - _tr;
5948
+ const _te = Date.now();
5949
+ const [queryVector] = await resolved.embed([query], { inputType: "query" });
5950
+ _embedMs = Date.now() - _te;
5951
+ if (!queryVector?.length) {
5952
+ throw new Error("No vector generated for query.");
5953
+ }
5954
+ vector = queryVector;
5955
+ _embedSource = "computed";
5881
5956
  }
5882
- vector = queryVector;
5883
5957
  vectorStr = `ARRAY[${vector.join(",")}]`;
5884
5958
  vectorExpr = `${vectorStr}::vector`;
5885
5959
  }
@@ -5906,6 +5980,7 @@ var init_vector_search = __esm({
5906
5980
  const languages = configuration.languages?.length ? configuration.languages : ["english"];
5907
5981
  console.log("[EXULU] Vector search params:", { method, query, cutoffs, languages });
5908
5982
  let resultChunks = [];
5983
+ const _tSql = Date.now();
5909
5984
  switch (method) {
5910
5985
  case "tsvector":
5911
5986
  chunksQuery.limit(limit * 2);
@@ -6019,6 +6094,11 @@ var init_vector_search = __esm({
6019
6094
  ]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
6020
6095
  resultChunks = await hybridQuery;
6021
6096
  }
6097
+ if (process.env.EXULU_VS_TIMING) {
6098
+ console.log(
6099
+ `[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`
6100
+ );
6101
+ }
6022
6102
  console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
6023
6103
  let results = resultChunks.map((chunk) => ({
6024
6104
  chunk_content: chunk.content,
@@ -6480,13 +6560,14 @@ var init_decorator = __esm({
6480
6560
  });
6481
6561
 
6482
6562
  // src/exulu/context.ts
6483
- var import_knex5, getTableName, getChunksTableName, ExuluContext2;
6563
+ var import_knex5, ExuluContext2;
6484
6564
  var init_context = __esm({
6485
6565
  "src/exulu/context.ts"() {
6486
6566
  "use strict";
6487
6567
  init_cjs_shims();
6488
6568
  init_storage();
6489
6569
  init_sanitize_name();
6570
+ init_table_names();
6490
6571
  import_knex5 = __toESM(require("pgvector/knex"), 1);
6491
6572
  init_chunker();
6492
6573
  init_resolve_embedder();
@@ -6497,15 +6578,10 @@ var init_context = __esm({
6497
6578
  init_vector_search();
6498
6579
  init_convert_context_to_table_definition();
6499
6580
  init_apply_filters();
6581
+ init_access_control();
6500
6582
  init_map_types();
6501
6583
  init_decorator();
6502
6584
  init_entities();
6503
- getTableName = (id) => {
6504
- return sanitizeName(id) + "_items";
6505
- };
6506
- getChunksTableName = (id) => {
6507
- return sanitizeName(id) + "_chunks";
6508
- };
6509
6585
  ExuluContext2 = class {
6510
6586
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
6511
6587
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -6999,12 +7075,18 @@ var init_context = __esm({
6999
7075
  };
7000
7076
  getItems = async ({
7001
7077
  filters,
7002
- fields
7078
+ fields,
7079
+ user,
7080
+ role
7003
7081
  }) => {
7004
7082
  const { db: db2 } = await postgresClient();
7005
- let query = db2.from(getTableName(this.id)).select(fields || "*");
7006
7083
  const tableDefinition = convertContextToTableDefinition(this);
7084
+ let query = db2.from(getTableName(this.id)).select(fields || "*");
7007
7085
  query = applyFilters(query, filters || [], tableDefinition);
7086
+ if (user) {
7087
+ const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
7088
+ query = applyAccessControl(tableDefinition, query, acUser);
7089
+ }
7008
7090
  const items = await query;
7009
7091
  return items;
7010
7092
  };
@@ -7365,11 +7447,9 @@ async function resolveReranker(input) {
7365
7447
  rerank_score: r.relevance_score ?? 0
7366
7448
  }));
7367
7449
  reranked.sort((a, b) => b.rerank_score - a.rerank_score);
7368
- import_fs.default.writeFileSync("reranked.json", JSON.stringify(reranked, null, 2));
7369
7450
  return reranked;
7370
7451
  } catch (err) {
7371
7452
  console.error("[EXULU] Error reranking:", err);
7372
- import_fs.default.writeFileSync("reranked.json", JSON.stringify(err, null, 2));
7373
7453
  return [];
7374
7454
  }
7375
7455
  };
@@ -7383,7 +7463,7 @@ var init_resolve_reranker = __esm({
7383
7463
  init_supervisor();
7384
7464
  init_budget_service();
7385
7465
  init_tags();
7386
- import_fs = __toESM(require("fs"), 1);
7466
+ import_fs = require("fs");
7387
7467
  ResolveRerankerError = class extends Error {
7388
7468
  constructor(code, message) {
7389
7469
  super(message);
@@ -7984,9 +8064,21 @@ var init_memory_tool = __esm({
7984
8064
  case "longText":
7985
8065
  case "shortText":
7986
8066
  case "code":
7987
- case "enum":
7988
8067
  fields[field.name] = import_zod4.z.string().describe("The " + field.name + " of the item to create");
7989
8068
  break;
8069
+ case "enum":
8070
+ if (field.enumValues && field.enumValues.length > 0) {
8071
+ const enumValues = field.enumValues;
8072
+ fields[field.name] = import_zod4.z.preprocess(
8073
+ (v) => typeof v === "string" ? v.toUpperCase() : v,
8074
+ import_zod4.z.enum(enumValues)
8075
+ ).describe(
8076
+ "The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
8077
+ );
8078
+ } else {
8079
+ fields[field.name] = import_zod4.z.string().describe("The " + field.name + " of the item to create");
8080
+ }
8081
+ break;
7990
8082
  case "json":
7991
8083
  fields[field.name] = import_zod4.z.string({}).describe(
7992
8084
  "The " + field.name + " of the item to create, it should be a valid JSON string."
@@ -8012,6 +8104,9 @@ var init_memory_tool = __esm({
8012
8104
  break;
8013
8105
  }
8014
8106
  }
8107
+ fields["visibility"] = import_zod4.z.enum(["private", "public"]).optional().describe(
8108
+ "Whether this memory is private to the user or shared (public). Ask the user if unknown."
8109
+ );
8015
8110
  const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
8016
8111
  return new ExuluTool({
8017
8112
  id: toolName,
@@ -8021,14 +8116,36 @@ var init_memory_tool = __esm({
8021
8116
  type: "function",
8022
8117
  inputSchema: import_zod4.z.object(fields),
8023
8118
  config: [],
8024
- execute: async ({ name, description, surroundingContext, mode, information, exuluConfig, user }) => {
8119
+ execute: async (params) => {
8120
+ const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
8025
8121
  let result = { result: "" };
8122
+ if (!visibility) {
8123
+ return {
8124
+ 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.`
8125
+ };
8126
+ }
8026
8127
  try {
8128
+ const extraFields = {};
8129
+ for (const field of context.fields ?? []) {
8130
+ if (field.type === "enum" && field.enumValues && field.enumValues.length > 0) {
8131
+ const raw = params[field.name];
8132
+ if (raw !== void 0 && raw !== null && raw !== "") {
8133
+ const rawStr = String(raw);
8134
+ const canonical = field.enumValues.find(
8135
+ (v) => v.toUpperCase() === rawStr.toUpperCase()
8136
+ );
8137
+ if (canonical !== void 0) {
8138
+ extraFields[field.name] = canonical;
8139
+ }
8140
+ }
8141
+ }
8142
+ }
8027
8143
  const newItem = {
8028
8144
  name,
8029
8145
  description: "Description: " + description + "\n\nSurrounding Context: " + surroundingContext,
8030
8146
  information: "Information: " + information,
8031
- rights_mode: "public"
8147
+ rights_mode: visibility === "private" ? "private" : "public",
8148
+ ...extraFields
8032
8149
  };
8033
8150
  const { item: createdItem, job: createdJob } = await context.createItem(
8034
8151
  newItem,
@@ -8841,7 +8958,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
8841
8958
  contexts = [];
8842
8959
  }
8843
8960
  let sharedSessionSandbox;
8844
- if (sessionID && exuluConfig) {
8961
+ if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
8845
8962
  try {
8846
8963
  sharedSessionSandbox = await createSessionSandbox(
8847
8964
  sessionID,
@@ -10922,11 +11039,13 @@ __export(index_exports, {
10922
11039
  ExuluProvider: () => ExuluProvider,
10923
11040
  ExuluPython: () => ExuluPython,
10924
11041
  ExuluQueues: () => queues,
11042
+ ExuluReadApi: () => ExuluReadApi,
10925
11043
  ExuluReranker: () => ExuluReranker,
10926
11044
  ExuluTool: () => ExuluTool,
10927
11045
  ExuluTrajectoryRegistry: () => trajectoryRegistry,
10928
11046
  ExuluVariables: () => ExuluVariables,
10929
- defaultChunker: () => defaultChunker
11047
+ defaultChunker: () => defaultChunker,
11048
+ postgresClient: () => postgresClient
10930
11049
  });
10931
11050
  module.exports = __toCommonJS(index_exports);
10932
11051
  init_cjs_shims();
@@ -10945,6 +11064,73 @@ var redisServer = {
10945
11064
  username: process.env.REDIS_USER || ""
10946
11065
  };
10947
11066
 
11067
+ // ee/queues/redis-startup.ts
11068
+ init_cjs_shims();
11069
+ var REDIS_STARTUP_TIMEOUT_MS = 6e4;
11070
+ var WATCHDOG_INTERVAL_MS = 1e4;
11071
+ var ERROR_LOG_THROTTLE_MS = 3e4;
11072
+ var log = (line) => console.log(`[EXULU-REDIS] ${line}`);
11073
+ var warn = (line) => console.warn(`[EXULU-REDIS] ${line}`);
11074
+ var errorLog = (line) => console.error(`[EXULU-REDIS] ${line}`);
11075
+ var redisAddress = () => `${redisServer.host || "(unset)"}:${redisServer.port || "(unset)"}`;
11076
+ var describeError = (e) => {
11077
+ const any = e;
11078
+ const head = any?.message ? String(any.message).split("\n")[0] : void 0;
11079
+ if (any?.code) return head && head !== any.code ? `${any.code} (${head})` : `${any.code}`;
11080
+ return head ?? String(e);
11081
+ };
11082
+ function logRedisErrors(source, label) {
11083
+ let count = 0;
11084
+ let lastLoggedAt = 0;
11085
+ source.on("error", (err) => {
11086
+ count += 1;
11087
+ const now = Date.now();
11088
+ if (count === 1 || now - lastLoggedAt >= ERROR_LOG_THROTTLE_MS) {
11089
+ errorLog(`${label} connection error (${redisAddress()}): ${describeError(err)}${count > 1 ? ` (x${count})` : ""}`);
11090
+ lastLoggedAt = now;
11091
+ }
11092
+ });
11093
+ }
11094
+ async function guardRedisStartup(label, run, source) {
11095
+ const addr = redisAddress();
11096
+ log(`Connecting to Redis (${addr}) for ${label}\u2026`);
11097
+ const startedAt = Date.now();
11098
+ let lastError;
11099
+ const onError = (err) => {
11100
+ lastError = err;
11101
+ };
11102
+ source?.on("error", onError);
11103
+ const watchdog = setInterval(() => {
11104
+ const secs = Math.round((Date.now() - startedAt) / 1e3);
11105
+ warn(
11106
+ `\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)`
11107
+ );
11108
+ }, WATCHDOG_INTERVAL_MS);
11109
+ watchdog.unref?.();
11110
+ let timer2;
11111
+ const timeout = new Promise((_resolve, reject) => {
11112
+ timer2 = setTimeout(() => {
11113
+ reject(
11114
+ new Error(
11115
+ `[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}.`
11116
+ )
11117
+ );
11118
+ }, REDIS_STARTUP_TIMEOUT_MS);
11119
+ });
11120
+ const runPromise = Promise.resolve().then(run);
11121
+ runPromise.catch(() => {
11122
+ });
11123
+ try {
11124
+ const result = await Promise.race([runPromise, timeout]);
11125
+ log(`Redis ready; ${label} initialized (${addr}, ${((Date.now() - startedAt) / 1e3).toFixed(1)}s).`);
11126
+ return result;
11127
+ } finally {
11128
+ clearInterval(watchdog);
11129
+ if (timer2) clearTimeout(timer2);
11130
+ source?.off?.("error", onError);
11131
+ }
11132
+ }
11133
+
10948
11134
  // src/redis/client.ts
10949
11135
  var client = {};
10950
11136
  async function redisClient() {
@@ -10962,9 +11148,11 @@ async function redisClient() {
10962
11148
  client["exulu"] = (0, import_redis.createClient)({
10963
11149
  url
10964
11150
  });
10965
- await client["exulu"].connect();
11151
+ logRedisErrors(client["exulu"], "client");
11152
+ await guardRedisStartup("client", () => client["exulu"].connect().then(() => void 0), client["exulu"]);
10966
11153
  } catch (error) {
10967
11154
  console.error(`[EXULU] error connecting to redis:`, error);
11155
+ delete client["exulu"];
10968
11156
  return { client: null };
10969
11157
  }
10970
11158
  }
@@ -10989,7 +11177,8 @@ init_client();
10989
11177
  init_auth();
10990
11178
  var requestValidators = {
10991
11179
  authenticate: async (req) => {
10992
- const apikey = req.headers["exulu-api-key"] || null;
11180
+ const rawApiKey = req.headers["exulu-api-key"] || req.headers["x-api-key"];
11181
+ const apikey = rawApiKey?.replace(/^Bearer\s+/i, "") || null;
10993
11182
  const { db: db2 } = await postgresClient();
10994
11183
  let authtoken = null;
10995
11184
  if (typeof apikey !== "string") {
@@ -11082,7 +11271,7 @@ var requestValidators = {
11082
11271
  init_statistics();
11083
11272
  init_client();
11084
11273
  var import_express5 = __toESM(require("express"), 1);
11085
- var import_server4 = require("@apollo/server");
11274
+ var import_server5 = require("@apollo/server");
11086
11275
  var import_cors = __toESM(require("cors"), 1);
11087
11276
  var import_reflect_metadata = require("reflect-metadata");
11088
11277
 
@@ -11275,6 +11464,14 @@ var ExuluQueues = class {
11275
11464
  },
11276
11465
  telemetry: new import_bullmq_otel.BullMQOtel("simple-guide")
11277
11466
  });
11467
+ logRedisErrors(newQueue, `queue "${name}"`);
11468
+ try {
11469
+ await guardRedisStartup(`queue "${name}"`, () => newQueue.waitUntilReady(), newQueue);
11470
+ } catch (err) {
11471
+ void newQueue.close().catch(() => {
11472
+ });
11473
+ throw err;
11474
+ }
11278
11475
  await newQueue.setGlobalConcurrency(queueConcurrency);
11279
11476
  this.queues.push({
11280
11477
  queue: newQueue,
@@ -13436,6 +13633,8 @@ var createWorkers = async (providers, queues2, config, contexts, evals, tools, t
13436
13633
  },
13437
13634
  maxRetriesPerRequest: null
13438
13635
  });
13636
+ logRedisErrors(redisConnection, "worker");
13637
+ await guardRedisStartup("workers", () => redisConnection.ping().then(() => void 0), redisConnection);
13439
13638
  }
13440
13639
  const workers = queues2.map((queue) => {
13441
13640
  console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
@@ -14615,7 +14814,7 @@ var renderTranscript = (segments, speakers) => {
14615
14814
 
14616
14815
  // src/exulu/transcription/service.ts
14617
14816
  var TABLE2 = "transcription_jobs";
14618
- var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
14817
+ var log3 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
14619
14818
  var parseJsonField = (v) => {
14620
14819
  if (v == null) return null;
14621
14820
  if (typeof v === "string") {
@@ -14686,7 +14885,7 @@ var transcriptionService = {
14686
14885
  error: err.message,
14687
14886
  updatedAt: /* @__PURE__ */ new Date()
14688
14887
  }).returning("*");
14689
- log2(`Failed to dispatch job ${row.id}: ${err.message}`);
14888
+ log3(`Failed to dispatch job ${row.id}: ${err.message}`);
14690
14889
  return this._rowFromDb(failed);
14691
14890
  }
14692
14891
  },
@@ -14714,9 +14913,9 @@ var transcriptionService = {
14714
14913
  updatedAt: /* @__PURE__ */ new Date()
14715
14914
  });
14716
14915
  } else if (err instanceof TranscriptionServerUnavailable) {
14717
- log2(`Whisper server unreachable while polling ${row.id}; will retry`);
14916
+ log3(`Whisper server unreachable while polling ${row.id}; will retry`);
14718
14917
  } else {
14719
- log2(`Error polling job ${row.id}: ${err.message}`);
14918
+ log3(`Error polling job ${row.id}: ${err.message}`);
14720
14919
  }
14721
14920
  }
14722
14921
  }
@@ -14766,7 +14965,7 @@ var transcriptionService = {
14766
14965
  } catch (err) {
14767
14966
  const code = err.code;
14768
14967
  if (code !== "JOB_NOT_FOUND") {
14769
- log2(`Best-effort cancel of whisper job failed: ${err.message}`);
14968
+ log3(`Best-effort cancel of whisper job failed: ${err.message}`);
14770
14969
  }
14771
14970
  }
14772
14971
  }
@@ -14852,7 +15051,7 @@ var transcriptionService = {
14852
15051
  []
14853
15052
  );
14854
15053
  } catch (err) {
14855
- log2(`RBAC update failed for item ${itemId}: ${err.message}`);
15054
+ log3(`RBAC update failed for item ${itemId}: ${err.message}`);
14856
15055
  }
14857
15056
  }
14858
15057
  const projectId = input.project_id ?? row.project_id ?? null;
@@ -15149,7 +15348,7 @@ var durationFromSegments = (segments) => {
15149
15348
  // src/exulu/recall/service.ts
15150
15349
  var TABLE3 = "transcription_jobs";
15151
15350
  var DEFAULT_BOT_NAME = "Exulu Notetaker";
15152
- var log3 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
15351
+ var log4 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
15153
15352
  var parseJson = (v) => {
15154
15353
  if (v == null) return null;
15155
15354
  if (typeof v === "string") {
@@ -15246,7 +15445,7 @@ var recallService = {
15246
15445
  error: err.message,
15247
15446
  updatedAt: /* @__PURE__ */ new Date()
15248
15447
  }).returning("*");
15249
- log3(`createBot failed for job ${inserted.id}: ${err.message}`);
15448
+ log4(`createBot failed for job ${inserted.id}: ${err.message}`);
15250
15449
  return this._row(failed);
15251
15450
  }
15252
15451
  },
@@ -15260,7 +15459,7 @@ var recallService = {
15260
15459
  const name = event?.event ?? "";
15261
15460
  const job = await this._findJob(ids.botId, ids.recordingId, ids.transcriptId);
15262
15461
  if (!job) {
15263
- log3(`No job for event ${name} (bot=${ids.botId}); ignoring.`);
15462
+ log4(`No job for event ${name} (bot=${ids.botId}); ignoring.`);
15264
15463
  return;
15265
15464
  }
15266
15465
  if (name.startsWith("bot.")) {
@@ -15285,12 +15484,12 @@ var recallService = {
15285
15484
  await this._fail(job.id, ids.subCode || ids.code || "transcript failed");
15286
15485
  return;
15287
15486
  default:
15288
- log3(`Unhandled event ${name} for job ${job.id}`);
15487
+ log4(`Unhandled event ${name} for job ${job.id}`);
15289
15488
  }
15290
15489
  },
15291
15490
  async _onRecordingDone(jobId, recordingId) {
15292
15491
  if (!recordingId) {
15293
- log3(`recording.done for job ${jobId} had no recording id`);
15492
+ log4(`recording.done for job ${jobId} had no recording id`);
15294
15493
  return;
15295
15494
  }
15296
15495
  const { db: db2 } = await postgresClient();
@@ -15300,7 +15499,7 @@ var recallService = {
15300
15499
  updatedAt: /* @__PURE__ */ new Date()
15301
15500
  });
15302
15501
  if (!claimed) {
15303
- log3(`recording.done for job ${jobId} already handled; skipping.`);
15502
+ log4(`recording.done for job ${jobId} already handled; skipping.`);
15304
15503
  return;
15305
15504
  }
15306
15505
  try {
@@ -15320,7 +15519,7 @@ var recallService = {
15320
15519
  if (!dbRow) return;
15321
15520
  const job = this._row(dbRow);
15322
15521
  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.`);
15522
+ log4(`transcript.done for job ${jobId} already processed; skipping.`);
15324
15523
  return;
15325
15524
  }
15326
15525
  try {
@@ -15339,7 +15538,7 @@ var recallService = {
15339
15538
  const recDuration = recordingDurationSeconds(rec);
15340
15539
  if (recDuration != null) duration = recDuration;
15341
15540
  } catch (err) {
15342
- log3(`could not fetch recording duration for job ${jobId}: ${err.message}`);
15541
+ log4(`could not fetch recording duration for job ${jobId}: ${err.message}`);
15343
15542
  }
15344
15543
  }
15345
15544
  await this._update(jobId, {
@@ -15379,11 +15578,11 @@ var recallService = {
15379
15578
  const prompts = job.post_processing_prompts ?? [];
15380
15579
  if (prompts.length === 0) return [];
15381
15580
  if (job.post_processing_outputs && job.post_processing_outputs.length > 0) {
15382
- log3(`post-processing for job ${jobId} already ran; skipping.`);
15581
+ log4(`post-processing for job ${jobId} already ran; skipping.`);
15383
15582
  return job.post_processing_outputs;
15384
15583
  }
15385
15584
  if (!job.raw_segments || job.raw_segments.length === 0) {
15386
- log3(`post-processing for job ${jobId} skipped: no transcript.`);
15585
+ log4(`post-processing for job ${jobId} skipped: no transcript.`);
15387
15586
  return [];
15388
15587
  }
15389
15588
  const outputs = [];
@@ -15456,7 +15655,7 @@ ${transcriptText}`,
15456
15655
  ran_at: ranAt
15457
15656
  };
15458
15657
  } catch (err) {
15459
- log3(`post-processing prompt ${promptId} failed for job ${job.id}: ${err.message}`);
15658
+ log4(`post-processing prompt ${promptId} failed for job ${job.id}: ${err.message}`);
15460
15659
  return {
15461
15660
  prompt_id: promptId,
15462
15661
  agent_id: agentId,
@@ -19655,6 +19854,53 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
19655
19854
  return passed ? { ok: true } : { ok: false, reason: "signature mismatch" };
19656
19855
  };
19657
19856
 
19857
+ // src/exulu/shared-artifacts.ts
19858
+ init_cjs_shims();
19859
+ var import_bcryptjs4 = __toESM(require("bcryptjs"), 1);
19860
+ var normalizeS3Key = (key, bucket) => {
19861
+ const segments = key.split("/").filter((s, i) => !(i === 0 && s === "")).map((s) => decodeURIComponent(s));
19862
+ if (segments[0] === bucket) segments.shift();
19863
+ return segments.join("/");
19864
+ };
19865
+ var isHtmlKey = (key) => /\.html?$/i.test(key);
19866
+ var deriveFilename = (key) => {
19867
+ const base = key.split("/").pop() ?? key;
19868
+ return base.split("_EXULU_").pop() ?? base;
19869
+ };
19870
+ var slugifyShareName = (input) => deriveFilename(input).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
19871
+ var isExpired = (expiresAt, now) => {
19872
+ if (!expiresAt) return false;
19873
+ return new Date(expiresAt).getTime() <= now.getTime();
19874
+ };
19875
+ var validateCreateInput = (input, now) => {
19876
+ if (!input.s3key) return { ok: false, message: "s3key is required." };
19877
+ if (!input.name) return { ok: false, message: "name is required." };
19878
+ const mode = input.auth_mode;
19879
+ if (mode !== "public" && mode !== "password" && mode !== "regular") {
19880
+ return { ok: false, message: "auth_mode must be public, password, or regular." };
19881
+ }
19882
+ if (mode === "password" && !input.password) {
19883
+ return { ok: false, message: "A password is required for password mode." };
19884
+ }
19885
+ if (input.expires_at && Number.isNaN(new Date(input.expires_at).getTime())) {
19886
+ return { ok: false, message: "expires_at is not a valid date." };
19887
+ }
19888
+ if (input.expires_at && isExpired(input.expires_at, now)) {
19889
+ return { ok: false, message: "expires_at must be in the future." };
19890
+ }
19891
+ return { ok: true };
19892
+ };
19893
+ var hashSharePassword = (password) => import_bcryptjs4.default.hash(password, 10);
19894
+ var verifySharePassword = (password, hash) => import_bcryptjs4.default.compare(password, hash);
19895
+ var contentHeadersFor = (key, contentType, filename) => {
19896
+ if (isHtmlKey(key)) return { contentType: "text/html; charset=utf-8" };
19897
+ return {
19898
+ contentType: contentType || "application/octet-stream",
19899
+ disposition: `attachment; filename="${filename.replace(/"/g, "")}"`
19900
+ };
19901
+ };
19902
+ var getSharedArtifactByName = (db2, name) => db2("shared_artifacts").where({ name }).first();
19903
+
19658
19904
  // src/exulu/routes.ts
19659
19905
  var REQUEST_SIZE_LIMIT = "50mb";
19660
19906
  var getExuluVersionNumber = async () => {
@@ -19771,7 +20017,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
19771
20017
  config,
19772
20018
  evals
19773
20019
  );
19774
- const server = new import_server4.ApolloServer({
20020
+ const server = new import_server5.ApolloServer({
19775
20021
  cache: new import_utils5.InMemoryLRUCache(),
19776
20022
  schema,
19777
20023
  introspection: true
@@ -22689,6 +22935,126 @@ ${style.markdown}` : params.prompt;
22689
22935
  }
22690
22936
  res.status(204).send();
22691
22937
  });
22938
+ app.post("/shared-artifacts", async (req, res) => {
22939
+ const { db: db2 } = await postgresClient();
22940
+ const auth = await requestValidators.authenticate(req);
22941
+ if (!auth.user?.id) {
22942
+ res.status(401).json({ detail: "Authentication required." });
22943
+ return;
22944
+ }
22945
+ const now = /* @__PURE__ */ new Date();
22946
+ const valid = validateCreateInput(req.body, now);
22947
+ if (!valid.ok) {
22948
+ res.status(400).json({ detail: valid.message });
22949
+ return;
22950
+ }
22951
+ const bucket = config.fileUploads?.s3Bucket ?? "";
22952
+ const s3key = normalizeS3Key(req.body.s3key, bucket);
22953
+ const name = slugifyShareName(req.body.name);
22954
+ if (!name) {
22955
+ res.status(400).json({ detail: "name must contain url-safe characters." });
22956
+ return;
22957
+ }
22958
+ const existing = await getSharedArtifactByName(db2, name);
22959
+ if (existing) {
22960
+ res.status(409).json({ detail: "That share name is already taken." });
22961
+ return;
22962
+ }
22963
+ const auth_mode = req.body.auth_mode;
22964
+ const password_hash = auth_mode === "password" ? await hashSharePassword(req.body.password) : null;
22965
+ const rights_mode = auth_mode === "regular" ? req.body.rights_mode ?? "private" : "public";
22966
+ const [row] = await db2("shared_artifacts").insert({
22967
+ name,
22968
+ s3key,
22969
+ auth_mode,
22970
+ password_hash,
22971
+ expires_at: req.body.expires_at ?? null,
22972
+ content_type: req.body.content_type ?? null,
22973
+ rights_mode,
22974
+ created_by: auth.user.id
22975
+ }).returning("*");
22976
+ if (auth_mode === "regular" && req.body.rbac) {
22977
+ await handleRBACUpdate(db2, "shared_artifact", row.id, req.body.rbac, []);
22978
+ }
22979
+ res.status(201).json({ name: row.name });
22980
+ });
22981
+ app.get("/shared-artifacts/:name/meta", async (req, res) => {
22982
+ const { db: db2 } = await postgresClient();
22983
+ const row = await getSharedArtifactByName(db2, req.params.name ?? "");
22984
+ if (!row) {
22985
+ res.status(404).json({ detail: "Not found." });
22986
+ return;
22987
+ }
22988
+ if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
22989
+ res.status(410).json({ detail: "This link has expired." });
22990
+ return;
22991
+ }
22992
+ res.json({
22993
+ auth_mode: row.auth_mode,
22994
+ expires_at: row.expires_at,
22995
+ filename: deriveFilename(row.s3key),
22996
+ content_type: row.content_type,
22997
+ is_html: /\.html?$/i.test(row.s3key)
22998
+ });
22999
+ });
23000
+ app.get("/shared-artifacts/:name/content", async (req, res) => {
23001
+ const { db: db2 } = await postgresClient();
23002
+ const row = await getSharedArtifactByName(db2, req.params.name ?? "");
23003
+ if (!row) {
23004
+ res.status(404).json({ detail: "Not found." });
23005
+ return;
23006
+ }
23007
+ if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
23008
+ res.status(410).json({ detail: "This link has expired." });
23009
+ return;
23010
+ }
23011
+ if (row.auth_mode === "password") {
23012
+ const pw = req.headers["x-share-password"] || "";
23013
+ if (!row.password_hash || !await verifySharePassword(pw, row.password_hash)) {
23014
+ res.status(401).json({ detail: "Incorrect password." });
23015
+ return;
23016
+ }
23017
+ } else if (row.auth_mode === "regular") {
23018
+ const viewer = await requestValidators.authenticate(req);
23019
+ if (!viewer.user?.id) {
23020
+ res.status(401).json({ detail: "Authentication required." });
23021
+ return;
23022
+ }
23023
+ const rbac = await RBACResolver(
23024
+ db2,
23025
+ "shared_artifact",
23026
+ row.id,
23027
+ row.rights_mode || "private"
23028
+ );
23029
+ const ok = await checkRecordAccess({ ...row, RBAC: rbac }, "read", viewer.user);
23030
+ if (!ok) {
23031
+ res.status(403).json({ detail: "You don't have access to this artifact." });
23032
+ return;
23033
+ }
23034
+ }
23035
+ let bytes;
23036
+ try {
23037
+ bytes = await getS3ObjectBytes(row.s3key, config);
23038
+ } catch (e) {
23039
+ if (e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.$metadata?.httpStatusCode === 404) {
23040
+ res.status(404).json({ detail: "Artifact file not found." });
23041
+ return;
23042
+ }
23043
+ console.error("[EXULU] shared-artifact content read failed", e);
23044
+ res.status(500).json({ detail: "Failed to read artifact." });
23045
+ return;
23046
+ }
23047
+ const headers = contentHeadersFor(
23048
+ row.s3key,
23049
+ row.content_type,
23050
+ deriveFilename(row.s3key)
23051
+ );
23052
+ res.setHeader("Content-Type", headers.contentType);
23053
+ res.setHeader("X-Content-Type-Options", "nosniff");
23054
+ res.setHeader("Cache-Control", "private, no-store");
23055
+ if (headers.disposition) res.setHeader("Content-Disposition", headers.disposition);
23056
+ res.send(bytes);
23057
+ });
22692
23058
  app.use(import_express5.default.static("public"));
22693
23059
  await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config);
22694
23060
  return app;
@@ -26668,6 +27034,83 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
26668
27034
  // src/index.ts
26669
27035
  init_chunker();
26670
27036
  init_context();
27037
+
27038
+ // src/exulu/read-api.ts
27039
+ init_cjs_shims();
27040
+ init_table_names();
27041
+ init_types();
27042
+ init_client();
27043
+ init_access_control();
27044
+ init_convert_context_to_table_definition();
27045
+ init_resolve_embedder();
27046
+ var authorizedRead = async (context, user, role, opts = {}) => {
27047
+ if (!opts.itemIds?.length && !opts.externalIds?.length) {
27048
+ throw new Error("authorizedRead requires itemIds or externalIds to constrain the read.");
27049
+ }
27050
+ const { db: db2 } = await postgresClient();
27051
+ const table = convertContextToTableDefinition(context);
27052
+ const itemsTable = getTableName(context.id);
27053
+ const chunksTable = getChunksTableName(context.id);
27054
+ const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
27055
+ let q = db2(chunksTable + " as chunks").select([
27056
+ "chunks.id as chunk_id",
27057
+ "chunks.source as chunk_source",
27058
+ "chunks.content as chunk_content",
27059
+ "chunks.chunk_index",
27060
+ "chunks.metadata as chunk_metadata",
27061
+ db2.raw('chunks."createdAt" as chunk_created_at'),
27062
+ db2.raw('chunks."updatedAt" as chunk_updated_at'),
27063
+ "items.id as item_id",
27064
+ "items.name as item_name",
27065
+ "items.external_id as item_external_id",
27066
+ db2.raw('items."createdAt" as item_created_at'),
27067
+ db2.raw('items."updatedAt" as item_updated_at')
27068
+ ]);
27069
+ q = q.leftJoin(itemsTable + " as items", "chunks.source", "items.id");
27070
+ if (opts.itemIds?.length) q = q.whereIn("items.id", opts.itemIds);
27071
+ if (opts.externalIds?.length) q = q.whereIn("items.external_id", opts.externalIds);
27072
+ if (opts.chunkIndexRange) {
27073
+ const { from, to } = opts.chunkIndexRange;
27074
+ if (typeof from === "number") q = q.where("chunks.chunk_index", ">=", from);
27075
+ if (typeof to === "number") q = q.where("chunks.chunk_index", "<=", to);
27076
+ }
27077
+ q = applyAccessControl(table, q, acUser, "items");
27078
+ q = q.orderBy("chunks.source").orderBy("chunks.chunk_index");
27079
+ return await q;
27080
+ };
27081
+ var entitiesAvailable = async (context) => {
27082
+ if (!context.entities) return false;
27083
+ const { db: db2 } = await postgresClient();
27084
+ const table = getChunkEntitiesTableName(context.id);
27085
+ const res = await db2.raw(
27086
+ "SELECT to_regclass(?) IS NOT NULL AS exists",
27087
+ [table]
27088
+ );
27089
+ return res.rows?.[0]?.exists === true;
27090
+ };
27091
+ var embedQuery = async (context, text, opts = {}) => {
27092
+ const resolved = await resolveEmbedder({
27093
+ model: context.embedder.model,
27094
+ contextId: context.id,
27095
+ contextName: context.name,
27096
+ user: opts.user,
27097
+ roleId: opts.role
27098
+ });
27099
+ const [vector] = await resolved.embed([text], { inputType: opts.inputType ?? "query" });
27100
+ return vector ?? [];
27101
+ };
27102
+ var ExuluReadApi = {
27103
+ getTableName,
27104
+ getChunksTableName,
27105
+ getEntitiesTableName,
27106
+ getChunkEntitiesTableName,
27107
+ entitiesAvailable,
27108
+ authorizedRead,
27109
+ embedQuery
27110
+ };
27111
+
27112
+ // src/index.ts
27113
+ init_client();
26671
27114
  init_tool();
26672
27115
  init_sentence2();
26673
27116
 
@@ -26704,7 +27147,8 @@ var {
26704
27147
  promptFavoritesSchema: promptFavoritesSchema3,
26705
27148
  transcriptionJobsSchema: transcriptionJobsSchema3,
26706
27149
  imageGenerationsSchema: imageGenerationsSchema2,
26707
- oauthTokensSchema: oauthTokensSchema2
27150
+ oauthTokensSchema: oauthTokensSchema2,
27151
+ sharedArtifactsSchema: sharedArtifactsSchema2
26708
27152
  } = coreSchemas.get();
26709
27153
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
26710
27154
  for (const field of fields) {
@@ -26748,6 +27192,7 @@ var up = async function(knex) {
26748
27192
  transcriptionJobsSchema3(),
26749
27193
  imageGenerationsSchema2(),
26750
27194
  oauthTokensSchema2(),
27195
+ sharedArtifactsSchema2(),
26751
27196
  rbacSchema3(),
26752
27197
  agentsSchema3(),
26753
27198
  feedbackSchema3(),
@@ -27038,7 +27483,7 @@ var checkLiteLLMDatabaseSafety = (configPath) => {
27038
27483
 
27039
27484
  // src/exulu/litellm/db-init.ts
27040
27485
  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) => {
27486
+ var warn2 = (lines) => {
27042
27487
  console.warn(`
27043
27488
  ${WARNING_BANNER}`);
27044
27489
  console.warn("\u26A0 [EXULU-LITELLM] CONFIGURATION WARNING");
@@ -27046,13 +27491,13 @@ ${WARNING_BANNER}`);
27046
27491
  console.warn(`${WARNING_BANNER}
27047
27492
  `);
27048
27493
  };
27049
- var log4 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
27494
+ var log5 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
27050
27495
  var initLiteLLMDatabase = async (packageRoot) => {
27051
27496
  const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path9.resolve)(process.cwd(), "./config.litellm.yaml");
27052
27497
  const safety = checkLiteLLMDatabaseSafety(configPath);
27053
27498
  if (safety.ok && safety.reason === "no-litellm-db-mode") return;
27054
27499
  if (!safety.ok && safety.reason === "unparseable-url") {
27055
- warn([
27500
+ warn2([
27056
27501
  `LiteLLM's database_url is not a valid postgres URL:`,
27057
27502
  ` ${safety.rawUrl}`,
27058
27503
  `Expected postgres://user:pass@host:port/database. Skipping setup.`
@@ -27061,7 +27506,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27061
27506
  }
27062
27507
  if (!safety.ok && safety.reason === "shared-with-exulu") {
27063
27508
  const { exuluTarget } = safety;
27064
- warn([
27509
+ warn2([
27065
27510
  `LiteLLM's database_url points to the SAME database Exulu uses:`,
27066
27511
  ` ${exuluTarget.host}:${exuluTarget.port}/${exuluTarget.database}`,
27067
27512
  ``,
@@ -27080,7 +27525,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27080
27525
  return;
27081
27526
  }
27082
27527
  const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
27083
- log4(
27528
+ log5(
27084
27529
  `LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
27085
27530
  );
27086
27531
  const ensureDatabaseExists2 = async () => {
@@ -27092,7 +27537,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27092
27537
  } catch (err) {
27093
27538
  const code = err?.code;
27094
27539
  if (code !== "3D000") {
27095
- warn([
27540
+ warn2([
27096
27541
  `Could not connect to LiteLLM's target database:`,
27097
27542
  ` ${err instanceof Error ? err.message : String(err)}`,
27098
27543
  ``,
@@ -27104,13 +27549,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
27104
27549
  const url = new URL(litellmUrl);
27105
27550
  const targetDbName = url.pathname.replace(/^\//, "");
27106
27551
  if (!targetDbName) {
27107
- warn([`LiteLLM database_url has no database name; cannot auto-create.`]);
27552
+ warn2([`LiteLLM database_url has no database name; cannot auto-create.`]);
27108
27553
  return false;
27109
27554
  }
27110
27555
  url.pathname = "/postgres";
27111
- log4(`Target database "${targetDbName}" does not exist; creating it\u2026`);
27556
+ log5(`Target database "${targetDbName}" does not exist; creating it\u2026`);
27112
27557
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
27113
- warn([
27558
+ warn2([
27114
27559
  `Refusing to auto-create database "${targetDbName}" \u2014 name`,
27115
27560
  `contains characters that would require quoting. Create it`,
27116
27561
  `manually: createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`
@@ -27121,10 +27566,10 @@ var initLiteLLMDatabase = async (packageRoot) => {
27121
27566
  try {
27122
27567
  await admin.connect();
27123
27568
  await admin.query(`CREATE DATABASE "${targetDbName}"`);
27124
- log4(`\u2713 Created database "${targetDbName}".`);
27569
+ log5(`\u2713 Created database "${targetDbName}".`);
27125
27570
  return true;
27126
27571
  } catch (createErr) {
27127
- warn([
27572
+ warn2([
27128
27573
  `Failed to auto-create database "${targetDbName}":`,
27129
27574
  ` ${createErr instanceof Error ? createErr.message : String(createErr)}`,
27130
27575
  ``,
@@ -27143,7 +27588,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27143
27588
  }
27144
27589
  };
27145
27590
  if (!await ensureDatabaseExists2()) return;
27146
- log4("Checking that the target database is safe to push into\u2026");
27591
+ log5("Checking that the target database is safe to push into\u2026");
27147
27592
  const client2 = new import_pg.Client({ connectionString: litellmUrl });
27148
27593
  let foreignTables = [];
27149
27594
  try {
@@ -27159,7 +27604,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27159
27604
  );
27160
27605
  foreignTables = res.rows.map((r) => r.table_name);
27161
27606
  } catch (err) {
27162
- warn([
27607
+ warn2([
27163
27608
  `Could not query LiteLLM's target database to verify it is safe`,
27164
27609
  `to push into:`,
27165
27610
  ` ${err instanceof Error ? err.message : String(err)}`,
@@ -27175,7 +27620,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27175
27620
  }
27176
27621
  }
27177
27622
  if (foreignTables.length > 0) {
27178
- warn([
27623
+ warn2([
27179
27624
  `LiteLLM's target database contains ${foreignTables.length} table(s) that are NOT`,
27180
27625
  `part of LiteLLM's schema:`,
27181
27626
  ...foreignTables.slice(0, 10).map((t) => ` - ${t}`),
@@ -27191,7 +27636,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27191
27636
  const venvLibDir = (0, import_node_path9.resolve)(packageRoot, "ee/python/.venv/lib");
27192
27637
  const pythonVersionDir = (0, import_node_fs9.existsSync)(venvLibDir) ? (0, import_node_fs9.readdirSync)(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
27193
27638
  if (!pythonVersionDir) {
27194
- warn([
27639
+ warn2([
27195
27640
  `Could not find a python3.* directory under ${venvLibDir}.`,
27196
27641
  `Run \`npm run python:setup\` to create the venv.`,
27197
27642
  `Skipping LiteLLM database setup.`
@@ -27205,7 +27650,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27205
27650
  );
27206
27651
  const schemaPath = (0, import_node_path9.resolve)(litellmProxyDir, "schema.prisma");
27207
27652
  if (!(0, import_node_fs9.existsSync)(prismaCli)) {
27208
- warn([
27653
+ warn2([
27209
27654
  `Prisma CLI not found at ${prismaCli}.`,
27210
27655
  `Run \`npm run python:setup\` to create the venv and install prisma.`,
27211
27656
  `Skipping LiteLLM database setup.`
@@ -27213,13 +27658,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
27213
27658
  return;
27214
27659
  }
27215
27660
  if (!(0, import_node_fs9.existsSync)(schemaPath)) {
27216
- warn([
27661
+ warn2([
27217
27662
  `LiteLLM Prisma schema not found at ${schemaPath}.`,
27218
27663
  `Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
27219
27664
  ]);
27220
27665
  return;
27221
27666
  }
27222
- log4("Running `prisma db push` against LiteLLM's schema\u2026");
27667
+ log5("Running `prisma db push` against LiteLLM's schema\u2026");
27223
27668
  const result = (0, import_node_child_process5.spawnSync)(prismaCli, ["db", "push", "--skip-generate"], {
27224
27669
  cwd: litellmProxyDir,
27225
27670
  env: {
@@ -27233,14 +27678,14 @@ var initLiteLLMDatabase = async (packageRoot) => {
27233
27678
  encoding: "utf8"
27234
27679
  });
27235
27680
  if (result.error) {
27236
- warn([
27681
+ warn2([
27237
27682
  `Failed to launch prisma: ${result.error.message}`,
27238
27683
  `Skipping LiteLLM database setup.`
27239
27684
  ]);
27240
27685
  return;
27241
27686
  }
27242
27687
  if (result.status !== 0) {
27243
- warn([
27688
+ warn2([
27244
27689
  `prisma db push exited with status ${result.status}.`,
27245
27690
  `stdout:`,
27246
27691
  ...(result.stdout || "(empty)").split("\n").map((l) => ` ${l}`),
@@ -27249,7 +27694,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
27249
27694
  ]);
27250
27695
  return;
27251
27696
  }
27252
- log4("\u2713 LiteLLM database ready.");
27697
+ log5("\u2713 LiteLLM database ready.");
27253
27698
  };
27254
27699
 
27255
27700
  // src/postgres/init-litellm-db.ts
@@ -28532,14 +28977,37 @@ ${setupResult.output || ""}`);
28532
28977
  model: config.processor.model ?? "mistral-ocr",
28533
28978
  ...config.attribution
28534
28979
  });
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);
28980
+ const maxPagesPerChunk = config.processor.maxPagesPerChunk ?? 25;
28981
+ const chunksDir = path2.join(path2.dirname(paths.json), "ocr_chunks");
28982
+ const splitResult = await executePythonScript({
28983
+ scriptPath: "ee/python/documents/processing/split_pdf.py",
28984
+ args: [paths.source, chunksDir, "--chunk-size", String(maxPagesPerChunk)],
28985
+ timeout: 5 * 60 * 1e3
28986
+ });
28987
+ const pdfChunks = JSON.parse(splitResult.stdout);
28988
+ console.log(`[EXULU] PDF split into ${pdfChunks.length} chunk(s) for OCR (max ${maxPagesPerChunk} pages each)`);
28989
+ const chunkLimit = (0, import_p_limit.default)(3);
28990
+ const chunkResults = await Promise.all(
28991
+ pdfChunks.map(
28992
+ (chunk, i) => chunkLimit(async () => {
28993
+ await new Promise((resolve8) => setImmediate(resolve8));
28994
+ await new Promise((resolve8) => setTimeout(resolve8, Math.floor(Math.random() * 1e3) + 200));
28995
+ console.log(`[EXULU] OCR chunk ${i + 1}/${pdfChunks.length}: pages ${chunk.start_page}\u2013${chunk.end_page - 1}`);
28996
+ const chunkBuffer = await fs5.promises.readFile(chunk.path);
28997
+ const chunkBase64 = chunkBuffer.toString("base64");
28998
+ const chunkResponse = await withRetry(async () => {
28999
+ return await resolved.ocr({
29000
+ type: "document_url",
29001
+ document_url: "data:application/pdf;base64," + chunkBase64
29002
+ }, { includeImageBase64: false });
29003
+ }, 10);
29004
+ return { pages: chunkResponse.pages, offset: chunk.start_page };
29005
+ })
29006
+ )
29007
+ );
29008
+ const mergedPages = chunkResults.sort((a, b) => a.offset - b.offset).flatMap(
29009
+ ({ pages, offset }) => pages.map((p) => ({ ...p, index: p.index + offset }))
29010
+ );
28543
29011
  const parser = new import_liteparse.LiteParse();
28544
29012
  const screenshots = await parser.screenshot(paths.source, void 0);
28545
29013
  await fs5.promises.mkdir(paths.images, { recursive: true });
@@ -28553,7 +29021,7 @@ ${setupResult.output || ""}`);
28553
29021
  );
28554
29022
  screenshot.imagePath = path2.join(paths.images, `${screenshot.pageNum}.png`);
28555
29023
  }
28556
- json = ocrResponse.pages.map((page) => ({
29024
+ json = mergedPages.map((page) => ({
28557
29025
  page: page.index + 1,
28558
29026
  content: page.markdown,
28559
29027
  image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
@@ -28887,9 +29355,11 @@ var ExuluPython = {
28887
29355
  ExuluProvider,
28888
29356
  ExuluPython,
28889
29357
  ExuluQueues,
29358
+ ExuluReadApi,
28890
29359
  ExuluReranker,
28891
29360
  ExuluTool,
28892
29361
  ExuluTrajectoryRegistry,
28893
29362
  ExuluVariables,
28894
- defaultChunker
29363
+ defaultChunker,
29364
+ postgresClient
28895
29365
  });