@exulu/backend 3.5.1 → 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,
@@ -11452,6 +11454,52 @@ var usersSchema = {
11452
11454
  // triggered by the key with project_id_ for LiteLLM cost attribution.
11453
11455
  name: "project",
11454
11456
  type: "uuid"
11457
+ },
11458
+ // ── Lead capture (see lead-capture spec §5) ─────────────────────────────
11459
+ // Consent must be *provable* under Art. 7(1) GDPR: who agreed, when, and
11460
+ // to which wording. Hence a timestamp per consent and a version string
11461
+ // pointing at the exact text that was shown.
11462
+ {
11463
+ // Current state of the optional marketing consent.
11464
+ name: "marketing_consent",
11465
+ type: "boolean",
11466
+ default: false
11467
+ },
11468
+ {
11469
+ // When marketing consent was GIVEN.
11470
+ name: "marketing_consent_at",
11471
+ type: "date"
11472
+ },
11473
+ {
11474
+ // When marketing consent was WITHDRAWN. Deliberately a separate column:
11475
+ // overwriting marketing_consent_at on withdrawal would destroy the grant
11476
+ // time, and with it the ability to prove the order of events if someone
11477
+ // later claims they were mailed after withdrawing.
11478
+ name: "marketing_consent_withdrawn_at",
11479
+ type: "date"
11480
+ },
11481
+ {
11482
+ // When the mandatory storage/processing consent was given.
11483
+ name: "processing_consent_at",
11484
+ type: "date"
11485
+ },
11486
+ {
11487
+ // When the user asked for erasure (Art. 17). The row survives so the
11488
+ // one-month deadline stays queryable even if the notification mail fails.
11489
+ name: "erasure_requested_at",
11490
+ type: "date"
11491
+ },
11492
+ {
11493
+ // Where the lead came from, e.g. "eu-ai-act-bot".
11494
+ name: "signup_source",
11495
+ type: "text"
11496
+ },
11497
+ {
11498
+ // Version of the consent wording that was actually displayed, e.g.
11499
+ // "2026-08-18". The wording itself lives in the frontend repo under
11500
+ // version control; this column points at it.
11501
+ name: "consent_version",
11502
+ type: "text"
11455
11503
  }
11456
11504
  ]
11457
11505
  };
@@ -11760,6 +11808,32 @@ var addCoreFields = (schema) => {
11760
11808
  }
11761
11809
  return schema;
11762
11810
  };
11811
+ var otpSendAttemptsSchema = {
11812
+ type: "otp_send_attempts",
11813
+ name: {
11814
+ plural: "otp_send_attempts",
11815
+ singular: "otp_send_attempt"
11816
+ },
11817
+ fields: [
11818
+ {
11819
+ name: "key",
11820
+ type: "text",
11821
+ index: true,
11822
+ unique: true,
11823
+ required: true
11824
+ },
11825
+ {
11826
+ name: "count",
11827
+ type: "number",
11828
+ required: true
11829
+ },
11830
+ {
11831
+ name: "window_start",
11832
+ type: "date",
11833
+ required: true
11834
+ }
11835
+ ]
11836
+ };
11763
11837
  var coreSchemas = {
11764
11838
  get: () => {
11765
11839
  const license = checkLicense();
@@ -11780,7 +11854,8 @@ var coreSchemas = {
11780
11854
  contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
11781
11855
  sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
11782
11856
  transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
11783
- imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
11857
+ imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema),
11858
+ otpSendAttemptsSchema: () => addCoreFields(otpSendAttemptsSchema)
11784
11859
  };
11785
11860
  if (license["agent-feedback"]) {
11786
11861
  schemas.feedbackSchema = () => addCoreFields(feedbackSchema);
@@ -13641,7 +13716,7 @@ var ExuluContext2 = class {
13641
13716
  }
13642
13717
  console.log("[EXULU] context configuration", this.configuration);
13643
13718
  let jobs = [];
13644
- 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");
13645
13720
  if (this.processor) {
13646
13721
  const processor = this.processor;
13647
13722
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -13663,12 +13738,12 @@ var ExuluContext2 = class {
13663
13738
  ...processorResult
13664
13739
  });
13665
13740
  if (processor.config?.generateEmbeddings) {
13666
- shouldGenerateEmbeddings = true;
13741
+ shouldGenerateEmbeddings2 = true;
13667
13742
  }
13668
13743
  }
13669
13744
  }
13670
13745
  }
13671
- if (shouldGenerateEmbeddings) {
13746
+ if (shouldGenerateEmbeddings2) {
13672
13747
  console.log("[EXULU] generating embeddings for item", results[0].id);
13673
13748
  const { job: embeddingsJob } = await this.embeddings.generate.one({
13674
13749
  item: {
@@ -13712,7 +13787,7 @@ var ExuluContext2 = class {
13712
13787
  }).returning("id");
13713
13788
  await mutation;
13714
13789
  let jobs = [];
13715
- 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");
13716
13791
  if (this.processor) {
13717
13792
  const processor = this.processor;
13718
13793
  if (processor && runProcessorOverwrite !== false && (runProcessorOverwrite || processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -13734,12 +13809,12 @@ var ExuluContext2 = class {
13734
13809
  ...processorResult
13735
13810
  });
13736
13811
  if (processor.config?.generateEmbeddings) {
13737
- shouldGenerateEmbeddings = true;
13812
+ shouldGenerateEmbeddings2 = true;
13738
13813
  }
13739
13814
  }
13740
13815
  }
13741
13816
  }
13742
- if (shouldGenerateEmbeddings) {
13817
+ if (shouldGenerateEmbeddings2) {
13743
13818
  const { job: embeddingsJob } = await this.embeddings.generate.one({
13744
13819
  item: record,
13745
13820
  // important we need to full record here with all fields for the embedder
@@ -15595,6 +15670,18 @@ var applyAgentGuestFieldTransforms = async (input) => {
15595
15670
  return input;
15596
15671
  };
15597
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
+
15598
15685
  // src/graphql/mutations/index.ts
15599
15686
  var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
15600
15687
  var postprocessDeletion = async ({
@@ -15663,7 +15750,9 @@ var postprocessUpdate = async ({
15663
15750
  result,
15664
15751
  user,
15665
15752
  role,
15666
- config
15753
+ config,
15754
+ operation,
15755
+ generateEmbeddings
15667
15756
  }) => {
15668
15757
  if (!result) {
15669
15758
  return result;
@@ -15690,7 +15779,11 @@ var postprocessUpdate = async ({
15690
15779
  if (!context.embedder) {
15691
15780
  return result;
15692
15781
  }
15693
- 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
+ })) {
15694
15787
  const { db: db2 } = await postgresClient();
15695
15788
  console.log("[EXULU] Deleting chunks for item", result.id);
15696
15789
  const exists = await context.chunksTableExists();
@@ -15940,7 +16033,9 @@ function createMutations(table, contexts, tools, config) {
15940
16033
  result: results[0],
15941
16034
  user: context.user.id,
15942
16035
  role: context.user.role?.id,
15943
- config
16036
+ config,
16037
+ operation: "create",
16038
+ generateEmbeddings: args.generateEmbeddings
15944
16039
  });
15945
16040
  return {
15946
16041
  // Filter result to only include requested fields
@@ -16014,7 +16109,9 @@ function createMutations(table, contexts, tools, config) {
16014
16109
  result,
16015
16110
  user: context.user.id,
16016
16111
  role: context.user.role?.id,
16017
- config
16112
+ config,
16113
+ operation: "update",
16114
+ generateEmbeddings: args.generateEmbeddings
16018
16115
  });
16019
16116
  return {
16020
16117
  item: finalizeRequestedFields({
@@ -16078,7 +16175,9 @@ function createMutations(table, contexts, tools, config) {
16078
16175
  result,
16079
16176
  user: context.user.id,
16080
16177
  role: context.user.role?.id,
16081
- config
16178
+ config,
16179
+ operation: "update",
16180
+ generateEmbeddings: args.generateEmbeddings
16082
16181
  });
16083
16182
  return {
16084
16183
  item: finalizeRequestedFields({
@@ -16348,6 +16447,7 @@ function createMutations(table, contexts, tools, config) {
16348
16447
  };
16349
16448
  }
16350
16449
  query = applyFilters(query, args.where, table);
16450
+ query = applyAccessControl(table, query, context.user);
16351
16451
  if (args.limit) {
16352
16452
  query = query.limit(args.limit);
16353
16453
  }
@@ -19788,6 +19888,33 @@ var renderTranscript = (segments, speakers) => {
19788
19888
  return blocks.map((b) => `${b.speaker}: ${b.text}`).join("\n");
19789
19889
  };
19790
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
+
19791
19918
  // src/exulu/transcription/service.ts
19792
19919
  var TABLE2 = "transcription_jobs";
19793
19920
  var log3 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
@@ -19980,21 +20107,14 @@ var transcriptionService = {
19980
20107
  const transcriptText = renderTranscript(row.raw_segments, input.speakers);
19981
20108
  const rightsMode = input.target_rights_mode ?? row.target_rights_mode ?? "private";
19982
20109
  const isReSave = row.status === "saved" && !!row.saved_item_id;
19983
- const itemInput = {
19984
- // Carrying the id on re-save makes context.createItem upsert in place.
19985
- ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
19986
- name: input.title ?? row.title ?? "Transcript",
19987
- transcript_text: transcriptText,
19988
- audio_s3key: row.audio_s3key,
19989
- language: row.language ?? void 0,
19990
- duration_seconds: row.duration_seconds ?? void 0,
20110
+ const itemInput = buildTranscriptItemInput({
20111
+ row,
20112
+ title: input.title,
19991
20113
  speakers: input.speakers,
19992
- raw_segments: row.raw_segments,
19993
- // Recall meeting-bot post-processing results (null for Whisper jobs).
19994
- post_processing: row.post_processing_outputs ?? void 0,
19995
- rights_mode: rightsMode,
19996
- created_by: row.created_by
19997
- };
20114
+ transcriptText,
20115
+ rightsMode,
20116
+ isReSave
20117
+ });
19998
20118
  let item;
19999
20119
  try {
20000
20120
  const result = await context.createItem(
@@ -20240,25 +20360,34 @@ var recordingDurationSeconds = (rec) => {
20240
20360
  }
20241
20361
  return null;
20242
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
+ });
20243
20385
  var recallClient = {
20244
20386
  isConfigured: () => !!recallApiKey() && !!process.env.RECALL_REGION,
20245
20387
  /** POST /bot — schedule/launch a bot for a meeting. */
20246
20388
  createBot: (input) => request2("/bot/", {
20247
20389
  method: "POST",
20248
- body: JSON.stringify({
20249
- meeting_url: input.meeting_url,
20250
- join_at: input.join_at,
20251
- ...input.bot_name ? { bot_name: input.bot_name } : {},
20252
- ...input.notifyChat ? {
20253
- chat: {
20254
- on_bot_join: {
20255
- send_to: "everyone",
20256
- message: input.notifyChat.message,
20257
- pin: true
20258
- }
20259
- }
20260
- } : {}
20261
- })
20390
+ body: JSON.stringify(buildCreateBotPayload(input))
20262
20391
  }),
20263
20392
  /**
20264
20393
  * POST /recording/{id}/create_transcript — start a post-meeting (async)
@@ -20574,6 +20703,28 @@ var recallService = {
20574
20703
  }
20575
20704
  return null;
20576
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
+ },
20577
20728
  /**
20578
20729
  * Run the selected {prompt, agent} pairs against the transcript and store the
20579
20730
  * results on the row. Idempotent (skips if outputs already exist). Each entry
@@ -21469,11 +21620,11 @@ function createSDL(tables, contexts, tools, config, evals) {
21469
21620
  `;
21470
21621
  }
21471
21622
  mutationDefs += `
21472
- ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean): ${tableNameSingular}MutationPayload
21623
+ ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
21473
21624
  ${tableNamePlural}CopyOneById(id: ID!): ${tableNameSingular}MutationPayload
21474
21625
 
21475
- ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
21476
- ${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
21477
21628
  ${tableNamePlural}RemoveOneById(id: ID!): ${tableNameSingular}
21478
21629
  ${tableNamePlural}RemoveOne(where: JSON!): ${tableNameSingular}
21479
21630
  `;
@@ -30179,7 +30330,11 @@ var transcriptionsContext = new ExuluContext2({
30179
30330
  // Post-processing results carried from the transcription job at finalize:
30180
30331
  // [{ prompt_id, agent_id, prompt_name, output, ran_at }]. Recall meeting
30181
30332
  // transcripts only for now.
30182
- { 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" }
30183
30338
  ],
30184
30339
  sources: [],
30185
30340
  active: true,
@@ -31533,6 +31688,16 @@ init_supervisor();
31533
31688
  init_cjs_shims();
31534
31689
  init_client();
31535
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();
31536
31701
  var {
31537
31702
  agentsSchema: agentsSchema3,
31538
31703
  feedbackSchema: feedbackSchema3,
@@ -31560,7 +31725,8 @@ var {
31560
31725
  promptFavoritesSchema: promptFavoritesSchema3,
31561
31726
  transcriptionJobsSchema: transcriptionJobsSchema3,
31562
31727
  imageGenerationsSchema: imageGenerationsSchema2,
31563
- sharedArtifactsSchema: sharedArtifactsSchema2
31728
+ sharedArtifactsSchema: sharedArtifactsSchema2,
31729
+ otpSendAttemptsSchema: otpSendAttemptsSchema2
31564
31730
  } = coreSchemas.get();
31565
31731
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
31566
31732
  for (const field of fields) {
@@ -31578,8 +31744,9 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
31578
31744
  await knex.schema.alterTable(tableName, (table) => {
31579
31745
  mapType(table, type, sanitizedName, defaultValue, unique);
31580
31746
  });
31747
+ } else {
31748
+ console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
31581
31749
  }
31582
- console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
31583
31750
  }
31584
31751
  };
31585
31752
  var migrateUserCredentialsDataColumn = async (knex) => {
@@ -31631,7 +31798,8 @@ var up = async function(knex) {
31631
31798
  variablesSchema3(),
31632
31799
  skillsSchema3(),
31633
31800
  workflowTemplatesSchema3(),
31634
- workflowTriggersSchema2()
31801
+ workflowTriggersSchema2(),
31802
+ otpSendAttemptsSchema2()
31635
31803
  ];
31636
31804
  const createTable = async (schema) => {
31637
31805
  if (!await knex.schema.hasTable(schema.name.plural)) {
@@ -31762,11 +31930,18 @@ var up = async function(knex) {
31762
31930
  }
31763
31931
  };
31764
31932
  var contextDatabases = async (contexts) => {
31933
+ const { db: knex } = await postgresClient();
31765
31934
  for (const context of contexts) {
31766
31935
  const itemsTableExists = await context.tableExists();
31767
31936
  if (!itemsTableExists) {
31768
31937
  console.log("[EXULU] items table does not exist, creating it.");
31769
31938
  await context.createItemsTable();
31939
+ } else {
31940
+ await addMissingFields(
31941
+ knex,
31942
+ getTableName(context.id),
31943
+ contextFieldsForSync(context)
31944
+ );
31770
31945
  }
31771
31946
  const chunksTableExists = await context.chunksTableExists();
31772
31947
  if (!chunksTableExists && context.embedder) {
@@ -33647,8 +33822,58 @@ async function rerank(input) {
33647
33822
 
33648
33823
  // src/index.ts
33649
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
33650
33863
  init_statistics();
33651
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
33652
33877
  var ExuluJobs = {
33653
33878
  redis: redisClient
33654
33879
  };
@@ -33746,10 +33971,12 @@ var ExuluPython = {
33746
33971
  ExuluDocumentProcessor,
33747
33972
  ExuluEval,
33748
33973
  ExuluJobs,
33974
+ ExuluModels,
33749
33975
  ExuluOtel,
33750
33976
  ExuluPython,
33751
33977
  ExuluQueues,
33752
33978
  ExuluReadApi,
33979
+ ExuluRecall,
33753
33980
  ExuluReranker,
33754
33981
  ExuluTool,
33755
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,
@@ -2568,6 +2569,52 @@ var usersSchema = {
2568
2569
  // triggered by the key with project_id_ for LiteLLM cost attribution.
2569
2570
  name: "project",
2570
2571
  type: "uuid"
2572
+ },
2573
+ // ── Lead capture (see lead-capture spec §5) ─────────────────────────────
2574
+ // Consent must be *provable* under Art. 7(1) GDPR: who agreed, when, and
2575
+ // to which wording. Hence a timestamp per consent and a version string
2576
+ // pointing at the exact text that was shown.
2577
+ {
2578
+ // Current state of the optional marketing consent.
2579
+ name: "marketing_consent",
2580
+ type: "boolean",
2581
+ default: false
2582
+ },
2583
+ {
2584
+ // When marketing consent was GIVEN.
2585
+ name: "marketing_consent_at",
2586
+ type: "date"
2587
+ },
2588
+ {
2589
+ // When marketing consent was WITHDRAWN. Deliberately a separate column:
2590
+ // overwriting marketing_consent_at on withdrawal would destroy the grant
2591
+ // time, and with it the ability to prove the order of events if someone
2592
+ // later claims they were mailed after withdrawing.
2593
+ name: "marketing_consent_withdrawn_at",
2594
+ type: "date"
2595
+ },
2596
+ {
2597
+ // When the mandatory storage/processing consent was given.
2598
+ name: "processing_consent_at",
2599
+ type: "date"
2600
+ },
2601
+ {
2602
+ // When the user asked for erasure (Art. 17). The row survives so the
2603
+ // one-month deadline stays queryable even if the notification mail fails.
2604
+ name: "erasure_requested_at",
2605
+ type: "date"
2606
+ },
2607
+ {
2608
+ // Where the lead came from, e.g. "eu-ai-act-bot".
2609
+ name: "signup_source",
2610
+ type: "text"
2611
+ },
2612
+ {
2613
+ // Version of the consent wording that was actually displayed, e.g.
2614
+ // "2026-08-18". The wording itself lives in the frontend repo under
2615
+ // version control; this column points at it.
2616
+ name: "consent_version",
2617
+ type: "text"
2571
2618
  }
2572
2619
  ]
2573
2620
  };
@@ -2876,6 +2923,32 @@ var addCoreFields = (schema) => {
2876
2923
  }
2877
2924
  return schema;
2878
2925
  };
2926
+ var otpSendAttemptsSchema = {
2927
+ type: "otp_send_attempts",
2928
+ name: {
2929
+ plural: "otp_send_attempts",
2930
+ singular: "otp_send_attempt"
2931
+ },
2932
+ fields: [
2933
+ {
2934
+ name: "key",
2935
+ type: "text",
2936
+ index: true,
2937
+ unique: true,
2938
+ required: true
2939
+ },
2940
+ {
2941
+ name: "count",
2942
+ type: "number",
2943
+ required: true
2944
+ },
2945
+ {
2946
+ name: "window_start",
2947
+ type: "date",
2948
+ required: true
2949
+ }
2950
+ ]
2951
+ };
2879
2952
  var coreSchemas = {
2880
2953
  get: () => {
2881
2954
  const license = checkLicense();
@@ -2896,7 +2969,8 @@ var coreSchemas = {
2896
2969
  contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
2897
2970
  sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
2898
2971
  transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
2899
- imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
2972
+ imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema),
2973
+ otpSendAttemptsSchema: () => addCoreFields(otpSendAttemptsSchema)
2900
2974
  };
2901
2975
  if (license["agent-feedback"]) {
2902
2976
  schemas.feedbackSchema = () => addCoreFields(feedbackSchema);
@@ -4731,7 +4805,7 @@ var ExuluContext2 = class {
4731
4805
  }
4732
4806
  console.log("[EXULU] context configuration", this.configuration);
4733
4807
  let jobs = [];
4734
- 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");
4735
4809
  if (this.processor) {
4736
4810
  const processor = this.processor;
4737
4811
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -4753,12 +4827,12 @@ var ExuluContext2 = class {
4753
4827
  ...processorResult
4754
4828
  });
4755
4829
  if (processor.config?.generateEmbeddings) {
4756
- shouldGenerateEmbeddings = true;
4830
+ shouldGenerateEmbeddings2 = true;
4757
4831
  }
4758
4832
  }
4759
4833
  }
4760
4834
  }
4761
- if (shouldGenerateEmbeddings) {
4835
+ if (shouldGenerateEmbeddings2) {
4762
4836
  console.log("[EXULU] generating embeddings for item", results[0].id);
4763
4837
  const { job: embeddingsJob } = await this.embeddings.generate.one({
4764
4838
  item: {
@@ -4802,7 +4876,7 @@ var ExuluContext2 = class {
4802
4876
  }).returning("id");
4803
4877
  await mutation;
4804
4878
  let jobs = [];
4805
- 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");
4806
4880
  if (this.processor) {
4807
4881
  const processor = this.processor;
4808
4882
  if (processor && runProcessorOverwrite !== false && (runProcessorOverwrite || processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
@@ -4824,12 +4898,12 @@ var ExuluContext2 = class {
4824
4898
  ...processorResult
4825
4899
  });
4826
4900
  if (processor.config?.generateEmbeddings) {
4827
- shouldGenerateEmbeddings = true;
4901
+ shouldGenerateEmbeddings2 = true;
4828
4902
  }
4829
4903
  }
4830
4904
  }
4831
4905
  }
4832
- if (shouldGenerateEmbeddings) {
4906
+ if (shouldGenerateEmbeddings2) {
4833
4907
  const { job: embeddingsJob } = await this.embeddings.generate.one({
4834
4908
  item: record,
4835
4909
  // important we need to full record here with all fields for the embedder
@@ -6634,6 +6708,17 @@ var applyAgentGuestFieldTransforms = async (input) => {
6634
6708
  return input;
6635
6709
  };
6636
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
+
6637
6722
  // src/graphql/mutations/index.ts
6638
6723
  var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
6639
6724
  var postprocessDeletion = async ({
@@ -6702,7 +6787,9 @@ var postprocessUpdate = async ({
6702
6787
  result,
6703
6788
  user,
6704
6789
  role,
6705
- config
6790
+ config,
6791
+ operation,
6792
+ generateEmbeddings
6706
6793
  }) => {
6707
6794
  if (!result) {
6708
6795
  return result;
@@ -6729,7 +6816,11 @@ var postprocessUpdate = async ({
6729
6816
  if (!context.embedder) {
6730
6817
  return result;
6731
6818
  }
6732
- 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
+ })) {
6733
6824
  const { db } = await postgresClient();
6734
6825
  console.log("[EXULU] Deleting chunks for item", result.id);
6735
6826
  const exists = await context.chunksTableExists();
@@ -6979,7 +7070,9 @@ function createMutations(table, contexts, tools, config) {
6979
7070
  result: results[0],
6980
7071
  user: context.user.id,
6981
7072
  role: context.user.role?.id,
6982
- config
7073
+ config,
7074
+ operation: "create",
7075
+ generateEmbeddings: args.generateEmbeddings
6983
7076
  });
6984
7077
  return {
6985
7078
  // Filter result to only include requested fields
@@ -7053,7 +7146,9 @@ function createMutations(table, contexts, tools, config) {
7053
7146
  result,
7054
7147
  user: context.user.id,
7055
7148
  role: context.user.role?.id,
7056
- config
7149
+ config,
7150
+ operation: "update",
7151
+ generateEmbeddings: args.generateEmbeddings
7057
7152
  });
7058
7153
  return {
7059
7154
  item: finalizeRequestedFields({
@@ -7117,7 +7212,9 @@ function createMutations(table, contexts, tools, config) {
7117
7212
  result,
7118
7213
  user: context.user.id,
7119
7214
  role: context.user.role?.id,
7120
- config
7215
+ config,
7216
+ operation: "update",
7217
+ generateEmbeddings: args.generateEmbeddings
7121
7218
  });
7122
7219
  return {
7123
7220
  item: finalizeRequestedFields({
@@ -7387,6 +7484,7 @@ function createMutations(table, contexts, tools, config) {
7387
7484
  };
7388
7485
  }
7389
7486
  query = applyFilters(query, args.where, table);
7487
+ query = applyAccessControl(table, query, context.user);
7390
7488
  if (args.limit) {
7391
7489
  query = query.limit(args.limit);
7392
7490
  }
@@ -10762,6 +10860,32 @@ var renderTranscript = (segments, speakers) => {
10762
10860
  return blocks.map((b) => `${b.speaker}: ${b.text}`).join("\n");
10763
10861
  };
10764
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
+
10765
10889
  // src/exulu/transcription/service.ts
10766
10890
  var TABLE = "transcription_jobs";
10767
10891
  var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
@@ -10954,21 +11078,14 @@ var transcriptionService = {
10954
11078
  const transcriptText = renderTranscript(row.raw_segments, input.speakers);
10955
11079
  const rightsMode = input.target_rights_mode ?? row.target_rights_mode ?? "private";
10956
11080
  const isReSave = row.status === "saved" && !!row.saved_item_id;
10957
- const itemInput = {
10958
- // Carrying the id on re-save makes context.createItem upsert in place.
10959
- ...isReSave && row.saved_item_id ? { id: row.saved_item_id } : {},
10960
- name: input.title ?? row.title ?? "Transcript",
10961
- transcript_text: transcriptText,
10962
- audio_s3key: row.audio_s3key,
10963
- language: row.language ?? void 0,
10964
- duration_seconds: row.duration_seconds ?? void 0,
11081
+ const itemInput = buildTranscriptItemInput({
11082
+ row,
11083
+ title: input.title,
10965
11084
  speakers: input.speakers,
10966
- raw_segments: row.raw_segments,
10967
- // Recall meeting-bot post-processing results (null for Whisper jobs).
10968
- post_processing: row.post_processing_outputs ?? void 0,
10969
- rights_mode: rightsMode,
10970
- created_by: row.created_by
10971
- };
11085
+ transcriptText,
11086
+ rightsMode,
11087
+ isReSave
11088
+ });
10972
11089
  let item;
10973
11090
  try {
10974
11091
  const result = await context.createItem(
@@ -11206,25 +11323,34 @@ var recordingDurationSeconds = (rec) => {
11206
11323
  }
11207
11324
  return null;
11208
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
+ });
11209
11348
  var recallClient = {
11210
11349
  isConfigured: () => !!recallApiKey() && !!process.env.RECALL_REGION,
11211
11350
  /** POST /bot — schedule/launch a bot for a meeting. */
11212
11351
  createBot: (input) => request2("/bot/", {
11213
11352
  method: "POST",
11214
- body: JSON.stringify({
11215
- meeting_url: input.meeting_url,
11216
- join_at: input.join_at,
11217
- ...input.bot_name ? { bot_name: input.bot_name } : {},
11218
- ...input.notifyChat ? {
11219
- chat: {
11220
- on_bot_join: {
11221
- send_to: "everyone",
11222
- message: input.notifyChat.message,
11223
- pin: true
11224
- }
11225
- }
11226
- } : {}
11227
- })
11353
+ body: JSON.stringify(buildCreateBotPayload(input))
11228
11354
  }),
11229
11355
  /**
11230
11356
  * POST /recording/{id}/create_transcript — start a post-meeting (async)
@@ -11539,6 +11665,28 @@ var recallService = {
11539
11665
  }
11540
11666
  return null;
11541
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
+ },
11542
11690
  /**
11543
11691
  * Run the selected {prompt, agent} pairs against the transcript and store the
11544
11692
  * results on the row. Idempotent (skips if outputs already exist). Each entry
@@ -12426,11 +12574,11 @@ function createSDL(tables, contexts, tools, config, evals) {
12426
12574
  `;
12427
12575
  }
12428
12576
  mutationDefs += `
12429
- ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean): ${tableNameSingular}MutationPayload
12577
+ ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!, upsert: Boolean, generateEmbeddings: Boolean): ${tableNameSingular}MutationPayload
12430
12578
  ${tableNamePlural}CopyOneById(id: ID!): ${tableNameSingular}MutationPayload
12431
12579
 
12432
- ${tableNamePlural}UpdateOne(where: [Filter${tableNameSingularUpperCaseFirst}], input: ${tableNameSingular}Input!): ${tableNameSingular}MutationPayload
12433
- ${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
12434
12582
  ${tableNamePlural}RemoveOneById(id: ID!): ${tableNameSingular}
12435
12583
  ${tableNamePlural}RemoveOne(where: JSON!): ${tableNameSingular}
12436
12584
  `;
@@ -20817,7 +20965,11 @@ var transcriptionsContext = new ExuluContext2({
20817
20965
  // Post-processing results carried from the transcription job at finalize:
20818
20966
  // [{ prompt_id, agent_id, prompt_name, output, ran_at }]. Recall meeting
20819
20967
  // transcripts only for now.
20820
- { 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" }
20821
20973
  ],
20822
20974
  sources: [],
20823
20975
  active: true,
@@ -22146,6 +22298,12 @@ var ExuluReadApi = {
22146
22298
  embedQuery
22147
22299
  };
22148
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
+
22149
22307
  // src/postgres/init-exulu-db.ts
22150
22308
  var {
22151
22309
  agentsSchema: agentsSchema3,
@@ -22174,7 +22332,8 @@ var {
22174
22332
  promptFavoritesSchema: promptFavoritesSchema3,
22175
22333
  transcriptionJobsSchema: transcriptionJobsSchema3,
22176
22334
  imageGenerationsSchema: imageGenerationsSchema2,
22177
- sharedArtifactsSchema: sharedArtifactsSchema2
22335
+ sharedArtifactsSchema: sharedArtifactsSchema2,
22336
+ otpSendAttemptsSchema: otpSendAttemptsSchema2
22178
22337
  } = coreSchemas.get();
22179
22338
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
22180
22339
  for (const field of fields) {
@@ -22192,8 +22351,9 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
22192
22351
  await knex.schema.alterTable(tableName, (table) => {
22193
22352
  mapType(table, type, sanitizedName, defaultValue, unique);
22194
22353
  });
22354
+ } else {
22355
+ console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
22195
22356
  }
22196
- console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
22197
22357
  }
22198
22358
  };
22199
22359
  var migrateUserCredentialsDataColumn = async (knex) => {
@@ -22245,7 +22405,8 @@ var up = async function(knex) {
22245
22405
  variablesSchema3(),
22246
22406
  skillsSchema3(),
22247
22407
  workflowTemplatesSchema3(),
22248
- workflowTriggersSchema2()
22408
+ workflowTriggersSchema2(),
22409
+ otpSendAttemptsSchema2()
22249
22410
  ];
22250
22411
  const createTable = async (schema) => {
22251
22412
  if (!await knex.schema.hasTable(schema.name.plural)) {
@@ -22376,11 +22537,18 @@ var up = async function(knex) {
22376
22537
  }
22377
22538
  };
22378
22539
  var contextDatabases = async (contexts) => {
22540
+ const { db: knex } = await postgresClient();
22379
22541
  for (const context of contexts) {
22380
22542
  const itemsTableExists = await context.tableExists();
22381
22543
  if (!itemsTableExists) {
22382
22544
  console.log("[EXULU] items table does not exist, creating it.");
22383
22545
  await context.createItemsTable();
22546
+ } else {
22547
+ await addMissingFields(
22548
+ knex,
22549
+ getTableName(context.id),
22550
+ contextFieldsForSync(context)
22551
+ );
22384
22552
  }
22385
22553
  const chunksTableExists = await context.chunksTableExists();
22386
22554
  if (!chunksTableExists && context.embedder) {
@@ -24237,6 +24405,47 @@ async function rerank(input) {
24237
24405
  return resolved.rerank(query, items, { topN });
24238
24406
  }
24239
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
+
24240
24449
  // src/index.ts
24241
24450
  var ExuluJobs = {
24242
24451
  redis: redisClient
@@ -24334,10 +24543,12 @@ export {
24334
24543
  ExuluDocumentProcessor,
24335
24544
  ExuluEval,
24336
24545
  ExuluJobs,
24546
+ ExuluModels,
24337
24547
  ExuluOtel,
24338
24548
  ExuluPython,
24339
24549
  queues as ExuluQueues,
24340
24550
  ExuluReadApi,
24551
+ ExuluRecall,
24341
24552
  ExuluReranker,
24342
24553
  ExuluTool,
24343
24554
  ExuluVariables,
@@ -24,17 +24,26 @@ pyannote.audio>=3.3.0
24
24
  # Belt-and-suspenders: keep huggingface_hub on the 0.x line so pyannote 3.x's
25
25
  # `use_auth_token=` calls keep working (1.x removed that kwarg → diarization off).
26
26
  huggingface_hub<1.0
27
- fastapi
28
27
  uvicorn
29
28
  python-multipart
30
29
  requests
31
30
  # LiteLLM proxy — only used when EXULU_USE_LITELLM=true. Always installed so
32
31
  # the dep is ready when the env var is flipped. Pinned to a tested version;
33
32
  # upgrade deliberately.
34
- litellm[proxy]==1.85.1
33
+ litellm[proxy]==1.97.0
34
+ # FastAPI ceiling — NOT cosmetic. litellm 1.97.0 declares fastapi>=0.136.3,<1.0
35
+ # but imports the private `get_flat_dependant` from fastapi.dependencies.utils
36
+ # (proxy/management_endpoints/management_v1/common.py:7), which FastAPI deleted
37
+ # in 0.140.7. Any resolve that picks 0.140.7+ yields a proxy that cannot start:
38
+ # "ImportError: cannot import name 'get_flat_dependant'". 0.140.6 is the last
39
+ # version that still has it. NOTE: fastapi is also a first-party dependency of
40
+ # ee/python/transcription/server.py (not just a litellm transitive). Once
41
+ # litellm ships its own ceiling, replace this constrained pin with a bare
42
+ # `fastapi` line — do not delete it entirely.
43
+ fastapi>=0.136.3,<0.140.7 # to confirm this pin is still needed: grep -r get_flat_dependant $(pip show litellm | awk '/Location/{print $2}')/litellm/proxy/
35
44
  # Prisma Python client — required by LiteLLM proxy whenever a `database_url`
36
45
  # is set in config.litellm.yaml (used for user/budget tracking, key
37
- # management, etc.). NOT included in litellm[proxy] in 1.85.1, hence the
46
+ # management, etc.). NOT included in litellm[proxy] in 1.97.0, hence the
38
47
  # separate pin. setup.sh runs `prisma generate` against LiteLLM's bundled
39
48
  # schema after pip install so the Python client module is materialized.
40
49
  prisma==0.15.0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "3.5.1",
4
+ "version": "3.7.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {