@exulu/backend 3.6.0 → 3.7.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.
@@ -1713,7 +1713,7 @@ var ExuluTool = class _ExuluTool {
1713
1713
  if (!agent) {
1714
1714
  throw new Error("Agent not found.");
1715
1715
  }
1716
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-TZ2UKWR4.js");
1716
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-F3CJU6JN.js");
1717
1717
  const tools = await convertExuluToolsToAiSdkTools2(
1718
1718
  [this],
1719
1719
  [],
@@ -8183,5 +8183,6 @@ export {
8183
8183
  ExuluTool,
8184
8184
  resolveReranker,
8185
8185
  withRetry,
8186
+ microCallProviderOptions,
8186
8187
  createAgenticRetrievalTool
8187
8188
  };
@@ -2,7 +2,7 @@ import "dotenv/config";
2
2
  import {
3
3
  convertExuluToolsToAiSdkTools,
4
4
  hydrateVariables
5
- } from "./chunk-7AZH4ETH.js";
5
+ } from "./chunk-HA2ZSGOW.js";
6
6
  import "./chunk-4PDWNVNT.js";
7
7
  export {
8
8
  convertExuluToolsToAiSdkTools,
package/dist/index.cjs CHANGED
@@ -8892,10 +8892,12 @@ __export(index_exports, {
8892
8892
  ExuluDocumentProcessor: () => ExuluDocumentProcessor,
8893
8893
  ExuluEval: () => ExuluEval,
8894
8894
  ExuluJobs: () => ExuluJobs,
8895
+ ExuluModels: () => ExuluModels,
8895
8896
  ExuluOtel: () => ExuluOtel,
8896
8897
  ExuluPython: () => ExuluPython,
8897
8898
  ExuluQueues: () => queues,
8898
8899
  ExuluReadApi: () => ExuluReadApi,
8900
+ ExuluRecall: () => ExuluRecall,
8899
8901
  ExuluReranker: () => ExuluReranker,
8900
8902
  ExuluTool: () => ExuluTool,
8901
8903
  ExuluVariables: () => ExuluVariables,
@@ -13714,7 +13716,7 @@ var ExuluContext2 = class {
13714
13716
  }
13715
13717
  console.log("[EXULU] context configuration", this.configuration);
13716
13718
  let jobs = [];
13717
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
13719
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
13718
13720
  if (this.processor) {
13719
13721
  const processor = this.processor;
13720
13722
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -13736,12 +13738,12 @@ var ExuluContext2 = class {
13736
13738
  ...processorResult
13737
13739
  });
13738
13740
  if (processor.config?.generateEmbeddings) {
13739
- shouldGenerateEmbeddings = true;
13741
+ shouldGenerateEmbeddings2 = true;
13740
13742
  }
13741
13743
  }
13742
13744
  }
13743
13745
  }
13744
- if (shouldGenerateEmbeddings) {
13746
+ if (shouldGenerateEmbeddings2) {
13745
13747
  console.log("[EXULU] generating embeddings for item", results[0].id);
13746
13748
  const { job: embeddingsJob } = await this.embeddings.generate.one({
13747
13749
  item: {
@@ -13785,7 +13787,7 @@ var ExuluContext2 = class {
13785
13787
  }).returning("id");
13786
13788
  await mutation;
13787
13789
  let jobs = [];
13788
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
13790
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
13789
13791
  if (this.processor) {
13790
13792
  const processor = this.processor;
13791
13793
  if (processor && runProcessorOverwrite !== false && (runProcessorOverwrite || processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -13807,12 +13809,12 @@ var ExuluContext2 = class {
13807
13809
  ...processorResult
13808
13810
  });
13809
13811
  if (processor.config?.generateEmbeddings) {
13810
- shouldGenerateEmbeddings = true;
13812
+ shouldGenerateEmbeddings2 = true;
13811
13813
  }
13812
13814
  }
13813
13815
  }
13814
13816
  }
13815
- if (shouldGenerateEmbeddings) {
13817
+ if (shouldGenerateEmbeddings2) {
13816
13818
  const { job: embeddingsJob } = await this.embeddings.generate.one({
13817
13819
  item: record,
13818
13820
  // important we need to full record here with all fields for the embedder
@@ -15668,6 +15670,18 @@ var applyAgentGuestFieldTransforms = async (input) => {
15668
15670
  return input;
15669
15671
  };
15670
15672
 
15673
+ // src/graphql/mutations/should-generate-embeddings.ts
15674
+ init_cjs_shims();
15675
+ var shouldGenerateEmbeddings = ({
15676
+ calculateVectors,
15677
+ operation,
15678
+ override
15679
+ }) => {
15680
+ if (override !== void 0) return override;
15681
+ if (calculateVectors === "always") return true;
15682
+ return operation === "create" ? calculateVectors === "onInsert" : calculateVectors === "onUpdate";
15683
+ };
15684
+
15671
15685
  // src/graphql/mutations/index.ts
15672
15686
  var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
15673
15687
  var postprocessDeletion = async ({
@@ -15736,7 +15750,9 @@ var postprocessUpdate = async ({
15736
15750
  result,
15737
15751
  user,
15738
15752
  role,
15739
- config
15753
+ config,
15754
+ operation,
15755
+ generateEmbeddings
15740
15756
  }) => {
15741
15757
  if (!result) {
15742
15758
  return result;
@@ -15763,7 +15779,11 @@ var postprocessUpdate = async ({
15763
15779
  if (!context.embedder) {
15764
15780
  return result;
15765
15781
  }
15766
- if (context.embedder && (context.configuration.calculateVectors === "onUpdate" || context.configuration.calculateVectors === "always")) {
15782
+ if (shouldGenerateEmbeddings({
15783
+ calculateVectors: context.configuration.calculateVectors,
15784
+ operation,
15785
+ override: generateEmbeddings
15786
+ })) {
15767
15787
  const { db: db2 } = await postgresClient();
15768
15788
  console.log("[EXULU] Deleting chunks for item", result.id);
15769
15789
  const exists = await context.chunksTableExists();
@@ -16013,7 +16033,9 @@ function createMutations(table, contexts, tools, config) {
16013
16033
  result: results[0],
16014
16034
  user: context.user.id,
16015
16035
  role: context.user.role?.id,
16016
- config
16036
+ config,
16037
+ operation: "create",
16038
+ generateEmbeddings: args.generateEmbeddings
16017
16039
  });
16018
16040
  return {
16019
16041
  // Filter result to only include requested fields
@@ -16087,7 +16109,9 @@ function createMutations(table, contexts, tools, config) {
16087
16109
  result,
16088
16110
  user: context.user.id,
16089
16111
  role: context.user.role?.id,
16090
- config
16112
+ config,
16113
+ operation: "update",
16114
+ generateEmbeddings: args.generateEmbeddings
16091
16115
  });
16092
16116
  return {
16093
16117
  item: finalizeRequestedFields({
@@ -16151,7 +16175,9 @@ function createMutations(table, contexts, tools, config) {
16151
16175
  result,
16152
16176
  user: context.user.id,
16153
16177
  role: context.user.role?.id,
16154
- config
16178
+ config,
16179
+ operation: "update",
16180
+ generateEmbeddings: args.generateEmbeddings
16155
16181
  });
16156
16182
  return {
16157
16183
  item: finalizeRequestedFields({
@@ -16421,6 +16447,7 @@ function createMutations(table, contexts, tools, config) {
16421
16447
  };
16422
16448
  }
16423
16449
  query = applyFilters(query, args.where, table);
16450
+ query = applyAccessControl(table, query, context.user);
16424
16451
  if (args.limit) {
16425
16452
  query = query.limit(args.limit);
16426
16453
  }
@@ -19861,6 +19888,33 @@ var renderTranscript = (segments, speakers) => {
19861
19888
  return blocks.map((b) => `${b.speaker}: ${b.text}`).join("\n");
19862
19889
  };
19863
19890
 
19891
+ // src/exulu/transcription/build-transcript-item.ts
19892
+ init_cjs_shims();
19893
+ var buildTranscriptItemInput = ({
19894
+ row,
19895
+ title,
19896
+ speakers,
19897
+ transcriptText,
19898
+ rightsMode,
19899
+ isReSave
19900
+ }) => ({
19901
+ // Carrying the id on re-save makes context.createItem upsert in place.
19902
+ ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
19903
+ name: title ?? row.title ?? "Transcript",
19904
+ transcript_text: transcriptText,
19905
+ audio_s3key: row.audio_s3key,
19906
+ language: row.language ?? void 0,
19907
+ duration_seconds: row.duration_seconds ?? void 0,
19908
+ speakers,
19909
+ raw_segments: row.raw_segments,
19910
+ // Recall meeting-bot post-processing results (null for Whisper jobs).
19911
+ post_processing: row.post_processing_outputs ?? void 0,
19912
+ // Handle for the meeting video; null for Whisper uploads.
19913
+ recall_recording_id: row.recall_recording_id ?? void 0,
19914
+ rights_mode: rightsMode,
19915
+ created_by: row.created_by
19916
+ });
19917
+
19864
19918
  // src/exulu/transcription/service.ts
19865
19919
  var TABLE2 = "transcription_jobs";
19866
19920
  var log3 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
@@ -20053,21 +20107,14 @@ var transcriptionService = {
20053
20107
  const transcriptText = renderTranscript(row.raw_segments, input.speakers);
20054
20108
  const rightsMode = input.target_rights_mode ?? row.target_rights_mode ?? "private";
20055
20109
  const isReSave = row.status === "saved" && !!row.saved_item_id;
20056
- const itemInput = {
20057
- // Carrying the id on re-save makes context.createItem upsert in place.
20058
- ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
20059
- name: input.title ?? row.title ?? "Transcript",
20060
- transcript_text: transcriptText,
20061
- audio_s3key: row.audio_s3key,
20062
- language: row.language ?? void 0,
20063
- duration_seconds: row.duration_seconds ?? void 0,
20110
+ const itemInput = buildTranscriptItemInput({
20111
+ row,
20112
+ title: input.title,
20064
20113
  speakers: input.speakers,
20065
- raw_segments: row.raw_segments,
20066
- // Recall meeting-bot post-processing results (null for Whisper jobs).
20067
- post_processing: row.post_processing_outputs ?? void 0,
20068
- rights_mode: rightsMode,
20069
- created_by: row.created_by
20070
- };
20114
+ transcriptText,
20115
+ rightsMode,
20116
+ isReSave
20117
+ });
20071
20118
  let item;
20072
20119
  try {
20073
20120
  const result = await context.createItem(
@@ -20313,25 +20360,34 @@ var recordingDurationSeconds = (rec) => {
20313
20360
  }
20314
20361
  return null;
20315
20362
  };
20363
+ var RECORDING_RETENTION_HOURS = 2160;
20364
+ var buildCreateBotPayload = (input) => ({
20365
+ meeting_url: input.meeting_url,
20366
+ join_at: input.join_at,
20367
+ ...input.bot_name ? { bot_name: input.bot_name } : {},
20368
+ recording_config: {
20369
+ video_mixed_mp4: {},
20370
+ video_mixed_layout: "speaker_view",
20371
+ participant_events: {},
20372
+ meeting_metadata: {},
20373
+ retention: { type: "timed", hours: RECORDING_RETENTION_HOURS }
20374
+ },
20375
+ ...input.notifyChat ? {
20376
+ chat: {
20377
+ on_bot_join: {
20378
+ send_to: "everyone",
20379
+ message: input.notifyChat.message,
20380
+ pin: true
20381
+ }
20382
+ }
20383
+ } : {}
20384
+ });
20316
20385
  var recallClient = {
20317
20386
  isConfigured: () => !!recallApiKey() && !!process.env.RECALL_REGION,
20318
20387
  /** POST /bot — schedule/launch a bot for a meeting. */
20319
20388
  createBot: (input) => request2("/bot/", {
20320
20389
  method: "POST",
20321
- body: JSON.stringify({
20322
- meeting_url: input.meeting_url,
20323
- join_at: input.join_at,
20324
- ...input.bot_name ? { bot_name: input.bot_name } : {},
20325
- ...input.notifyChat ? {
20326
- chat: {
20327
- on_bot_join: {
20328
- send_to: "everyone",
20329
- message: input.notifyChat.message,
20330
- pin: true
20331
- }
20332
- }
20333
- } : {}
20334
- })
20390
+ body: JSON.stringify(buildCreateBotPayload(input))
20335
20391
  }),
20336
20392
  /**
20337
20393
  * POST /recording/{id}/create_transcript — start a post-meeting (async)
@@ -20647,6 +20703,28 @@ var recallService = {
20647
20703
  }
20648
20704
  return null;
20649
20705
  },
20706
+ /**
20707
+ * Fresh signed URL for a recording's mixed MP4, or null when there is no
20708
+ * usable video (still processing, no video artifact, recording expired or
20709
+ * deleted).
20710
+ *
20711
+ * Never cache the result: the URL carries X-Amz-Expires=21600, so it dies
20712
+ * after six hours. Resolve at point of use.
20713
+ */
20714
+ async getRecordingVideoUrl(recordingId) {
20715
+ try {
20716
+ const recording = await recallClient.retrieveRecording(recordingId);
20717
+ const video = recording?.media_shortcuts?.video_mixed;
20718
+ if (!video) return null;
20719
+ if (video.status?.code && video.status.code !== "done") return null;
20720
+ return video.data?.download_url ?? null;
20721
+ } catch (err) {
20722
+ log4(
20723
+ `could not resolve video url for recording ${recordingId}: ${err.message}`
20724
+ );
20725
+ return null;
20726
+ }
20727
+ },
20650
20728
  /**
20651
20729
  * Run the selected {prompt, agent} pairs against the transcript and store the
20652
20730
  * results on the row. Idempotent (skips if outputs already exist). Each entry
@@ -21542,11 +21620,11 @@ function createSDL(tables, contexts, tools, config, evals) {
21542
21620
  `;
21543
21621
  }
21544
21622
  mutationDefs += `
21545
- ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean): ${tableNameSingular}MutationPayload
21623
+ ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
21546
21624
  ${tableNamePlural}CopyOneById(id: ID!): ${tableNameSingular}MutationPayload
21547
21625
 
21548
- ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
21549
- ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
21626
+ ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
21627
+ ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
21550
21628
  ${tableNamePlural}RemoveOneById(id: ID!): ${tableNameSingular}
21551
21629
  ${tableNamePlural}RemoveOne(where: JSON!): ${tableNameSingular}
21552
21630
  `;
@@ -30252,7 +30330,11 @@ var transcriptionsContext = new ExuluContext2({
30252
30330
  // Post-processing results carried from the transcription job at finalize:
30253
30331
  // [{ prompt_id, agent_id, prompt_name, output, ran_at }]. Recall meeting
30254
30332
  // transcripts only for now.
30255
- { name: "post_processing", type: "json", editable: false }
30333
+ { name: "post_processing", type: "json", editable: false },
30334
+ // Link back to the Recall recording so the mixed video stays reachable
30335
+ // (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
30336
+ // after six hours). Null for Whisper uploads.
30337
+ { name: "recall_recording_id", type: "text" }
30256
30338
  ],
30257
30339
  sources: [],
30258
30340
  active: true,
@@ -31606,6 +31688,16 @@ init_supervisor();
31606
31688
  init_cjs_shims();
31607
31689
  init_client();
31608
31690
  init_sanitize_name();
31691
+
31692
+ // src/exulu/context-fields-for-sync.ts
31693
+ init_cjs_shims();
31694
+ var contextFieldsForSync = (context) => (context.fields ?? []).filter((field) => !!field?.name && !!field?.type).map((field) => ({
31695
+ ...field,
31696
+ name: field.type === "file" ? `${field.name}_s3key` : field.name
31697
+ }));
31698
+
31699
+ // src/postgres/init-exulu-db.ts
31700
+ init_table_names();
31609
31701
  var {
31610
31702
  agentsSchema: agentsSchema3,
31611
31703
  feedbackSchema: feedbackSchema3,
@@ -31652,8 +31744,9 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
31652
31744
  await knex.schema.alterTable(tableName, (table) => {
31653
31745
  mapType(table, type, sanitizedName, defaultValue, unique);
31654
31746
  });
31747
+ } else {
31748
+ console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
31655
31749
  }
31656
- console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
31657
31750
  }
31658
31751
  };
31659
31752
  var migrateUserCredentialsDataColumn = async (knex) => {
@@ -31837,11 +31930,18 @@ var up = async function(knex) {
31837
31930
  }
31838
31931
  };
31839
31932
  var contextDatabases = async (contexts) => {
31933
+ const { db: knex } = await postgresClient();
31840
31934
  for (const context of contexts) {
31841
31935
  const itemsTableExists = await context.tableExists();
31842
31936
  if (!itemsTableExists) {
31843
31937
  console.log("[EXULU] items table does not exist, creating it.");
31844
31938
  await context.createItemsTable();
31939
+ } else {
31940
+ await addMissingFields(
31941
+ knex,
31942
+ getTableName(context.id),
31943
+ contextFieldsForSync(context)
31944
+ );
31845
31945
  }
31846
31946
  const chunksTableExists = await context.chunksTableExists();
31847
31947
  if (!chunksTableExists && context.embedder) {
@@ -33722,8 +33822,58 @@ async function rerank(input) {
33722
33822
 
33723
33823
  // src/index.ts
33724
33824
  init_pipeline();
33825
+
33826
+ // src/exulu/models/public.ts
33827
+ init_cjs_shims();
33828
+ init_resolve_model();
33829
+ init_client();
33830
+ init_micro_call();
33831
+ var ExuluModels = {
33832
+ /**
33833
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
33834
+ *
33835
+ * Pass `userId` wherever one is known — it is what carries the caller's
33836
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
33837
+ * that user's team budget. Resolution still succeeds without it; the calls
33838
+ * are simply unattributed.
33839
+ */
33840
+ resolve: async ({
33841
+ modelId,
33842
+ userId
33843
+ }) => {
33844
+ let user;
33845
+ if (userId != null) {
33846
+ const { db: db2 } = await postgresClient();
33847
+ user = await db2("users").where({ id: userId }).first();
33848
+ }
33849
+ const { languageModel } = await resolveModel({ modelId, user: user || void 0 });
33850
+ return languageModel;
33851
+ },
33852
+ /**
33853
+ * Provider options for a constrained structured-output call.
33854
+ *
33855
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
33856
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
33857
+ * undefined for non-Gemini models.
33858
+ */
33859
+ providerOptions: (model) => microCallProviderOptions(model)
33860
+ };
33861
+
33862
+ // src/index.ts
33725
33863
  init_statistics();
33726
33864
  init_errors();
33865
+
33866
+ // src/exulu/recall/public.ts
33867
+ init_cjs_shims();
33868
+ var ExuluRecall = {
33869
+ /**
33870
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
33871
+ * of use — the URL expires after six hours.
33872
+ */
33873
+ getRecordingVideoUrl: (recordingId) => recallService.getRecordingVideoUrl(recordingId)
33874
+ };
33875
+
33876
+ // src/index.ts
33727
33877
  var ExuluJobs = {
33728
33878
  redis: redisClient
33729
33879
  };
@@ -33821,10 +33971,12 @@ var ExuluPython = {
33821
33971
  ExuluDocumentProcessor,
33822
33972
  ExuluEval,
33823
33973
  ExuluJobs,
33974
+ ExuluModels,
33824
33975
  ExuluOtel,
33825
33976
  ExuluPython,
33826
33977
  ExuluQueues,
33827
33978
  ExuluReadApi,
33979
+ ExuluRecall,
33828
33980
  ExuluReranker,
33829
33981
  ExuluTool,
33830
33982
  ExuluVariables,
package/dist/index.d.cts CHANGED
@@ -2506,6 +2506,37 @@ declare function createAgenticRetrievalTool(opts: {
2506
2506
  projectScope?: ProjectScope;
2507
2507
  }): ExuluTool | undefined;
2508
2508
 
2509
+ /**
2510
+ * Model access for consuming projects.
2511
+ *
2512
+ * Exists because ExuluApp exposes no model route and resolveModel is internal,
2513
+ * so a consumer's only alternative was building its own provider against
2514
+ * PROXY_BASE_URL — which produces untagged calls that per-team LiteLLM budgets
2515
+ * cannot attribute.
2516
+ */
2517
+ declare const ExuluModels: {
2518
+ /**
2519
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
2520
+ *
2521
+ * Pass `userId` wherever one is known — it is what carries the caller's
2522
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
2523
+ * that user's team budget. Resolution still succeeds without it; the calls
2524
+ * are simply unattributed.
2525
+ */
2526
+ resolve: ({ modelId, userId, }: {
2527
+ modelId: string;
2528
+ userId?: number;
2529
+ }) => Promise<LanguageModel>;
2530
+ /**
2531
+ * Provider options for a constrained structured-output call.
2532
+ *
2533
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
2534
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
2535
+ * undefined for non-Gemini models.
2536
+ */
2537
+ providerOptions: (model: LanguageModel | string) => Record<string, Record<string, string>> | undefined;
2538
+ };
2539
+
2509
2540
  type JOB_STATUS = "completed" | "failed" | "delayed" | "active" | "waiting" | "paused" | "stuck" | "waiting_approval" | "filtered" | "cancelled";
2510
2541
  declare const JOB_STATUS_ENUM: {
2511
2542
  completed: string;
@@ -2526,9 +2557,19 @@ declare class CredentialInvalidError extends Error {
2526
2557
  constructor(provider: string, reason?: string);
2527
2558
  }
2528
2559
 
2560
+ /** Public Recall surface for consuming projects. */
2561
+ declare const ExuluRecall: {
2562
+ /**
2563
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
2564
+ * of use — the URL expires after six hours.
2565
+ */
2566
+ getRecordingVideoUrl: (recordingId: string) => Promise<string | null>;
2567
+ };
2568
+
2529
2569
  declare const ExuluJobs: {
2530
2570
  redis: typeof redisClient;
2531
2571
  };
2572
+
2532
2573
  declare const ExuluDefaultTools: {
2533
2574
  agentic: {
2534
2575
  retrieval: {
@@ -2600,4 +2641,4 @@ declare const ExuluPython: {
2600
2641
  instructions: typeof getPythonSetupInstructions;
2601
2642
  };
2602
2643
 
2603
- export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
2644
+ export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, ExuluModels, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluRecall, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
package/dist/index.d.ts CHANGED
@@ -2506,6 +2506,37 @@ declare function createAgenticRetrievalTool(opts: {
2506
2506
  projectScope?: ProjectScope;
2507
2507
  }): ExuluTool | undefined;
2508
2508
 
2509
+ /**
2510
+ * Model access for consuming projects.
2511
+ *
2512
+ * Exists because ExuluApp exposes no model route and resolveModel is internal,
2513
+ * so a consumer's only alternative was building its own provider against
2514
+ * PROXY_BASE_URL — which produces untagged calls that per-team LiteLLM budgets
2515
+ * cannot attribute.
2516
+ */
2517
+ declare const ExuluModels: {
2518
+ /**
2519
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
2520
+ *
2521
+ * Pass `userId` wherever one is known — it is what carries the caller's
2522
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
2523
+ * that user's team budget. Resolution still succeeds without it; the calls
2524
+ * are simply unattributed.
2525
+ */
2526
+ resolve: ({ modelId, userId, }: {
2527
+ modelId: string;
2528
+ userId?: number;
2529
+ }) => Promise<LanguageModel>;
2530
+ /**
2531
+ * Provider options for a constrained structured-output call.
2532
+ *
2533
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
2534
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
2535
+ * undefined for non-Gemini models.
2536
+ */
2537
+ providerOptions: (model: LanguageModel | string) => Record<string, Record<string, string>> | undefined;
2538
+ };
2539
+
2509
2540
  type JOB_STATUS = "completed" | "failed" | "delayed" | "active" | "waiting" | "paused" | "stuck" | "waiting_approval" | "filtered" | "cancelled";
2510
2541
  declare const JOB_STATUS_ENUM: {
2511
2542
  completed: string;
@@ -2526,9 +2557,19 @@ declare class CredentialInvalidError extends Error {
2526
2557
  constructor(provider: string, reason?: string);
2527
2558
  }
2528
2559
 
2560
+ /** Public Recall surface for consuming projects. */
2561
+ declare const ExuluRecall: {
2562
+ /**
2563
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
2564
+ * of use — the URL expires after six hours.
2565
+ */
2566
+ getRecordingVideoUrl: (recordingId: string) => Promise<string | null>;
2567
+ };
2568
+
2529
2569
  declare const ExuluJobs: {
2530
2570
  redis: typeof redisClient;
2531
2571
  };
2572
+
2532
2573
  declare const ExuluDefaultTools: {
2533
2574
  agentic: {
2534
2575
  retrieval: {
@@ -2600,4 +2641,4 @@ declare const ExuluPython: {
2600
2641
  instructions: typeof getPythonSetupInstructions;
2601
2642
  };
2602
2643
 
2603
- export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
2644
+ export { type AuditConfig, type AuditEvent, type AuditLogger, type ChunkerOperation, type ChunkerResponse, type CredentialField, CredentialInvalidError, type JOB_STATUS as EXULU_JOB_STATUS, JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM, type STATISTICS_TYPE as EXULU_STATISTICS_TYPE, STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM, type ExuluAgent, ExuluApp, type ExuluAuthConfig, ExuluAuthentication, ExuluChunkers, ExuluContext, type ExuluContextEmbedder, type ExuluCredentialsToolContext, ExuluDatabase, ExuluDefaultTools, ExuluDocumentProcessor, ExuluEval, type Item as ExuluItem, ExuluJobs, ExuluModels, type ExuluOauthConfig, type ExuluOauthToolContext, ExuluOtel, ExuluPython, queues as ExuluQueues, ExuluReadApi, ExuluRecall, ExuluReranker, ExuluTool, type ExuluUserCredentialsConfig, ExuluVariables, type VectorSearchChunkResult, defaultChunker, enableLiteLLMClientMode, postgresClient };
package/dist/index.js CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  listS3ObjectsByPrefix,
67
67
  listTagsByPrefix,
68
68
  mapStreamErrorMessage,
69
+ microCallProviderOptions,
69
70
  parseResetAt,
70
71
  postgresClient,
71
72
  provisionDefaultUserBudget,
@@ -87,7 +88,7 @@ import {
87
88
  verifyCredentialNonce,
88
89
  waitForLiteLLMReady,
89
90
  withRetry
90
- } from "./chunk-7AZH4ETH.js";
91
+ } from "./chunk-HA2ZSGOW.js";
91
92
  import {
92
93
  LiteLLMAdminError,
93
94
  findLiteLLMModel,
@@ -4804,7 +4805,7 @@ var ExuluContext2 = class {
4804
4805
  }
4805
4806
  console.log("[EXULU] context configuration", this.configuration);
4806
4807
  let jobs = [];
4807
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
4808
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
4808
4809
  if (this.processor) {
4809
4810
  const processor = this.processor;
4810
4811
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -4826,12 +4827,12 @@ var ExuluContext2 = class {
4826
4827
  ...processorResult
4827
4828
  });
4828
4829
  if (processor.config?.generateEmbeddings) {
4829
- shouldGenerateEmbeddings = true;
4830
+ shouldGenerateEmbeddings2 = true;
4830
4831
  }
4831
4832
  }
4832
4833
  }
4833
4834
  }
4834
- if (shouldGenerateEmbeddings) {
4835
+ if (shouldGenerateEmbeddings2) {
4835
4836
  console.log("[EXULU] generating embeddings for item", results[0].id);
4836
4837
  const { job: embeddingsJob } = await this.embeddings.generate.one({
4837
4838
  item: {
@@ -4875,7 +4876,7 @@ var ExuluContext2 = class {
4875
4876
  }).returning("id");
4876
4877
  await mutation;
4877
4878
  let jobs = [];
4878
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
4879
+ let shouldGenerateEmbeddings2 = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
4879
4880
  if (this.processor) {
4880
4881
  const processor = this.processor;
4881
4882
  if (processor && runProcessorOverwrite !== false && (runProcessorOverwrite || processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -4897,12 +4898,12 @@ var ExuluContext2 = class {
4897
4898
  ...processorResult
4898
4899
  });
4899
4900
  if (processor.config?.generateEmbeddings) {
4900
- shouldGenerateEmbeddings = true;
4901
+ shouldGenerateEmbeddings2 = true;
4901
4902
  }
4902
4903
  }
4903
4904
  }
4904
4905
  }
4905
- if (shouldGenerateEmbeddings) {
4906
+ if (shouldGenerateEmbeddings2) {
4906
4907
  const { job: embeddingsJob } = await this.embeddings.generate.one({
4907
4908
  item: record,
4908
4909
  // important we need to full record here with all fields for the embedder
@@ -6707,6 +6708,17 @@ var applyAgentGuestFieldTransforms = async (input) => {
6707
6708
  return input;
6708
6709
  };
6709
6710
 
6711
+ // src/graphql/mutations/should-generate-embeddings.ts
6712
+ var shouldGenerateEmbeddings = ({
6713
+ calculateVectors,
6714
+ operation,
6715
+ override
6716
+ }) => {
6717
+ if (override !== void 0) return override;
6718
+ if (calculateVectors === "always") return true;
6719
+ return operation === "create" ? calculateVectors === "onInsert" : calculateVectors === "onUpdate";
6720
+ };
6721
+
6710
6722
  // src/graphql/mutations/index.ts
6711
6723
  var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
6712
6724
  var postprocessDeletion = async ({
@@ -6775,7 +6787,9 @@ var postprocessUpdate = async ({
6775
6787
  result,
6776
6788
  user,
6777
6789
  role,
6778
- config
6790
+ config,
6791
+ operation,
6792
+ generateEmbeddings
6779
6793
  }) => {
6780
6794
  if (!result) {
6781
6795
  return result;
@@ -6802,7 +6816,11 @@ var postprocessUpdate = async ({
6802
6816
  if (!context.embedder) {
6803
6817
  return result;
6804
6818
  }
6805
- if (context.embedder && (context.configuration.calculateVectors === "onUpdate" || context.configuration.calculateVectors === "always")) {
6819
+ if (shouldGenerateEmbeddings({
6820
+ calculateVectors: context.configuration.calculateVectors,
6821
+ operation,
6822
+ override: generateEmbeddings
6823
+ })) {
6806
6824
  const { db } = await postgresClient();
6807
6825
  console.log("[EXULU] Deleting chunks for item", result.id);
6808
6826
  const exists = await context.chunksTableExists();
@@ -7052,7 +7070,9 @@ function createMutations(table, contexts, tools, config) {
7052
7070
  result: results[0],
7053
7071
  user: context.user.id,
7054
7072
  role: context.user.role?.id,
7055
- config
7073
+ config,
7074
+ operation: "create",
7075
+ generateEmbeddings: args.generateEmbeddings
7056
7076
  });
7057
7077
  return {
7058
7078
  // Filter result to only include requested fields
@@ -7126,7 +7146,9 @@ function createMutations(table, contexts, tools, config) {
7126
7146
  result,
7127
7147
  user: context.user.id,
7128
7148
  role: context.user.role?.id,
7129
- config
7149
+ config,
7150
+ operation: "update",
7151
+ generateEmbeddings: args.generateEmbeddings
7130
7152
  });
7131
7153
  return {
7132
7154
  item: finalizeRequestedFields({
@@ -7190,7 +7212,9 @@ function createMutations(table, contexts, tools, config) {
7190
7212
  result,
7191
7213
  user: context.user.id,
7192
7214
  role: context.user.role?.id,
7193
- config
7215
+ config,
7216
+ operation: "update",
7217
+ generateEmbeddings: args.generateEmbeddings
7194
7218
  });
7195
7219
  return {
7196
7220
  item: finalizeRequestedFields({
@@ -7460,6 +7484,7 @@ function createMutations(table, contexts, tools, config) {
7460
7484
  };
7461
7485
  }
7462
7486
  query = applyFilters(query, args.where, table);
7487
+ query = applyAccessControl(table, query, context.user);
7463
7488
  if (args.limit) {
7464
7489
  query = query.limit(args.limit);
7465
7490
  }
@@ -10835,6 +10860,32 @@ var renderTranscript = (segments, speakers) => {
10835
10860
  return blocks.map((b) => `${b.speaker}: ${b.text}`).join("\n");
10836
10861
  };
10837
10862
 
10863
+ // src/exulu/transcription/build-transcript-item.ts
10864
+ var buildTranscriptItemInput = ({
10865
+ row,
10866
+ title,
10867
+ speakers,
10868
+ transcriptText,
10869
+ rightsMode,
10870
+ isReSave
10871
+ }) => ({
10872
+ // Carrying the id on re-save makes context.createItem upsert in place.
10873
+ ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
10874
+ name: title ?? row.title ?? "Transcript",
10875
+ transcript_text: transcriptText,
10876
+ audio_s3key: row.audio_s3key,
10877
+ language: row.language ?? void 0,
10878
+ duration_seconds: row.duration_seconds ?? void 0,
10879
+ speakers,
10880
+ raw_segments: row.raw_segments,
10881
+ // Recall meeting-bot post-processing results (null for Whisper jobs).
10882
+ post_processing: row.post_processing_outputs ?? void 0,
10883
+ // Handle for the meeting video; null for Whisper uploads.
10884
+ recall_recording_id: row.recall_recording_id ?? void 0,
10885
+ rights_mode: rightsMode,
10886
+ created_by: row.created_by
10887
+ });
10888
+
10838
10889
  // src/exulu/transcription/service.ts
10839
10890
  var TABLE = "transcription_jobs";
10840
10891
  var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
@@ -11027,21 +11078,14 @@ var transcriptionService = {
11027
11078
  const transcriptText = renderTranscript(row.raw_segments, input.speakers);
11028
11079
  const rightsMode = input.target_rights_mode ?? row.target_rights_mode ?? "private";
11029
11080
  const isReSave = row.status === "saved" && !!row.saved_item_id;
11030
- const itemInput = {
11031
- // Carrying the id on re-save makes context.createItem upsert in place.
11032
- ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
11033
- name: input.title ?? row.title ?? "Transcript",
11034
- transcript_text: transcriptText,
11035
- audio_s3key: row.audio_s3key,
11036
- language: row.language ?? void 0,
11037
- duration_seconds: row.duration_seconds ?? void 0,
11081
+ const itemInput = buildTranscriptItemInput({
11082
+ row,
11083
+ title: input.title,
11038
11084
  speakers: input.speakers,
11039
- raw_segments: row.raw_segments,
11040
- // Recall meeting-bot post-processing results (null for Whisper jobs).
11041
- post_processing: row.post_processing_outputs ?? void 0,
11042
- rights_mode: rightsMode,
11043
- created_by: row.created_by
11044
- };
11085
+ transcriptText,
11086
+ rightsMode,
11087
+ isReSave
11088
+ });
11045
11089
  let item;
11046
11090
  try {
11047
11091
  const result = await context.createItem(
@@ -11279,25 +11323,34 @@ var recordingDurationSeconds = (rec) => {
11279
11323
  }
11280
11324
  return null;
11281
11325
  };
11326
+ var RECORDING_RETENTION_HOURS = 2160;
11327
+ var buildCreateBotPayload = (input) => ({
11328
+ meeting_url: input.meeting_url,
11329
+ join_at: input.join_at,
11330
+ ...input.bot_name ? { bot_name: input.bot_name } : {},
11331
+ recording_config: {
11332
+ video_mixed_mp4: {},
11333
+ video_mixed_layout: "speaker_view",
11334
+ participant_events: {},
11335
+ meeting_metadata: {},
11336
+ retention: { type: "timed", hours: RECORDING_RETENTION_HOURS }
11337
+ },
11338
+ ...input.notifyChat ? {
11339
+ chat: {
11340
+ on_bot_join: {
11341
+ send_to: "everyone",
11342
+ message: input.notifyChat.message,
11343
+ pin: true
11344
+ }
11345
+ }
11346
+ } : {}
11347
+ });
11282
11348
  var recallClient = {
11283
11349
  isConfigured: () => !!recallApiKey() && !!process.env.RECALL_REGION,
11284
11350
  /** POST /bot — schedule/launch a bot for a meeting. */
11285
11351
  createBot: (input) => request2("/bot/", {
11286
11352
  method: "POST",
11287
- body: JSON.stringify({
11288
- meeting_url: input.meeting_url,
11289
- join_at: input.join_at,
11290
- ...input.bot_name ? { bot_name: input.bot_name } : {},
11291
- ...input.notifyChat ? {
11292
- chat: {
11293
- on_bot_join: {
11294
- send_to: "everyone",
11295
- message: input.notifyChat.message,
11296
- pin: true
11297
- }
11298
- }
11299
- } : {}
11300
- })
11353
+ body: JSON.stringify(buildCreateBotPayload(input))
11301
11354
  }),
11302
11355
  /**
11303
11356
  * POST /recording/{id}/create_transcript — start a post-meeting (async)
@@ -11612,6 +11665,28 @@ var recallService = {
11612
11665
  }
11613
11666
  return null;
11614
11667
  },
11668
+ /**
11669
+ * Fresh signed URL for a recording's mixed MP4, or null when there is no
11670
+ * usable video (still processing, no video artifact, recording expired or
11671
+ * deleted).
11672
+ *
11673
+ * Never cache the result: the URL carries X-Amz-Expires=21600, so it dies
11674
+ * after six hours. Resolve at point of use.
11675
+ */
11676
+ async getRecordingVideoUrl(recordingId) {
11677
+ try {
11678
+ const recording = await recallClient.retrieveRecording(recordingId);
11679
+ const video = recording?.media_shortcuts?.video_mixed;
11680
+ if (!video) return null;
11681
+ if (video.status?.code && video.status.code !== "done") return null;
11682
+ return video.data?.download_url ?? null;
11683
+ } catch (err) {
11684
+ log3(
11685
+ `could not resolve video url for recording ${recordingId}: ${err.message}`
11686
+ );
11687
+ return null;
11688
+ }
11689
+ },
11615
11690
  /**
11616
11691
  * Run the selected {prompt, agent} pairs against the transcript and store the
11617
11692
  * results on the row. Idempotent (skips if outputs already exist). Each entry
@@ -12499,11 +12574,11 @@ function createSDL(tables, contexts, tools, config, evals) {
12499
12574
  `;
12500
12575
  }
12501
12576
  mutationDefs += `
12502
- ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean): ${tableNameSingular}MutationPayload
12577
+ ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
12503
12578
  ${tableNamePlural}CopyOneById(id: ID!): ${tableNameSingular}MutationPayload
12504
12579
 
12505
- ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
12506
- ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
12580
+ ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
12581
+ ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
12507
12582
  ${tableNamePlural}RemoveOneById(id: ID!): ${tableNameSingular}
12508
12583
  ${tableNamePlural}RemoveOne(where: JSON!): ${tableNameSingular}
12509
12584
  `;
@@ -20890,7 +20965,11 @@ var transcriptionsContext = new ExuluContext2({
20890
20965
  // Post-processing results carried from the transcription job at finalize:
20891
20966
  // [{ prompt_id, agent_id, prompt_name, output, ran_at }]. Recall meeting
20892
20967
  // transcripts only for now.
20893
- { name: "post_processing", type: "json", editable: false }
20968
+ { name: "post_processing", type: "json", editable: false },
20969
+ // Link back to the Recall recording so the mixed video stays reachable
20970
+ // (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
20971
+ // after six hours). Null for Whisper uploads.
20972
+ { name: "recall_recording_id", type: "text" }
20894
20973
  ],
20895
20974
  sources: [],
20896
20975
  active: true,
@@ -22219,6 +22298,12 @@ var ExuluReadApi = {
22219
22298
  embedQuery
22220
22299
  };
22221
22300
 
22301
+ // src/exulu/context-fields-for-sync.ts
22302
+ var contextFieldsForSync = (context) => (context.fields ?? []).filter((field) => !!field?.name && !!field?.type).map((field) => ({
22303
+ ...field,
22304
+ name: field.type === "file" ? `${field.name}_s3key` : field.name
22305
+ }));
22306
+
22222
22307
  // src/postgres/init-exulu-db.ts
22223
22308
  var {
22224
22309
  agentsSchema: agentsSchema3,
@@ -22266,8 +22351,9 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
22266
22351
  await knex.schema.alterTable(tableName, (table) => {
22267
22352
  mapType(table, type, sanitizedName, defaultValue, unique);
22268
22353
  });
22354
+ } else {
22355
+ console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
22269
22356
  }
22270
- console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
22271
22357
  }
22272
22358
  };
22273
22359
  var migrateUserCredentialsDataColumn = async (knex) => {
@@ -22451,11 +22537,18 @@ var up = async function(knex) {
22451
22537
  }
22452
22538
  };
22453
22539
  var contextDatabases = async (contexts) => {
22540
+ const { db: knex } = await postgresClient();
22454
22541
  for (const context of contexts) {
22455
22542
  const itemsTableExists = await context.tableExists();
22456
22543
  if (!itemsTableExists) {
22457
22544
  console.log("[EXULU] items table does not exist, creating it.");
22458
22545
  await context.createItemsTable();
22546
+ } else {
22547
+ await addMissingFields(
22548
+ knex,
22549
+ getTableName(context.id),
22550
+ contextFieldsForSync(context)
22551
+ );
22459
22552
  }
22460
22553
  const chunksTableExists = await context.chunksTableExists();
22461
22554
  if (!chunksTableExists && context.embedder) {
@@ -24312,6 +24405,47 @@ async function rerank(input) {
24312
24405
  return resolved.rerank(query, items, { topN });
24313
24406
  }
24314
24407
 
24408
+ // src/exulu/models/public.ts
24409
+ var ExuluModels = {
24410
+ /**
24411
+ * A LanguageModel bound to the tenant's LiteLLM proxy.
24412
+ *
24413
+ * Pass `userId` wherever one is known — it is what carries the caller's
24414
+ * identity tags into LiteLLM, and therefore what makes the spend visible to
24415
+ * that user's team budget. Resolution still succeeds without it; the calls
24416
+ * are simply unattributed.
24417
+ */
24418
+ resolve: async ({
24419
+ modelId,
24420
+ userId
24421
+ }) => {
24422
+ let user;
24423
+ if (userId != null) {
24424
+ const { db } = await postgresClient();
24425
+ user = await db("users").where({ id: userId }).first();
24426
+ }
24427
+ const { languageModel } = await resolveModel({ modelId, user: user || void 0 });
24428
+ return languageModel;
24429
+ },
24430
+ /**
24431
+ * Provider options for a constrained structured-output call.
24432
+ *
24433
+ * Gemini 3+ counts thinking tokens against maxOutputTokens, so a call with a
24434
+ * token cap returns an empty 200 unless reasoning is disabled. Returns
24435
+ * undefined for non-Gemini models.
24436
+ */
24437
+ providerOptions: (model) => microCallProviderOptions(model)
24438
+ };
24439
+
24440
+ // src/exulu/recall/public.ts
24441
+ var ExuluRecall = {
24442
+ /**
24443
+ * Fresh signed URL for a recording's mixed MP4, or null. Resolve at point
24444
+ * of use — the URL expires after six hours.
24445
+ */
24446
+ getRecordingVideoUrl: (recordingId) => recallService.getRecordingVideoUrl(recordingId)
24447
+ };
24448
+
24315
24449
  // src/index.ts
24316
24450
  var ExuluJobs = {
24317
24451
  redis: redisClient
@@ -24409,10 +24543,12 @@ export {
24409
24543
  ExuluDocumentProcessor,
24410
24544
  ExuluEval,
24411
24545
  ExuluJobs,
24546
+ ExuluModels,
24412
24547
  ExuluOtel,
24413
24548
  ExuluPython,
24414
24549
  queues as ExuluQueues,
24415
24550
  ExuluReadApi,
24551
+ ExuluRecall,
24416
24552
  ExuluReranker,
24417
24553
  ExuluTool,
24418
24554
  ExuluVariables,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "3.6.0",
4
+ "version": "3.7.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {