@exulu/backend 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1221,6 +1221,18 @@ var init_sanitize_name = __esm({
1221
1221
  }
1222
1222
  });
1223
1223
 
1224
+ // src/exulu/table-names.ts
1225
+ var getTableName, getChunksTableName;
1226
+ var init_table_names = __esm({
1227
+ "src/exulu/table-names.ts"() {
1228
+ "use strict";
1229
+ init_cjs_shims();
1230
+ init_sanitize_name();
1231
+ getTableName = (id) => sanitizeName(id) + "_items";
1232
+ getChunksTableName = (id) => sanitizeName(id) + "_chunks";
1233
+ }
1234
+ });
1235
+
1224
1236
  // src/exulu/litellm/supervisor.ts
1225
1237
  var import_node_child_process, import_node_fs3, import_node_path2, LITELLM_UI_PATH, MAX_CRASHES, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, READY_TIMEOUT_MS, WAIT_TIMEOUT_MS, READY_POLL_INTERVAL_MS, SHUTDOWN_GRACE_MS, internal, isLiteLLMEnabled, resolveConfig, log2, pollHealth, spawnLiteLLM, supervise, _packageRoot, _clientMode, setLiteLLMPackageRoot, enableLiteLLMClientMode, startLiteLLMSupervisor, waitForLiteLLMReady, stopLiteLLM, shutdownHandlersRegistered, registerShutdownHandlers, getSupervisorState;
1226
1238
  var init_supervisor = __esm({
@@ -1819,8 +1831,8 @@ async function getTagDailyActivity(params) {
1819
1831
  }
1820
1832
  return await res.json().catch(() => ({}));
1821
1833
  }
1822
- async function getTagSpendByWindow(windows, endDate) {
1823
- const windowEntries = Object.entries(windows);
1834
+ async function getTagSpendByWindow(windows2, endDate) {
1835
+ const windowEntries = Object.entries(windows2);
1824
1836
  const out = {};
1825
1837
  for (const [tag] of windowEntries) out[tag] = 0;
1826
1838
  if (windowEntries.length === 0) return out;
@@ -1945,14 +1957,14 @@ function windowStartYmd(reset_at, duration) {
1945
1957
  async function enrichSpendFromActivity(map) {
1946
1958
  const names = Object.keys(map);
1947
1959
  if (names.length === 0) return;
1948
- const windows = {};
1960
+ const windows2 = {};
1949
1961
  for (const name of names) {
1950
1962
  const ti = map[name];
1951
- windows[name] = windowStartYmd(ti.budget_reset_at, ti.budget_duration);
1963
+ windows2[name] = windowStartYmd(ti.budget_reset_at, ti.budget_duration);
1952
1964
  }
1953
1965
  try {
1954
1966
  const spendByTag = await getTagSpendByWindow(
1955
- windows,
1967
+ windows2,
1956
1968
  ymd(new Date(Date.now() + DAY_MS))
1957
1969
  );
1958
1970
  for (const name of names) {
@@ -2524,89 +2536,152 @@ var init_singleton = __esm({
2524
2536
  }
2525
2537
  });
2526
2538
 
2527
- // src/exulu/oauth/validate.ts
2528
- var REQUIRED_STRING_FIELDS, validateOauthConfig;
2539
+ // src/exulu/auth/validate.ts
2540
+ var OAUTH_REQUIRED_STRING_FIELDS, validateAuthConfig;
2529
2541
  var init_validate = __esm({
2530
- "src/exulu/oauth/validate.ts"() {
2542
+ "src/exulu/auth/validate.ts"() {
2531
2543
  "use strict";
2532
2544
  init_cjs_shims();
2533
- REQUIRED_STRING_FIELDS = [
2545
+ OAUTH_REQUIRED_STRING_FIELDS = [
2534
2546
  "authorizationUrl",
2535
2547
  "tokenUrl",
2536
2548
  "clientId",
2537
2549
  "clientSecret"
2538
2550
  ];
2539
- validateOauthConfig = (toolId, config) => {
2540
- for (const field of REQUIRED_STRING_FIELDS) {
2541
- if (!config[field] || typeof config[field] !== "string") {
2551
+ validateAuthConfig = (toolId, config) => {
2552
+ if (config.authType === "oauth") {
2553
+ for (const field of OAUTH_REQUIRED_STRING_FIELDS) {
2554
+ const value = config[field];
2555
+ if (!value || typeof value !== "string") {
2556
+ throw new Error(
2557
+ `ExuluTool "${toolId}": oauth.${field} is required and must be a non-empty string.`
2558
+ );
2559
+ }
2560
+ }
2561
+ if (!Array.isArray(config.scopes)) {
2542
2562
  throw new Error(
2543
- `ExuluTool "${toolId}": oauth.${field} is required and must be a non-empty string.`
2563
+ `ExuluTool "${toolId}": oauth.scopes must be an array of strings (use [] to request no scopes).`
2544
2564
  );
2545
2565
  }
2566
+ if (config.provider !== void 0) {
2567
+ if (typeof config.provider !== "string" || config.provider.length === 0 || config.provider.trim() !== config.provider) {
2568
+ throw new Error(
2569
+ `ExuluTool "${toolId}": oauth.provider must be a non-empty string with no leading or trailing whitespace when set.`
2570
+ );
2571
+ }
2572
+ }
2573
+ if (!process.env.BACKEND) {
2574
+ throw new Error(
2575
+ `ExuluTool "${toolId}": oauth requires the BACKEND environment variable (the backend's public base URL) to build the redirect URI.`
2576
+ );
2577
+ }
2578
+ return;
2546
2579
  }
2547
- if (!Array.isArray(config.scopes)) {
2548
- throw new Error(
2549
- `ExuluTool "${toolId}": oauth.scopes must be an array of strings (use [] to request no scopes).`
2550
- );
2551
- }
2552
- if (config.provider !== void 0) {
2580
+ if (config.authType === "user_credentials") {
2553
2581
  if (typeof config.provider !== "string" || config.provider.length === 0 || config.provider.trim() !== config.provider) {
2554
2582
  throw new Error(
2555
- `ExuluTool "${toolId}": oauth.provider must be a non-empty string with no leading or trailing whitespace when set.`
2583
+ `ExuluTool "${toolId}": user_credentials.provider must be a non-empty string with no leading or trailing whitespace.`
2556
2584
  );
2557
2585
  }
2586
+ if (!Array.isArray(config.fields) || config.fields.length === 0) {
2587
+ throw new Error(
2588
+ `ExuluTool "${toolId}": user_credentials.fields must contain at least one field.`
2589
+ );
2590
+ }
2591
+ const seenNames = /* @__PURE__ */ new Set();
2592
+ for (let i = 0; i < config.fields.length; i++) {
2593
+ const field = config.fields[i];
2594
+ const name = field.name;
2595
+ if (typeof name !== "string" || name.length === 0 || name.trim() !== name) {
2596
+ throw new Error(
2597
+ `ExuluTool "${toolId}": user_credentials.fields[${i}].name must be a non-empty string with no leading or trailing whitespace.`
2598
+ );
2599
+ }
2600
+ if (seenNames.has(name)) {
2601
+ throw new Error(
2602
+ `ExuluTool "${toolId}": user_credentials.fields has duplicate field name '${name}'.`
2603
+ );
2604
+ }
2605
+ seenNames.add(name);
2606
+ const type = field.type;
2607
+ if (type !== "text" && type !== "password") {
2608
+ throw new Error(
2609
+ `ExuluTool "${toolId}": user_credentials.fields[${i}].type must be 'text' or 'password' (got '${type}').`
2610
+ );
2611
+ }
2612
+ }
2613
+ if (!process.env.BACKEND) {
2614
+ throw new Error(
2615
+ `ExuluTool "${toolId}": user_credentials requires the BACKEND environment variable (the backend's public base URL) to build the credential submit URL.`
2616
+ );
2617
+ }
2618
+ return;
2558
2619
  }
2559
- if (!process.env.BACKEND) {
2560
- throw new Error(
2561
- `ExuluTool "${toolId}": oauth requires the BACKEND environment variable (the backend's public base URL) to build the redirect URI.`
2562
- );
2563
- }
2620
+ throw new Error(
2621
+ `ExuluTool "${toolId}": auth.authType '${config.authType}' is not supported.`
2622
+ );
2564
2623
  };
2565
2624
  }
2566
2625
  });
2567
2626
 
2568
- // src/exulu/oauth/provider-key.ts
2627
+ // src/exulu/auth/provider-key.ts
2569
2628
  var providerKeyFor;
2570
2629
  var init_provider_key = __esm({
2571
- "src/exulu/oauth/provider-key.ts"() {
2630
+ "src/exulu/auth/provider-key.ts"() {
2572
2631
  "use strict";
2573
2632
  init_cjs_shims();
2574
2633
  providerKeyFor = (toolId, config) => config.provider && config.provider.length > 0 ? config.provider : toolId;
2575
2634
  }
2576
2635
  });
2577
2636
 
2578
- // src/exulu/oauth/registry.ts
2579
- var byProvider, byTool, STABLE_STRING_FIELDS, assertCompatible, oauthRegistry;
2637
+ // src/exulu/auth/registry.ts
2638
+ var byProvider, byTool, STABLE_OAUTH_STRING_FIELDS, assertCompatible, authRegistry;
2580
2639
  var init_registry = __esm({
2581
- "src/exulu/oauth/registry.ts"() {
2640
+ "src/exulu/auth/registry.ts"() {
2582
2641
  "use strict";
2583
2642
  init_cjs_shims();
2584
2643
  init_provider_key();
2585
2644
  byProvider = /* @__PURE__ */ new Map();
2586
2645
  byTool = /* @__PURE__ */ new Map();
2587
- STABLE_STRING_FIELDS = [
2646
+ STABLE_OAUTH_STRING_FIELDS = [
2588
2647
  "authorizationUrl",
2589
2648
  "tokenUrl",
2590
2649
  "clientId",
2591
2650
  "clientSecret"
2592
2651
  ];
2593
2652
  assertCompatible = (providerKey, toolId, existing, next) => {
2594
- for (const field of STABLE_STRING_FIELDS) {
2595
- if (existing[field] !== next[field]) {
2653
+ if (existing.authType !== next.authType) {
2654
+ throw new Error(
2655
+ `ExuluTool "${toolId}": auth.authType '${next.authType}' disagrees with another tool that shares provider "${providerKey}" using authType '${existing.authType}'.`
2656
+ );
2657
+ }
2658
+ if (existing.authType === "oauth" && next.authType === "oauth") {
2659
+ for (const field of STABLE_OAUTH_STRING_FIELDS) {
2660
+ if (existing[field] !== next[field]) {
2661
+ throw new Error(
2662
+ `ExuluTool "${toolId}": oauth.${field} disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must use identical authorizationUrl/tokenUrl/clientId/clientSecret.`
2663
+ );
2664
+ }
2665
+ }
2666
+ const a = new Set(existing.scopes);
2667
+ const b = new Set(next.scopes);
2668
+ if (a.size !== b.size || [...a].some((s) => !b.has(s))) {
2596
2669
  throw new Error(
2597
- `ExuluTool "${toolId}": oauth.${field} disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must use identical authorizationUrl/tokenUrl/clientId/clientSecret.`
2670
+ `ExuluTool "${toolId}": oauth.scopes disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must declare the same scope superset. Existing: [${[...a].sort().join(", ")}]. This tool: [${[...b].sort().join(", ")}].`
2598
2671
  );
2599
2672
  }
2673
+ return;
2600
2674
  }
2601
- const a = new Set(existing.scopes);
2602
- const b = new Set(next.scopes);
2603
- if (a.size !== b.size || [...a].some((s) => !b.has(s))) {
2604
- throw new Error(
2605
- `ExuluTool "${toolId}": oauth.scopes disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must declare the same scope superset. Existing: [${[...a].sort().join(", ")}]. This tool: [${[...b].sort().join(", ")}].`
2606
- );
2675
+ if (existing.authType === "user_credentials" && next.authType === "user_credentials") {
2676
+ if (JSON.stringify(existing.fields) !== JSON.stringify(next.fields)) {
2677
+ throw new Error(
2678
+ `ExuluTool "${toolId}": user_credentials.fields disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must declare structurally identical fields (same names, types, and order).`
2679
+ );
2680
+ }
2681
+ return;
2607
2682
  }
2608
2683
  };
2609
- oauthRegistry = {
2684
+ authRegistry = {
2610
2685
  register: (toolId, config) => {
2611
2686
  const providerKey = providerKeyFor(toolId, config);
2612
2687
  const existing = byProvider.get(providerKey);
@@ -2628,72 +2703,86 @@ var init_registry = __esm({
2628
2703
  }
2629
2704
  });
2630
2705
 
2631
- // src/exulu/oauth/token-store.ts
2632
- var import_crypto_js2, TABLE, encrypt, decrypt, oauthTokenStore;
2633
- var init_token_store = __esm({
2634
- "src/exulu/oauth/token-store.ts"() {
2706
+ // src/exulu/auth/credential-store.ts
2707
+ async function get(provider, userId) {
2708
+ const { db: db2 } = await postgresClient();
2709
+ const row = await db2.from(TABLE).where({ provider, user_id: String(userId) }).first();
2710
+ if (!row) {
2711
+ return null;
2712
+ }
2713
+ return {
2714
+ provider,
2715
+ userId,
2716
+ authType: row.auth_type,
2717
+ data: JSON.parse(decrypt(row.data))
2718
+ };
2719
+ }
2720
+ async function upsert(record) {
2721
+ const { db: db2 } = await postgresClient();
2722
+ const encrypted = encrypt(JSON.stringify(record.data));
2723
+ await db2.from(TABLE).insert({
2724
+ provider: record.provider,
2725
+ user_id: String(record.userId),
2726
+ auth_type: record.authType,
2727
+ data: encrypted,
2728
+ updated_at: /* @__PURE__ */ new Date()
2729
+ }).onConflict(["provider", "user_id"]).merge({ auth_type: record.authType, data: encrypted, updated_at: /* @__PURE__ */ new Date() });
2730
+ }
2731
+ async function listByUser(userId) {
2732
+ const { db: db2 } = await postgresClient();
2733
+ const list = await db2.from(TABLE).where({ user_id: String(userId) }).orderBy("provider");
2734
+ return list.map((row) => ({
2735
+ provider: row.provider,
2736
+ authType: row.auth_type,
2737
+ createdAt: row.created_at,
2738
+ updatedAt: row.updated_at
2739
+ }));
2740
+ }
2741
+ async function del(provider, userId) {
2742
+ const { db: db2 } = await postgresClient();
2743
+ await db2.from(TABLE).where({ provider, user_id: String(userId) }).del();
2744
+ }
2745
+ var import_crypto_js2, TABLE, encrypt, decrypt, credentialStore;
2746
+ var init_credential_store = __esm({
2747
+ "src/exulu/auth/credential-store.ts"() {
2635
2748
  "use strict";
2636
2749
  init_cjs_shims();
2637
2750
  import_crypto_js2 = __toESM(require("crypto-js"), 1);
2638
2751
  init_client();
2639
- TABLE = "oauth_tokens";
2752
+ TABLE = "user_credentials";
2640
2753
  encrypt = (value) => import_crypto_js2.default.AES.encrypt(value, process.env.NEXTAUTH_SECRET).toString();
2641
2754
  decrypt = (value) => import_crypto_js2.default.AES.decrypt(value, process.env.NEXTAUTH_SECRET).toString(import_crypto_js2.default.enc.Utf8);
2642
- oauthTokenStore = {
2643
- get: async (providerKey, userId) => {
2644
- const { db: db2 } = await postgresClient();
2645
- const row = await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).first();
2646
- if (!row) {
2647
- return null;
2648
- }
2649
- return {
2650
- accessToken: decrypt(row.access_token),
2651
- refreshToken: row.refresh_token ? decrypt(row.refresh_token) : null,
2652
- tokenType: row.token_type ?? null,
2653
- scopes: row.scopes ?? null,
2654
- expiresAt: row.expires_at ? new Date(row.expires_at) : null
2655
- };
2656
- },
2657
- // toolId is stored on every written row as an audit trail — which tool
2658
- // triggered the last grant/refresh — but does NOT participate in the key.
2659
- upsert: async (providerKey, userId, toolId, record) => {
2660
- const { db: db2 } = await postgresClient();
2661
- const existing = await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).first();
2662
- const values = {
2663
- provider: providerKey,
2664
- tool_id: toolId,
2665
- access_token: encrypt(record.accessToken),
2666
- // Providers like Google only send a refresh_token on first consent;
2667
- // never overwrite a stored one with nothing.
2668
- refresh_token: record.refreshToken ? encrypt(record.refreshToken) : existing?.refresh_token ?? null,
2669
- token_type: record.tokenType ?? null,
2670
- scopes: record.scopes ?? null,
2671
- expires_at: record.expiresAt ?? null,
2672
- updatedAt: /* @__PURE__ */ new Date()
2673
- };
2674
- if (existing) {
2675
- await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).update(values);
2676
- } else {
2677
- await db2.from(TABLE).insert({ user_id: userId, ...values });
2678
- }
2679
- },
2680
- delete: async (providerKey, userId) => {
2681
- const { db: db2 } = await postgresClient();
2682
- await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).del();
2683
- }
2684
- };
2755
+ credentialStore = { get, upsert, listByUser, delete: del };
2685
2756
  }
2686
2757
  });
2687
2758
 
2688
- // src/exulu/oauth/flow.ts
2759
+ // src/exulu/auth/flow.ts
2760
+ function oauthRecordToBlob(r) {
2761
+ return {
2762
+ accessToken: r.accessToken,
2763
+ refreshToken: r.refreshToken ?? null,
2764
+ tokenType: r.tokenType ?? null,
2765
+ scopes: r.scopes ?? null,
2766
+ expiresAt: r.expiresAt ? r.expiresAt.toISOString() : null
2767
+ };
2768
+ }
2769
+ function oauthBlobToRecord(b) {
2770
+ return {
2771
+ accessToken: b.accessToken,
2772
+ refreshToken: b.refreshToken,
2773
+ tokenType: b.tokenType,
2774
+ scopes: b.scopes,
2775
+ expiresAt: b.expiresAt ? new Date(b.expiresAt) : null
2776
+ };
2777
+ }
2689
2778
  var import_crypto_js3, import_node_crypto2, OAUTH_CALLBACK_PATH, STATE_TTL_MS, EXPIRY_SKEW_MS, getOauthRedirectUri, toBase64Url, fromBase64Url, encryptOauthState, decryptOauthState, buildAuthorizationUrl, tokenResponseToRecord, postTokenEndpoint, exchangeCodeForTokens, refreshAccessToken, getValidAccessToken;
2690
2779
  var init_flow = __esm({
2691
- "src/exulu/oauth/flow.ts"() {
2780
+ "src/exulu/auth/flow.ts"() {
2692
2781
  "use strict";
2693
2782
  init_cjs_shims();
2694
2783
  import_crypto_js3 = __toESM(require("crypto-js"), 1);
2695
2784
  import_node_crypto2 = require("crypto");
2696
- init_token_store();
2785
+ init_credential_store();
2697
2786
  init_provider_key();
2698
2787
  OAUTH_CALLBACK_PATH = "/oauth/callback";
2699
2788
  STATE_TTL_MS = 10 * 60 * 1e3;
@@ -2837,7 +2926,8 @@ var init_flow = __esm({
2837
2926
  toolId,
2838
2927
  config
2839
2928
  }) => {
2840
- const stored = await oauthTokenStore.get(providerKey, userId);
2929
+ const credRow = await credentialStore.get(providerKey, userId);
2930
+ const stored = credRow ? oauthBlobToRecord(credRow.data) : null;
2841
2931
  if (!stored) {
2842
2932
  return null;
2843
2933
  }
@@ -2846,7 +2936,7 @@ var init_flow = __esm({
2846
2936
  return stored;
2847
2937
  }
2848
2938
  if (!stored.refreshToken) {
2849
- await oauthTokenStore.delete(providerKey, userId);
2939
+ await credentialStore.delete(providerKey, userId);
2850
2940
  return null;
2851
2941
  }
2852
2942
  try {
@@ -2854,29 +2944,169 @@ var init_flow = __esm({
2854
2944
  if (!refreshed.refreshToken) {
2855
2945
  refreshed.refreshToken = stored.refreshToken;
2856
2946
  }
2857
- await oauthTokenStore.upsert(providerKey, userId, toolId, refreshed);
2947
+ await credentialStore.upsert({
2948
+ provider: providerKey,
2949
+ userId,
2950
+ authType: "oauth",
2951
+ data: oauthRecordToBlob(refreshed)
2952
+ });
2858
2953
  return refreshed;
2859
2954
  } catch (error) {
2860
2955
  console.error(
2861
2956
  `[EXULU] OAuth token refresh failed for provider "${providerKey}" tool "${toolId}" user ${userId}:`,
2862
2957
  error
2863
2958
  );
2864
- await oauthTokenStore.delete(providerKey, userId);
2959
+ await credentialStore.delete(providerKey, userId);
2865
2960
  return null;
2866
2961
  }
2867
2962
  };
2868
2963
  }
2869
2964
  });
2870
2965
 
2871
- // src/exulu/oauth/wrap-execute.ts
2872
- var wrapExecuteWithOauth;
2966
+ // src/exulu/auth/state.ts
2967
+ async function getValidUserCredentials(cfg, userId) {
2968
+ const row = await credentialStore.get(cfg.provider, userId);
2969
+ if (!row || row.authType !== "user_credentials") return null;
2970
+ const values = {};
2971
+ for (const [k, v] of Object.entries(row.data)) {
2972
+ if (typeof v !== "string") return null;
2973
+ values[k] = v;
2974
+ }
2975
+ return values;
2976
+ }
2977
+ var init_state = __esm({
2978
+ "src/exulu/auth/state.ts"() {
2979
+ "use strict";
2980
+ init_cjs_shims();
2981
+ init_credential_store();
2982
+ }
2983
+ });
2984
+
2985
+ // src/exulu/auth/credentials-request.ts
2986
+ function buildCredentialRequest(cfg, opts) {
2987
+ const ttl = opts.ttlSeconds ?? DEFAULT_TTL_SECONDS;
2988
+ const claims = {
2989
+ provider: cfg.provider,
2990
+ userId: opts.userId,
2991
+ expiresAt: Math.floor(Date.now() / 1e3) + ttl
2992
+ };
2993
+ const nonce = encrypt(JSON.stringify(claims));
2994
+ return {
2995
+ provider: cfg.provider,
2996
+ fields: cfg.fields,
2997
+ submitUrl: `${opts.baseUrl.replace(/\/+$/, "")}/credentials/submit`,
2998
+ nonce
2999
+ };
3000
+ }
3001
+ function verifyCredentialNonce(nonce) {
3002
+ let claims;
3003
+ try {
3004
+ claims = JSON.parse(decrypt(nonce));
3005
+ } catch {
3006
+ throw new Error("Invalid credential nonce");
3007
+ }
3008
+ if (typeof claims.provider !== "string" || typeof claims.userId !== "string" || typeof claims.expiresAt !== "number") {
3009
+ throw new Error("Malformed credential nonce claims");
3010
+ }
3011
+ if (claims.expiresAt < Math.floor(Date.now() / 1e3)) {
3012
+ throw new Error("Credential nonce expired");
3013
+ }
3014
+ return claims;
3015
+ }
3016
+ var DEFAULT_TTL_SECONDS;
3017
+ var init_credentials_request = __esm({
3018
+ "src/exulu/auth/credentials-request.ts"() {
3019
+ "use strict";
3020
+ init_cjs_shims();
3021
+ init_credential_store();
3022
+ DEFAULT_TTL_SECONDS = 15 * 60;
3023
+ }
3024
+ });
3025
+
3026
+ // src/exulu/auth/short-circuit.ts
3027
+ function credentialRequestResult(request3) {
3028
+ return { credentialRequest: request3, result: null };
3029
+ }
3030
+ var init_short_circuit = __esm({
3031
+ "src/exulu/auth/short-circuit.ts"() {
3032
+ "use strict";
3033
+ init_cjs_shims();
3034
+ }
3035
+ });
3036
+
3037
+ // src/exulu/auth/errors.ts
3038
+ var CredentialInvalidError;
3039
+ var init_errors = __esm({
3040
+ "src/exulu/auth/errors.ts"() {
3041
+ "use strict";
3042
+ init_cjs_shims();
3043
+ CredentialInvalidError = class extends Error {
3044
+ provider;
3045
+ reason;
3046
+ constructor(provider, reason) {
3047
+ super(reason ? `Credential invalid for provider '${provider}': ${reason}` : `Credential invalid for provider '${provider}'`);
3048
+ this.name = "CredentialInvalidError";
3049
+ this.provider = provider;
3050
+ this.reason = reason;
3051
+ }
3052
+ };
3053
+ }
3054
+ });
3055
+
3056
+ // src/exulu/auth/wrap-execute.ts
3057
+ var wrapExecuteWithAuth, wrapUserCredentials, wrapExecuteWithOauthInternal;
2873
3058
  var init_wrap_execute = __esm({
2874
- "src/exulu/oauth/wrap-execute.ts"() {
3059
+ "src/exulu/auth/wrap-execute.ts"() {
2875
3060
  "use strict";
2876
3061
  init_cjs_shims();
2877
3062
  init_flow();
2878
3063
  init_provider_key();
2879
- wrapExecuteWithOauth = (toolId, config, execute2) => {
3064
+ init_state();
3065
+ init_credentials_request();
3066
+ init_short_circuit();
3067
+ init_credential_store();
3068
+ init_errors();
3069
+ wrapExecuteWithAuth = (toolId, config, execute2) => {
3070
+ if (config.authType === "oauth") {
3071
+ return wrapExecuteWithOauthInternal(toolId, config, execute2);
3072
+ }
3073
+ if (config.authType === "user_credentials") {
3074
+ return wrapUserCredentials(toolId, config, execute2);
3075
+ }
3076
+ throw new Error(`ExuluTool "${toolId}": unknown authType`);
3077
+ };
3078
+ wrapUserCredentials = (toolId, config, execute2) => {
3079
+ return async (inputs, options) => {
3080
+ const userId = inputs?.user?.id;
3081
+ if (!userId) {
3082
+ return {
3083
+ result: `The "${toolId}" tool requires user-supplied credentials, which needs a signed-in user. No user identity is available for this run.`
3084
+ };
3085
+ }
3086
+ const baseUrl = (process.env.BACKEND ?? "").replace(/\/+$/, "");
3087
+ if (!baseUrl) {
3088
+ return {
3089
+ result: `The "${toolId}" tool requires the BACKEND env var to build the credential submit URL.`
3090
+ };
3091
+ }
3092
+ const values = await getValidUserCredentials(config, userId);
3093
+ if (!values) {
3094
+ const request3 = buildCredentialRequest(config, { baseUrl, userId: String(userId) });
3095
+ return credentialRequestResult(request3);
3096
+ }
3097
+ try {
3098
+ return await execute2({ ...inputs, credentials: values }, options);
3099
+ } catch (e) {
3100
+ if (e instanceof CredentialInvalidError && e.provider === config.provider) {
3101
+ await credentialStore.delete(config.provider, userId);
3102
+ const request3 = buildCredentialRequest(config, { baseUrl, userId: String(userId) });
3103
+ return credentialRequestResult(request3);
3104
+ }
3105
+ throw e;
3106
+ }
3107
+ };
3108
+ };
3109
+ wrapExecuteWithOauthInternal = (toolId, config, execute2) => {
2880
3110
  return async (inputs, options) => {
2881
3111
  const userId = inputs?.user?.id;
2882
3112
  if (!userId) {
@@ -3233,6 +3463,409 @@ var init_memory_tool = __esm({
3233
3463
  }
3234
3464
  });
3235
3465
 
3466
+ // src/utils/check-item-write-access.ts
3467
+ var checkItemWriteAccess;
3468
+ var init_check_item_write_access = __esm({
3469
+ "src/utils/check-item-write-access.ts"() {
3470
+ "use strict";
3471
+ init_cjs_shims();
3472
+ init_client();
3473
+ init_table_names();
3474
+ checkItemWriteAccess = async (context, record, user) => {
3475
+ if (!user) {
3476
+ return false;
3477
+ }
3478
+ if (user.super_admin === true) {
3479
+ return true;
3480
+ }
3481
+ if (user.type === "api" && (!user.scope_mode || user.scope_mode === "admin")) {
3482
+ return true;
3483
+ }
3484
+ if (record.rights_mode === "public") {
3485
+ return true;
3486
+ }
3487
+ if (record.rights_mode === "private") {
3488
+ return record.created_by != null && String(record.created_by) === String(user.id);
3489
+ }
3490
+ const validRightsModes = ["users", "roles", "teams"];
3491
+ if (!validRightsModes.includes(record.rights_mode)) {
3492
+ return false;
3493
+ }
3494
+ const entity = getTableName(context.id);
3495
+ const { db: db2 } = await postgresClient();
3496
+ if (record.rights_mode === "users") {
3497
+ const grant = await db2.from("rbac").where({
3498
+ entity,
3499
+ target_resource_id: record.id,
3500
+ access_type: "User",
3501
+ user_id: user.id,
3502
+ rights: "write"
3503
+ }).first();
3504
+ return !!grant;
3505
+ }
3506
+ if (record.rights_mode === "roles") {
3507
+ const roleId = typeof user.role === "string" ? user.role : user.role?.id;
3508
+ if (!roleId) {
3509
+ return false;
3510
+ }
3511
+ const grant = await db2.from("rbac").where({
3512
+ entity,
3513
+ target_resource_id: record.id,
3514
+ access_type: "Role",
3515
+ role_id: roleId,
3516
+ rights: "write"
3517
+ }).first();
3518
+ return !!grant;
3519
+ }
3520
+ if (record.rights_mode === "teams") {
3521
+ const teamId = typeof user.team === "string" ? user.team : user.team?.id;
3522
+ if (!teamId) {
3523
+ return false;
3524
+ }
3525
+ const grant = await db2.from("rbac").where({
3526
+ entity,
3527
+ target_resource_id: record.id,
3528
+ access_type: "Team",
3529
+ team_id: teamId,
3530
+ rights: "write"
3531
+ }).first();
3532
+ return !!grant;
3533
+ }
3534
+ return false;
3535
+ };
3536
+ }
3537
+ });
3538
+
3539
+ // src/templates/tools/kb-editor-config.ts
3540
+ var import_zod4, KB_EDITOR_TOOL_ID, permissionsSchema, emptyConfig, parseKbEditorConfig;
3541
+ var init_kb_editor_config = __esm({
3542
+ "src/templates/tools/kb-editor-config.ts"() {
3543
+ "use strict";
3544
+ init_cjs_shims();
3545
+ import_zod4 = require("zod");
3546
+ KB_EDITOR_TOOL_ID = "knowledge_base_editor";
3547
+ permissionsSchema = import_zod4.z.object({
3548
+ create: import_zod4.z.boolean().catch(false).default(false),
3549
+ update: import_zod4.z.boolean().catch(false).default(false)
3550
+ });
3551
+ emptyConfig = () => ({
3552
+ enabled: false,
3553
+ knowledgeBases: {},
3554
+ skipApproval: false
3555
+ });
3556
+ parseKbEditorConfig = (tools) => {
3557
+ let entries = tools;
3558
+ if (typeof entries === "string") {
3559
+ try {
3560
+ entries = JSON.parse(entries);
3561
+ } catch {
3562
+ return emptyConfig();
3563
+ }
3564
+ }
3565
+ if (!Array.isArray(entries)) {
3566
+ return emptyConfig();
3567
+ }
3568
+ const entry = entries.find((t) => t?.id === KB_EDITOR_TOOL_ID);
3569
+ if (!entry) {
3570
+ return emptyConfig();
3571
+ }
3572
+ const rawValue = (name) => {
3573
+ const row = Array.isArray(entry.config) ? entry.config.find((c) => c?.name === name) : void 0;
3574
+ return row?.value ?? row?.variable ?? row?.default;
3575
+ };
3576
+ let kbsRaw = rawValue("knowledge_bases");
3577
+ if (typeof kbsRaw === "string" && kbsRaw) {
3578
+ try {
3579
+ kbsRaw = JSON.parse(kbsRaw);
3580
+ } catch {
3581
+ kbsRaw = {};
3582
+ }
3583
+ }
3584
+ const knowledgeBases = {};
3585
+ if (kbsRaw && typeof kbsRaw === "object" && !Array.isArray(kbsRaw)) {
3586
+ for (const [contextId, value] of Object.entries(kbsRaw)) {
3587
+ const parsed = permissionsSchema.safeParse(value);
3588
+ if (parsed.success && (parsed.data.create || parsed.data.update)) {
3589
+ knowledgeBases[contextId] = parsed.data;
3590
+ }
3591
+ }
3592
+ }
3593
+ const skipRaw = rawValue("skip_approval");
3594
+ const skipApproval = skipRaw === true || skipRaw === "true" || skipRaw === 1;
3595
+ return { enabled: true, knowledgeBases, skipApproval };
3596
+ };
3597
+ }
3598
+ });
3599
+
3600
+ // src/templates/tools/context-write-tools.ts
3601
+ var import_zod5, MAX_CONTEXT_SEGMENT, RESERVED_INPUT_KEYS, buildWriteSchema, canonicalizeEnumFields, pickContent, jobNote, createContextWriteTools, createKbEditorPickerTool, collectKbWriteTools;
3602
+ var init_context_write_tools = __esm({
3603
+ "src/templates/tools/context-write-tools.ts"() {
3604
+ "use strict";
3605
+ init_cjs_shims();
3606
+ init_tool();
3607
+ import_zod5 = require("zod");
3608
+ init_sanitize_name();
3609
+ init_check_item_write_access();
3610
+ init_kb_editor_config();
3611
+ MAX_CONTEXT_SEGMENT = 68;
3612
+ RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([
3613
+ "model",
3614
+ "user",
3615
+ "contexts",
3616
+ "memory",
3617
+ "req",
3618
+ "upload",
3619
+ "sessionID",
3620
+ "sessionItems",
3621
+ "providerapikey",
3622
+ "allExuluTools",
3623
+ "currentTools",
3624
+ "exuluConfig",
3625
+ "toolVariablesConfig",
3626
+ "oauth"
3627
+ ]);
3628
+ buildWriteSchema = (context, mode) => {
3629
+ const shape = {};
3630
+ const contentKeys = [];
3631
+ const addContent = (key, schema, required) => {
3632
+ shape[key] = required && mode === "create" ? schema : schema.optional();
3633
+ contentKeys.push(key);
3634
+ };
3635
+ if (mode === "update") {
3636
+ shape["id"] = import_zod5.z.string().optional().describe("The id of the item to update.");
3637
+ shape["external_id"] = import_zod5.z.string().optional().describe("The external_id of the item to update, if the id is unknown. Lookup only \u2014 it is never changed.");
3638
+ }
3639
+ addContent("name", import_zod5.z.string().describe("The name of the item."), true);
3640
+ addContent("description", import_zod5.z.string().describe("A description of the item."), false);
3641
+ addContent("tags", import_zod5.z.array(import_zod5.z.string()).describe("Tags for the item."), false);
3642
+ if (mode === "create") {
3643
+ addContent(
3644
+ "external_id",
3645
+ import_zod5.z.string().describe("An optional external identifier for the item, e.g. an id from a source system."),
3646
+ false
3647
+ );
3648
+ }
3649
+ for (const field of context.fields ?? []) {
3650
+ if (field.type === "file" || field.type === "uuid") continue;
3651
+ if (field.calculated === true || field.editable === false) continue;
3652
+ if (field.hidden === true) continue;
3653
+ if (RESERVED_INPUT_KEYS.has(field.name)) continue;
3654
+ let schema;
3655
+ switch (field.type) {
3656
+ case "enum":
3657
+ schema = import_zod5.z.string().describe(
3658
+ `The ${field.name} of the item. Must be one of: ${(field.enumValues ?? []).join(", ")}`
3659
+ );
3660
+ break;
3661
+ case "json":
3662
+ schema = import_zod5.z.string().describe(`The ${field.name} of the item, as a valid JSON string.`);
3663
+ break;
3664
+ case "markdown":
3665
+ schema = import_zod5.z.string().describe(`The ${field.name} of the item, as a valid Markdown string.`);
3666
+ break;
3667
+ case "date":
3668
+ schema = import_zod5.z.string().describe(`The ${field.name} of the item, as an ISO-8601 date string.`);
3669
+ break;
3670
+ case "number":
3671
+ schema = import_zod5.z.number().describe(`The ${field.name} of the item.`);
3672
+ break;
3673
+ case "boolean":
3674
+ schema = import_zod5.z.boolean().describe(`The ${field.name} of the item.`);
3675
+ break;
3676
+ default:
3677
+ schema = import_zod5.z.string().describe(`The ${field.name} of the item.`);
3678
+ break;
3679
+ }
3680
+ addContent(field.name, schema, field.required === true);
3681
+ }
3682
+ return { shape, contentKeys };
3683
+ };
3684
+ canonicalizeEnumFields = (context, params) => {
3685
+ for (const field of context.fields ?? []) {
3686
+ if (field.type !== "enum" || !field.enumValues?.length) continue;
3687
+ const raw = params[field.name];
3688
+ if (raw === void 0 || raw === null || raw === "") continue;
3689
+ const rawStr = String(raw);
3690
+ const canonical = field.enumValues.find((v) => v.toUpperCase() === rawStr.toUpperCase());
3691
+ if (canonical === void 0) {
3692
+ return `Invalid value "${rawStr}" for field "${field.name}". Allowed values: ${field.enumValues.join(", ")}.`;
3693
+ }
3694
+ params[field.name] = canonical;
3695
+ }
3696
+ return void 0;
3697
+ };
3698
+ pickContent = (params, contentKeys) => {
3699
+ const item = {};
3700
+ for (const key of contentKeys) {
3701
+ if (params[key] !== void 0) {
3702
+ item[key] = params[key];
3703
+ }
3704
+ }
3705
+ return item;
3706
+ };
3707
+ jobNote = (job) => job ? ` Processing/embeddings queued (job: ${job}); changes become searchable when the job completes.` : "";
3708
+ createContextWriteTools = (context, perms, skipApproval) => {
3709
+ const tools = [];
3710
+ const segment = sanitizeName(context.id).slice(0, MAX_CONTEXT_SEGMENT);
3711
+ const contextLabel = context.description ? ` ${context.description}` : "";
3712
+ if (perms.create) {
3713
+ const { shape, contentKeys } = buildWriteSchema(context, "create");
3714
+ tools.push(
3715
+ new ExuluTool({
3716
+ id: `create_${segment}_item`,
3717
+ name: `Create ${context.name} item`,
3718
+ category: "knowledge_base_editing",
3719
+ description: `Create a new item in the "${context.name}" knowledge base.${contextLabel}`,
3720
+ type: "function",
3721
+ inputSchema: import_zod5.z.object(shape),
3722
+ config: [],
3723
+ needsApproval: !skipApproval,
3724
+ execute: async (params) => {
3725
+ const { user, exuluConfig } = params;
3726
+ if (!user?.id) {
3727
+ return { result: "Knowledge base writes require an authenticated user." };
3728
+ }
3729
+ try {
3730
+ const enumError = canonicalizeEnumFields(context, params);
3731
+ if (enumError) {
3732
+ return { result: enumError };
3733
+ }
3734
+ const item = pickContent(params, contentKeys);
3735
+ item.created_by = String(user.id);
3736
+ const { item: created, job } = await context.createItem(
3737
+ item,
3738
+ exuluConfig,
3739
+ user?.id,
3740
+ user?.role?.id,
3741
+ false
3742
+ );
3743
+ if (!created?.id) {
3744
+ return { result: `Failed to create item in "${context.name}".` };
3745
+ }
3746
+ return {
3747
+ result: `Created item ${created.id} in knowledge base "${context.name}".${jobNote(job)}`
3748
+ };
3749
+ } catch (error) {
3750
+ console.error(`[EXULU] Error creating item in context ${context.id}`, error);
3751
+ return {
3752
+ result: `Failed to create item in "${context.name}": ${error instanceof Error ? error.message : String(error)}`
3753
+ };
3754
+ }
3755
+ }
3756
+ })
3757
+ );
3758
+ }
3759
+ if (perms.update) {
3760
+ const { shape, contentKeys } = buildWriteSchema(context, "update");
3761
+ const NOT_FOUND = `Item not found in "${context.name}" or you don't have write access to it.`;
3762
+ tools.push(
3763
+ new ExuluTool({
3764
+ id: `update_${segment}_item`,
3765
+ name: `Update ${context.name} item`,
3766
+ category: "knowledge_base_editing",
3767
+ description: `Update an existing item in the "${context.name}" knowledge base. Provide the item's id (or external_id) plus only the fields to change; omitted fields keep their values.`,
3768
+ type: "function",
3769
+ inputSchema: import_zod5.z.object(shape),
3770
+ config: [],
3771
+ needsApproval: !skipApproval,
3772
+ execute: async (params) => {
3773
+ const { user, exuluConfig } = params;
3774
+ if (!user?.id) {
3775
+ return { result: "Knowledge base writes require an authenticated user." };
3776
+ }
3777
+ try {
3778
+ if (!params.id && !params.external_id) {
3779
+ return { result: "Provide the id or external_id of the item to update." };
3780
+ }
3781
+ const existing = await context.getItem({
3782
+ item: { id: params.id, external_id: params.external_id }
3783
+ });
3784
+ if (!existing?.id) {
3785
+ return { result: NOT_FOUND };
3786
+ }
3787
+ const allowed = await checkItemWriteAccess(context, existing, user);
3788
+ if (!allowed) {
3789
+ return { result: NOT_FOUND };
3790
+ }
3791
+ const enumError = canonicalizeEnumFields(context, params);
3792
+ if (enumError) {
3793
+ return { result: enumError };
3794
+ }
3795
+ const patch = pickContent(params, contentKeys);
3796
+ if (Object.keys(patch).length === 0) {
3797
+ return { result: "No fields to update were provided." };
3798
+ }
3799
+ patch.id = existing.id;
3800
+ const { job } = await context.updateItem(patch, exuluConfig, user?.id, user?.role?.id);
3801
+ const fresh = await context.getItem({ item: { id: existing.id } });
3802
+ const summary = { id: existing.id };
3803
+ for (const key of contentKeys) {
3804
+ if (fresh?.[key] !== void 0 && fresh?.[key] !== null) {
3805
+ summary[key] = fresh[key];
3806
+ }
3807
+ }
3808
+ return {
3809
+ result: `Updated item ${existing.id} in knowledge base "${context.name}".${jobNote(job)}
3810
+ Current item: ${JSON.stringify(summary)}`
3811
+ };
3812
+ } catch (error) {
3813
+ console.error(`[EXULU] Error updating item in context ${context.id}`, error);
3814
+ return {
3815
+ result: `Failed to update item in "${context.name}": ${error instanceof Error ? error.message : String(error)}`
3816
+ };
3817
+ }
3818
+ }
3819
+ })
3820
+ );
3821
+ }
3822
+ return tools;
3823
+ };
3824
+ createKbEditorPickerTool = () => new ExuluTool({
3825
+ id: KB_EDITOR_TOOL_ID,
3826
+ name: "Knowledge base editor",
3827
+ category: "default",
3828
+ description: "Let this agent create or update items in selected knowledge bases during chat. Configure per knowledge base whether the agent may create and/or update items.",
3829
+ type: "function",
3830
+ inputSchema: import_zod5.z.object({}),
3831
+ config: [
3832
+ {
3833
+ name: "knowledge_bases",
3834
+ description: "JSON record of context id to { create: boolean, update: boolean }. Contexts absent here get no write access.",
3835
+ type: "json"
3836
+ },
3837
+ {
3838
+ name: "skip_approval",
3839
+ description: "Run knowledge base writes without asking for approval in the chat.",
3840
+ type: "boolean",
3841
+ default: false
3842
+ }
3843
+ ],
3844
+ execute: async () => ({
3845
+ result: "This entry is configuration-only; Exulu expands it into per-context create/update tools at runtime."
3846
+ })
3847
+ });
3848
+ collectKbWriteTools = (agent, contexts) => {
3849
+ if (!agent?.tools || !contexts?.length) {
3850
+ return [];
3851
+ }
3852
+ const config = parseKbEditorConfig(agent.tools);
3853
+ if (!config.enabled) {
3854
+ return [];
3855
+ }
3856
+ const tools = [];
3857
+ for (const [contextId, perms] of Object.entries(config.knowledgeBases)) {
3858
+ const context = contexts.find((c) => c.id === contextId);
3859
+ if (!context) {
3860
+ continue;
3861
+ }
3862
+ tools.push(...createContextWriteTools(context, perms, config.skipApproval));
3863
+ }
3864
+ return tools;
3865
+ };
3866
+ }
3867
+ });
3868
+
3236
3869
  // src/exulu/system-dependencies.ts
3237
3870
  async function getNpmGlobalRoot() {
3238
3871
  if (cachedNpmGlobalRoot !== void 0) return cachedNpmGlobalRoot;
@@ -3747,9 +4380,9 @@ Probe error: ${probe.reason ?? "(no detail)"}`
3747
4380
  });
3748
4381
  const writeFileTool = (0, import_ai2.tool)({
3749
4382
  description: 'Write content to a file in the sandbox. Creates parent directories if needed. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. When the path is under the session artifact tree, the file is also uploaded to S3 and a short-lived presigned URL is returned in the tool output.',
3750
- inputSchema: import_zod4.z.object({
3751
- path: import_zod4.z.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
3752
- content: import_zod4.z.string().describe("The content to write to the file")
4383
+ inputSchema: import_zod6.z.object({
4384
+ path: import_zod6.z.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
4385
+ content: import_zod6.z.string().describe("The content to write to the file")
3753
4386
  }),
3754
4387
  execute: async ({ path: path2, content }) => {
3755
4388
  const resolvedPath = resolveSessionPath(path2, sessionDir);
@@ -3768,8 +4401,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
3768
4401
  });
3769
4402
  const readFileTool = (0, import_ai2.tool)({
3770
4403
  description: 'Read the contents of a file from the sandbox. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. If the file does not exist, the error message is surfaced verbatim.',
3771
- inputSchema: import_zod4.z.object({
3772
- path: import_zod4.z.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
4404
+ inputSchema: import_zod6.z.object({
4405
+ path: import_zod6.z.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
3773
4406
  }),
3774
4407
  execute: async ({ path: path2 }) => {
3775
4408
  const resolvedPath = resolveSessionPath(path2, sessionDir);
@@ -3780,8 +4413,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
3780
4413
  const originalBashTool = tools.bash;
3781
4414
  const bashTool = (0, import_ai2.tool)({
3782
4415
  description: originalBashTool.description ?? "",
3783
- inputSchema: import_zod4.z.object({
3784
- command: import_zod4.z.string().describe("The bash command to execute.")
4416
+ inputSchema: import_zod6.z.object({
4417
+ command: import_zod6.z.string().describe("The bash command to execute.")
3785
4418
  }),
3786
4419
  execute: async (args, opts) => {
3787
4420
  const before = persistenceEnabled ? await snapshotSessionArtifacts() : null;
@@ -3851,7 +4484,7 @@ ${lines.join("\n")}`;
3851
4484
  sandboxCache.set(sessionId, { handle, installedSkills });
3852
4485
  return handle;
3853
4486
  }
3854
- var import_sandbox_runtime, import_promises, import_node_fs5, import_node_path4, import_node_child_process3, import_node_util2, import_bash_tool, import_ai2, import_zod4, import_crypto_js4, getAllExuluVariables, execAsync2, EXEC_MAX_BUFFER, sandboxProbePromise, SANDBOX_FALLBACK_INSTRUCTIONS, degradedModeLogged, sandboxCache;
4487
+ var import_sandbox_runtime, import_promises, import_node_fs5, import_node_path4, import_node_child_process3, import_node_util2, import_bash_tool, import_ai2, import_zod6, import_crypto_js4, getAllExuluVariables, execAsync2, EXEC_MAX_BUFFER, sandboxProbePromise, SANDBOX_FALLBACK_INSTRUCTIONS, degradedModeLogged, sandboxCache;
3855
4488
  var init_create_sandbox = __esm({
3856
4489
  "ee/invoke-skills/create-sandbox.ts"() {
3857
4490
  "use strict";
@@ -3866,7 +4499,7 @@ var init_create_sandbox = __esm({
3866
4499
  init_system_dependencies();
3867
4500
  import_bash_tool = require("bash-tool");
3868
4501
  import_ai2 = require("ai");
3869
- import_zod4 = require("zod");
4502
+ import_zod6 = require("zod");
3870
4503
  init_variable();
3871
4504
  import_crypto_js4 = __toESM(require("crypto-js"), 1);
3872
4505
  init_client();
@@ -4101,13 +4734,47 @@ ${notice}`;
4101
4734
  }
4102
4735
  });
4103
4736
 
4737
+ // src/exulu/auth/scrub-text.ts
4738
+ var credentialScrubText, SCRUBBED_CREDENTIAL_TEXT, SCRUBBED_OAUTH_TEXT;
4739
+ var init_scrub_text = __esm({
4740
+ "src/exulu/auth/scrub-text.ts"() {
4741
+ "use strict";
4742
+ init_cjs_shims();
4743
+ credentialScrubText = (provider, fieldLabels) => `A secure credential form for provider "${provider}"` + (fieldLabels.length ? ` (fields: ${fieldLabels.join(", ")})` : "") + ` is shown to the user in the chat UI. Never ask for these values in chat. After the user confirms saving, call the tool again.`;
4744
+ SCRUBBED_CREDENTIAL_TEXT = "A secure credential form was shown to the user in the chat UI. Never ask for credential values in chat. After the user confirms saving, call the tool again.";
4745
+ SCRUBBED_OAUTH_TEXT = "Authorization is required. A Connect button was shown to the user in the chat UI. Do not relay any URL in chat. After the user confirms connecting, call the tool again.";
4746
+ }
4747
+ });
4748
+
4749
+ // src/templates/tools/auth-tool-model-output.ts
4750
+ var buildAuthToolModelOutput;
4751
+ var init_auth_tool_model_output = __esm({
4752
+ "src/templates/tools/auth-tool-model-output.ts"() {
4753
+ "use strict";
4754
+ init_cjs_shims();
4755
+ init_scrub_text();
4756
+ buildAuthToolModelOutput = (tool4) => ({ output }) => {
4757
+ if (output && typeof output === "object" && output.credentialRequest) {
4758
+ const auth = tool4.authentication;
4759
+ const labels = auth?.authType === "user_credentials" ? auth.fields.map((f) => f.label) : [];
4760
+ const provider = output.credentialRequest.provider ?? (auth && "provider" in auth ? auth.provider : "unknown");
4761
+ return { type: "text", value: credentialScrubText(provider, labels) };
4762
+ }
4763
+ if (output && typeof output === "object" && output.oauth?.authorizationUrl) {
4764
+ return { type: "text", value: SCRUBBED_OAUTH_TEXT };
4765
+ }
4766
+ return { type: "json", value: output ?? null };
4767
+ };
4768
+ }
4769
+ });
4770
+
4104
4771
  // src/templates/tools/session-file-read-tool.ts
4105
- var import_zod5, DEFAULT_LIMIT, MAX_CONTENT_CHARS, createSessionFileReadTool;
4772
+ var import_zod7, DEFAULT_LIMIT, MAX_CONTENT_CHARS, createSessionFileReadTool;
4106
4773
  var init_session_file_read_tool = __esm({
4107
4774
  "src/templates/tools/session-file-read-tool.ts"() {
4108
4775
  "use strict";
4109
4776
  init_cjs_shims();
4110
- import_zod5 = require("zod");
4777
+ import_zod7 = require("zod");
4111
4778
  init_tool();
4112
4779
  init_uppy();
4113
4780
  DEFAULT_LIMIT = 250;
@@ -4161,10 +4828,10 @@ var init_session_file_read_tool = __esm({
4161
4828
  name: "read_session_file",
4162
4829
  needsApproval: false,
4163
4830
  description: "Read a line range from a file stored in this session's files \u2014 including offloaded tool outputs (tool-output-*.txt) and uploaded documents. Use offset (1-based line number) and limit to page through large files instead of reading everything at once.",
4164
- inputSchema: import_zod5.z.object({
4165
- filename: import_zod5.z.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
4166
- offset: import_zod5.z.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
4167
- limit: import_zod5.z.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT})`)
4831
+ inputSchema: import_zod7.z.object({
4832
+ filename: import_zod7.z.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
4833
+ offset: import_zod7.z.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
4834
+ limit: import_zod7.z.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT})`)
4168
4835
  }),
4169
4836
  type: "function",
4170
4837
  category: "session",
@@ -4231,12 +4898,12 @@ var init_document_render_helpers = __esm({
4231
4898
  });
4232
4899
 
4233
4900
  // src/templates/tools/parse-document-tool.ts
4234
- var import_zod6, import_node_path6, import_officeparser, DEFAULT_LIMIT2, MAX_CONTENT_CHARS2, MIN_CHARS_PER_PAGE, OFFICE_EXTENSIONS, pagesPattern, createParseDocumentTool;
4901
+ var import_zod8, import_node_path6, import_officeparser, DEFAULT_LIMIT2, MAX_CONTENT_CHARS2, MIN_CHARS_PER_PAGE, OFFICE_EXTENSIONS, pagesPattern, createParseDocumentTool;
4235
4902
  var init_parse_document_tool = __esm({
4236
4903
  "src/templates/tools/parse-document-tool.ts"() {
4237
4904
  "use strict";
4238
4905
  init_cjs_shims();
4239
- import_zod6 = require("zod");
4906
+ import_zod8 = require("zod");
4240
4907
  import_node_path6 = require("path");
4241
4908
  import_officeparser = require("officeparser");
4242
4909
  init_tool();
@@ -4355,11 +5022,11 @@ ${text.trim()}`).join("\n");
4355
5022
  name: "parse_document",
4356
5023
  needsApproval: false,
4357
5024
  description: `Extract the text of an uploaded PDF or Office document from this session's files, with "--- page N ---" markers for PDFs so you can locate content by page. Free and fast (no OCR): works only on documents with a real text layer. To SEE a page or an image inside a document, use view_document_page.`,
4358
- inputSchema: import_zod6.z.object({
4359
- filename: import_zod6.z.string().describe('Exact session file name, e.g. "report.pdf"'),
4360
- pages: import_zod6.z.string().optional().describe('PDF page or range to extract, e.g. "2" or "1-5" (default: all pages) (PDF only)'),
4361
- offset: import_zod6.z.number().int().min(1).optional().describe("1-based first output line to read (default 1)"),
4362
- limit: import_zod6.z.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT2})`)
5025
+ inputSchema: import_zod8.z.object({
5026
+ filename: import_zod8.z.string().describe('Exact session file name, e.g. "report.pdf"'),
5027
+ pages: import_zod8.z.string().optional().describe('PDF page or range to extract, e.g. "2" or "1-5" (default: all pages) (PDF only)'),
5028
+ offset: import_zod8.z.number().int().min(1).optional().describe("1-based first output line to read (default 1)"),
5029
+ limit: import_zod8.z.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT2})`)
4363
5030
  }),
4364
5031
  type: "function",
4365
5032
  category: "session",
@@ -4622,12 +5289,12 @@ var init_tool_image_attachments = __esm({
4622
5289
  });
4623
5290
 
4624
5291
  // src/templates/tools/view-document-page-tool.ts
4625
- var import_zod7, import_node_path8, MAX_IMAGE_BYTES, SCALE_PRIMARY, SCALE_FALLBACK, IMAGE_MEDIA_TYPES, OFFICE_EXTENSIONS2, createViewDocumentPageTool;
5292
+ var import_zod9, import_node_path8, MAX_IMAGE_BYTES, SCALE_PRIMARY, SCALE_FALLBACK, IMAGE_MEDIA_TYPES, OFFICE_EXTENSIONS2, createViewDocumentPageTool;
4626
5293
  var init_view_document_page_tool = __esm({
4627
5294
  "src/templates/tools/view-document-page-tool.ts"() {
4628
5295
  "use strict";
4629
5296
  init_cjs_shims();
4630
- import_zod7 = require("zod");
5297
+ import_zod9 = require("zod");
4631
5298
  import_node_path8 = require("path");
4632
5299
  init_tool();
4633
5300
  init_uppy();
@@ -4753,9 +5420,9 @@ var init_view_document_page_tool = __esm({
4753
5420
  name: "view_document_page",
4754
5421
  needsApproval: false,
4755
5422
  description: "LOOK at a page of an uploaded PDF/Office document, or at an uploaded image, from this session's files. The rendered image is attached as a user message directly after this tool result so you can visually analyze photos, charts, scans, and layouts. Use parse_document first to find which page you need. Requires a vision-capable model.",
4756
- inputSchema: import_zod7.z.object({
4757
- filename: import_zod7.z.string().describe('Exact session file name, e.g. "report.pdf" or "screenshot.png"'),
4758
- page: import_zod7.z.number().int().min(1).optional().describe("Page number to render (default 1; ignored for image files)")
5423
+ inputSchema: import_zod9.z.object({
5424
+ filename: import_zod9.z.string().describe('Exact session file name, e.g. "report.pdf" or "screenshot.png"'),
5425
+ page: import_zod9.z.number().int().min(1).optional().describe("Page number to render (default 1; ignored for image files)")
4759
5426
  }),
4760
5427
  type: "function",
4761
5428
  category: "session",
@@ -4790,10 +5457,12 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4790
5457
  import_node_crypto4 = require("crypto");
4791
5458
  init_statistics();
4792
5459
  init_memory_tool();
5460
+ init_context_write_tools();
4793
5461
  init_create_sandbox();
4794
5462
  init_uppy();
4795
5463
  init_truncate_tool_output();
4796
5464
  init_tool_output_offload();
5465
+ init_auth_tool_model_output();
4797
5466
  init_session_file_read_tool();
4798
5467
  init_parse_document_tool();
4799
5468
  init_view_document_page_tool();
@@ -4961,6 +5630,11 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4961
5630
  currentTools.push(createNewMemoryTool);
4962
5631
  }
4963
5632
  }
5633
+ for (const kbWriteTool of collectKbWriteTools(agent, contexts)) {
5634
+ if (!disabled.has(kbWriteTool.id)) {
5635
+ currentTools.push(kbWriteTool);
5636
+ }
5637
+ }
4964
5638
  console.log("[EXULU] Convert tools array to object, session items", sessionItems);
4965
5639
  if (sessionItems) {
4966
5640
  const sessionItemsRetrievalTool = await createSessionItemsRetrievalTool({
@@ -5113,6 +5787,10 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
5113
5787
  // Vercel AI SDK uses the sanitized tool name as the key, so this matches.
5114
5788
  needsApproval: approvedTools?.includes("tool-" + cur.name) || !cur.needsApproval ? false : true,
5115
5789
  // todo make configurable
5790
+ // Auth-wrapped tools: the model sees scrub text instead of the
5791
+ // credentialRequest/oauth payload; the UI stream keeps the raw
5792
+ // output (spec 2026-07-22 §1.2).
5793
+ ...cur.authentication ? { toModelOutput: buildAuthToolModelOutput(cur) } : {},
5116
5794
  async *execute(inputs, options) {
5117
5795
  console.log(
5118
5796
  "[EXULU] Executing tool",
@@ -5252,13 +5930,13 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
5252
5930
  });
5253
5931
 
5254
5932
  // src/exulu/tool.ts
5255
- var import_ai3, import_zod8, import_node_crypto5, PUBLIC_TOOL_TYPES, ExuluTool;
5933
+ var import_ai3, import_zod10, import_node_crypto5, PUBLIC_TOOL_TYPES, ExuluTool;
5256
5934
  var init_tool = __esm({
5257
5935
  "src/exulu/tool.ts"() {
5258
5936
  "use strict";
5259
5937
  init_cjs_shims();
5260
5938
  import_ai3 = require("ai");
5261
- import_zod8 = require("zod");
5939
+ import_zod10 = require("zod");
5262
5940
  init_sanitize_name();
5263
5941
  import_node_crypto5 = require("crypto");
5264
5942
  init_singleton();
@@ -5279,7 +5957,7 @@ var init_tool = __esm({
5279
5957
  type;
5280
5958
  tool;
5281
5959
  needsApproval;
5282
- oauth;
5960
+ authentication;
5283
5961
  config;
5284
5962
  constructor({
5285
5963
  id,
@@ -5291,7 +5969,7 @@ var init_tool = __esm({
5291
5969
  execute: execute2,
5292
5970
  config,
5293
5971
  needsApproval,
5294
- oauth
5972
+ authentication: authentication2
5295
5973
  }) {
5296
5974
  if (!PUBLIC_TOOL_TYPES.includes(type)) {
5297
5975
  throw new Error(
@@ -5300,11 +5978,11 @@ var init_tool = __esm({
5300
5978
  )}. The "agent" and "context" types are managed by Exulu internally and cannot be set on a tool.`
5301
5979
  );
5302
5980
  }
5303
- if (oauth) {
5304
- validateOauthConfig(id, oauth);
5305
- oauthRegistry.register(id, oauth);
5981
+ if (authentication2) {
5982
+ validateAuthConfig(id, authentication2);
5983
+ authRegistry.register(id, authentication2);
5306
5984
  }
5307
- this.oauth = oauth;
5985
+ this.authentication = authentication2;
5308
5986
  this.id = id;
5309
5987
  this.config = config;
5310
5988
  this.needsApproval = needsApproval ?? true;
@@ -5315,8 +5993,8 @@ var init_tool = __esm({
5315
5993
  this.type = type;
5316
5994
  this.tool = (0, import_ai3.tool)({
5317
5995
  description,
5318
- inputSchema: inputSchema || import_zod8.z.object({}),
5319
- execute: oauth ? wrapExecuteWithOauth(id, oauth, execute2) : execute2
5996
+ inputSchema: inputSchema || import_zod10.z.object({}),
5997
+ execute: authentication2 ? wrapExecuteWithAuth(id, authentication2, execute2) : execute2
5320
5998
  });
5321
5999
  }
5322
6000
  /**
@@ -5608,62 +6286,62 @@ function effectiveKbSettings(profile, ctx) {
5608
6286
  keywordPrefilter: preset.keywordPrefilter
5609
6287
  };
5610
6288
  }
5611
- var import_zod9, KB_KINDS, DEFAULT_PREFILTER_CUTOFF, RRF_K, CHUNK_GROUP_MAX, kbProfileSchema, knowledgeBasesSchema, routingRuleSchema, routingSchema, identifierSetSchema, vocabularySchema, memorySchema, tuningSchema, boolVal, strVal, KIND_PRESETS;
6289
+ var import_zod11, KB_KINDS, DEFAULT_PREFILTER_CUTOFF, RRF_K, CHUNK_GROUP_MAX, kbProfileSchema, knowledgeBasesSchema, routingRuleSchema, routingSchema, identifierSetSchema, vocabularySchema, memorySchema, tuningSchema, boolVal, strVal, KIND_PRESETS;
5612
6290
  var init_config = __esm({
5613
6291
  "ee/agentic-retrieval/pipeline/config.ts"() {
5614
6292
  "use strict";
5615
6293
  init_cjs_shims();
5616
- import_zod9 = require("zod");
6294
+ import_zod11 = require("zod");
5617
6295
  KB_KINDS = ["documents", "conversations", "records"];
5618
6296
  DEFAULT_PREFILTER_CUTOFF = 2.5;
5619
6297
  RRF_K = 60;
5620
6298
  CHUNK_GROUP_MAX = 10;
5621
- kbProfileSchema = import_zod9.z.object({
5622
- enabled: import_zod9.z.boolean().default(true),
5623
- kind: import_zod9.z.enum(KB_KINDS).default("documents"),
5624
- instructions: import_zod9.z.string().default(""),
5625
- overrides: import_zod9.z.object({
5626
- limit: import_zod9.z.number().int().positive().optional(),
5627
- expand: import_zod9.z.number().int().min(0).optional(),
5628
- multiQuery: import_zod9.z.boolean().optional(),
5629
- hyde: import_zod9.z.boolean().optional()
6299
+ kbProfileSchema = import_zod11.z.object({
6300
+ enabled: import_zod11.z.boolean().default(true),
6301
+ kind: import_zod11.z.enum(KB_KINDS).default("documents"),
6302
+ instructions: import_zod11.z.string().default(""),
6303
+ overrides: import_zod11.z.object({
6304
+ limit: import_zod11.z.number().int().positive().optional(),
6305
+ expand: import_zod11.z.number().int().min(0).optional(),
6306
+ multiQuery: import_zod11.z.boolean().optional(),
6307
+ hyde: import_zod11.z.boolean().optional()
5630
6308
  }).default({})
5631
6309
  });
5632
- knowledgeBasesSchema = import_zod9.z.record(import_zod9.z.string(), kbProfileSchema);
5633
- routingRuleSchema = import_zod9.z.object({
5634
- id: import_zod9.z.string(),
5635
- label: import_zod9.z.string(),
5636
- description: import_zod9.z.string(),
5637
- main: import_zod9.z.array(import_zod9.z.string()),
5638
- fallback: import_zod9.z.array(import_zod9.z.string()).default([])
6310
+ knowledgeBasesSchema = import_zod11.z.record(import_zod11.z.string(), kbProfileSchema);
6311
+ routingRuleSchema = import_zod11.z.object({
6312
+ id: import_zod11.z.string(),
6313
+ label: import_zod11.z.string(),
6314
+ description: import_zod11.z.string(),
6315
+ main: import_zod11.z.array(import_zod11.z.string()),
6316
+ fallback: import_zod11.z.array(import_zod11.z.string()).default([])
5639
6317
  });
5640
- routingSchema = import_zod9.z.object({ rules: import_zod9.z.array(routingRuleSchema).default([]) });
5641
- identifierSetSchema = import_zod9.z.object({
5642
- name: import_zod9.z.string(),
5643
- description: import_zod9.z.string().default(""),
5644
- examples: import_zod9.z.array(import_zod9.z.string()).default([]),
5645
- strategy: import_zod9.z.enum(["fuzzy", "exact"]),
5646
- contexts: import_zod9.z.array(import_zod9.z.string()).default([])
6318
+ routingSchema = import_zod11.z.object({ rules: import_zod11.z.array(routingRuleSchema).default([]) });
6319
+ identifierSetSchema = import_zod11.z.object({
6320
+ name: import_zod11.z.string(),
6321
+ description: import_zod11.z.string().default(""),
6322
+ examples: import_zod11.z.array(import_zod11.z.string()).default([]),
6323
+ strategy: import_zod11.z.enum(["fuzzy", "exact"]),
6324
+ contexts: import_zod11.z.array(import_zod11.z.string()).default([])
5647
6325
  });
5648
- vocabularySchema = import_zod9.z.object({
5649
- glossary: import_zod9.z.array(import_zod9.z.object({ term: import_zod9.z.string(), meaning: import_zod9.z.string() })).default([]),
5650
- identifiers: import_zod9.z.array(identifierSetSchema).default([]),
5651
- rewrites: import_zod9.z.array(import_zod9.z.object({ find: import_zod9.z.string(), replace: import_zod9.z.string() })).default([]),
5652
- styleHint: import_zod9.z.string().default("")
6326
+ vocabularySchema = import_zod11.z.object({
6327
+ glossary: import_zod11.z.array(import_zod11.z.object({ term: import_zod11.z.string(), meaning: import_zod11.z.string() })).default([]),
6328
+ identifiers: import_zod11.z.array(identifierSetSchema).default([]),
6329
+ rewrites: import_zod11.z.array(import_zod11.z.object({ find: import_zod11.z.string(), replace: import_zod11.z.string() })).default([]),
6330
+ styleHint: import_zod11.z.string().default("")
5653
6331
  });
5654
- memorySchema = import_zod9.z.object({
5655
- enabled: import_zod9.z.boolean().default(true),
5656
- override: import_zod9.z.boolean().default(false),
5657
- filePrioritization: import_zod9.z.boolean().default(false),
5658
- queryAugmentation: import_zod9.z.boolean().default(true)
6332
+ memorySchema = import_zod11.z.object({
6333
+ enabled: import_zod11.z.boolean().default(true),
6334
+ override: import_zod11.z.boolean().default(false),
6335
+ filePrioritization: import_zod11.z.boolean().default(false),
6336
+ queryAugmentation: import_zod11.z.boolean().default(true)
5659
6337
  });
5660
- tuningSchema = import_zod9.z.object({
5661
- topK: import_zod9.z.number().int().positive().default(5),
5662
- fallbackThreshold: import_zod9.z.number().min(0).max(1).default(0.95),
5663
- pinBoost: import_zod9.z.number().min(0).max(1).default(0.15),
5664
- identifierBoost: import_zod9.z.number().min(0).max(1).default(0.15),
5665
- pageWindow: import_zod9.z.number().int().min(0).default(1),
5666
- maxQueriesPerContext: import_zod9.z.number().int().positive().default(5)
6338
+ tuningSchema = import_zod11.z.object({
6339
+ topK: import_zod11.z.number().int().positive().default(5),
6340
+ fallbackThreshold: import_zod11.z.number().min(0).max(1).default(0.95),
6341
+ pinBoost: import_zod11.z.number().min(0).max(1).default(0.15),
6342
+ identifierBoost: import_zod11.z.number().min(0).max(1).default(0.15),
6343
+ pageWindow: import_zod11.z.number().int().min(0).default(1),
6344
+ maxQueriesPerContext: import_zod11.z.number().int().positive().default(5)
5667
6345
  });
5668
6346
  boolVal = (v) => v === true || v === "true" || v === 1;
5669
6347
  strVal = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
@@ -5676,7 +6354,8 @@ var init_config = __esm({
5676
6354
  });
5677
6355
 
5678
6356
  // src/utils/with-retry.ts
5679
- async function withRetry(generateFn, maxRetries = 3) {
6357
+ async function withRetry(generateFn, maxRetries = 3, opts = {}) {
6358
+ const { shouldRetry, baseDelayMs = 1e3 } = opts;
5680
6359
  let lastError;
5681
6360
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
5682
6361
  try {
@@ -5684,10 +6363,10 @@ async function withRetry(generateFn, maxRetries = 3) {
5684
6363
  } catch (error) {
5685
6364
  lastError = error;
5686
6365
  console.error(`[EXULU] generateText attempt ${attempt} failed:`, error);
5687
- if (attempt === maxRetries) {
6366
+ if (attempt === maxRetries || shouldRetry && !shouldRetry(error)) {
5688
6367
  throw error;
5689
6368
  }
5690
- await new Promise((resolve8) => setTimeout(resolve8, Math.pow(2, attempt) * 1e3));
6369
+ await new Promise((resolve8) => setTimeout(resolve8, Math.pow(2, attempt) * baseDelayMs));
5691
6370
  }
5692
6371
  }
5693
6372
  throw lastError;
@@ -5699,6 +6378,64 @@ var init_with_retry = __esm({
5699
6378
  }
5700
6379
  });
5701
6380
 
6381
+ // ee/agentic-retrieval/pipeline/micro-call.ts
6382
+ function microCallProviderOptions(model) {
6383
+ const modelId = typeof model === "string" ? model : model?.modelId;
6384
+ return typeof modelId === "string" && /gemini/i.test(modelId) ? { litellm: { reasoningEffort: "disable" } } : void 0;
6385
+ }
6386
+ async function microCall(args) {
6387
+ const {
6388
+ model,
6389
+ system,
6390
+ prompt,
6391
+ messages,
6392
+ schema,
6393
+ temperature = 0,
6394
+ maxOutputTokens = MICRO_CALL_MAX_OUTPUT_TOKENS,
6395
+ maxAttempts = 3,
6396
+ retryBaseDelayMs
6397
+ } = args;
6398
+ return withRetry(
6399
+ async () => {
6400
+ const result = await (0, import_ai4.generateText)({
6401
+ model,
6402
+ temperature,
6403
+ system,
6404
+ prompt,
6405
+ messages,
6406
+ ...schema ? { output: import_ai4.Output.object({ schema }) } : {},
6407
+ maxOutputTokens,
6408
+ // withRetry owns retries. The SDK's internal retries on top of it
6409
+ // tripled request volume per attempt while the provider was already
6410
+ // rate-limiting.
6411
+ maxRetries: 0,
6412
+ providerOptions: microCallProviderOptions(model)
6413
+ });
6414
+ return {
6415
+ output: schema ? result.output : void 0,
6416
+ text: result.text
6417
+ };
6418
+ },
6419
+ maxAttempts,
6420
+ {
6421
+ // Empty output is deterministic for identical params — retrying only
6422
+ // added latency before the degraded path. Fail fast instead.
6423
+ shouldRetry: (error) => !import_ai4.NoOutputGeneratedError.isInstance(error),
6424
+ ...retryBaseDelayMs !== void 0 ? { baseDelayMs: retryBaseDelayMs } : {}
6425
+ }
6426
+ );
6427
+ }
6428
+ var import_ai4, MICRO_CALL_MAX_OUTPUT_TOKENS;
6429
+ var init_micro_call = __esm({
6430
+ "ee/agentic-retrieval/pipeline/micro-call.ts"() {
6431
+ "use strict";
6432
+ init_cjs_shims();
6433
+ import_ai4 = require("ai");
6434
+ init_with_retry();
6435
+ MICRO_CALL_MAX_OUTPUT_TOKENS = 2e3;
6436
+ }
6437
+ });
6438
+
5702
6439
  // ee/agentic-retrieval/pipeline/text-utils.ts
5703
6440
  function extractIdentifierTokens(parts) {
5704
6441
  const tokens = /* @__PURE__ */ new Set();
@@ -5903,22 +6640,15 @@ async function resolveIdentifierPins({
5903
6640
  identifierSets.map(async (set) => {
5904
6641
  if (!set.contexts.length) return;
5905
6642
  try {
5906
- const { output } = await withRetry(
5907
- () => (0, import_ai4.generateText)({
5908
- model,
5909
- temperature: 0,
5910
- system: set.strategy === "exact" ? EXACT_EXTRACTION_PROMPT(set) : FUZZY_EXTRACTION_PROMPT(set),
5911
- messages: [{ role: "user", content: question }],
5912
- output: import_ai4.Output.object({
5913
- schema: import_zod10.z.object({
5914
- hasMatches: import_zod10.z.boolean(),
5915
- matches: import_zod10.z.array(import_zod10.z.string()).optional()
5916
- })
5917
- }),
5918
- maxOutputTokens: 300
5919
- }),
5920
- 3
5921
- );
6643
+ const { output } = await microCall({
6644
+ model,
6645
+ system: set.strategy === "exact" ? EXACT_EXTRACTION_PROMPT(set) : FUZZY_EXTRACTION_PROMPT(set),
6646
+ messages: [{ role: "user", content: question }],
6647
+ schema: import_zod12.z.object({
6648
+ hasMatches: import_zod12.z.boolean(),
6649
+ matches: import_zod12.z.array(import_zod12.z.string()).optional()
6650
+ })
6651
+ });
5922
6652
  if (!output?.hasMatches || !output.matches?.length) return;
5923
6653
  steps.push({ text: `Detected ${set.name} in the question: ${output.matches.join(", ")}` });
5924
6654
  await Promise.all(
@@ -5960,15 +6690,14 @@ async function resolveIdentifierPins({
5960
6690
  );
5961
6691
  return { pinsByContext, exactPinsByContext, steps };
5962
6692
  }
5963
- var import_fuse, import_ai4, import_zod10, itemCaches, ensureItemsCache, FUZZY_EXTRACTION_PROMPT, EXACT_EXTRACTION_PROMPT;
6693
+ var import_fuse, import_zod12, itemCaches, ensureItemsCache, FUZZY_EXTRACTION_PROMPT, EXACT_EXTRACTION_PROMPT;
5964
6694
  var init_prefilter = __esm({
5965
6695
  "ee/agentic-retrieval/pipeline/prefilter.ts"() {
5966
6696
  "use strict";
5967
6697
  init_cjs_shims();
5968
6698
  import_fuse = __toESM(require("fuse.js"), 1);
5969
- import_ai4 = require("ai");
5970
- import_zod10 = require("zod");
5971
- init_with_retry();
6699
+ import_zod12 = require("zod");
6700
+ init_micro_call();
5972
6701
  init_text_utils();
5973
6702
  init_config();
5974
6703
  itemCaches = {};
@@ -6045,24 +6774,17 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
6045
6774
  const [docPageRaw, explicitKBRaw] = await Promise.all([
6046
6775
  (async () => {
6047
6776
  try {
6048
- return await withRetry(
6049
- () => (0, import_ai5.generateText)({
6050
- model,
6051
- temperature: 0,
6052
- system: buildDocPagePrompt(knownIdentifiers),
6053
- messages: [{ role: "user", content: question }],
6054
- output: import_ai5.Output.object({
6055
- schema: import_zod11.z.object({
6056
- hasFilenameHint: import_zod11.z.boolean(),
6057
- filenameHints: import_zod11.z.array(import_zod11.z.string()).optional(),
6058
- hasPageHint: import_zod11.z.boolean(),
6059
- pageNumber: import_zod11.z.number().int().nullable().optional()
6060
- })
6061
- }),
6062
- maxOutputTokens: 300
6063
- }),
6064
- 3
6065
- );
6777
+ return await microCall({
6778
+ model,
6779
+ system: buildDocPagePrompt(knownIdentifiers),
6780
+ messages: [{ role: "user", content: question }],
6781
+ schema: import_zod13.z.object({
6782
+ hasFilenameHint: import_zod13.z.boolean(),
6783
+ filenameHints: import_zod13.z.array(import_zod13.z.string()).optional(),
6784
+ hasPageHint: import_zod13.z.boolean(),
6785
+ pageNumber: import_zod13.z.number().int().nullable().optional()
6786
+ })
6787
+ });
6066
6788
  } catch (err) {
6067
6789
  steps.push({ text: "Doc/page detection failed \u2014 skipping filename and page hints." });
6068
6790
  return {
@@ -6077,23 +6799,16 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
6077
6799
  })(),
6078
6800
  (async () => {
6079
6801
  try {
6080
- return await withRetry(
6081
- () => (0, import_ai5.generateText)({
6082
- model,
6083
- temperature: 0,
6084
- system: kbSystemPrompt,
6085
- output: import_ai5.Output.object({
6086
- schema: import_zod11.z.object({
6087
- explicitlyRequestedKnowledgeBases: import_zod11.z.array(
6088
- import_zod11.z.enum(enabledContexts.map((c) => c.id))
6089
- )
6090
- })
6091
- }),
6092
- messages: [{ role: "user", content: question }],
6093
- maxOutputTokens: 200
6802
+ return await microCall({
6803
+ model,
6804
+ system: kbSystemPrompt,
6805
+ schema: import_zod13.z.object({
6806
+ explicitlyRequestedKnowledgeBases: import_zod13.z.array(
6807
+ import_zod13.z.enum(enabledContexts.map((c) => c.id))
6808
+ )
6094
6809
  }),
6095
- 3
6096
- );
6810
+ messages: [{ role: "user", content: question }]
6811
+ });
6097
6812
  } catch (err) {
6098
6813
  return { output: { explicitlyRequestedKnowledgeBases: [] } };
6099
6814
  }
@@ -6176,22 +6891,15 @@ ${extraInstructions}
6176
6891
  </instructions>`;
6177
6892
  }
6178
6893
  try {
6179
- const { output: classified } = await withRetry(
6180
- () => (0, import_ai5.generateText)({
6181
- model,
6182
- temperature: 0,
6183
- system: classifyPrompt,
6184
- messages: [{ role: "user", content: question }],
6185
- output: import_ai5.Output.object({
6186
- schema: import_zod11.z.object({
6187
- ruleId: import_zod11.z.enum(ruleIds),
6188
- reason: import_zod11.z.string()
6189
- })
6190
- }),
6191
- maxOutputTokens: 200
6192
- }),
6193
- 3
6194
- );
6894
+ const { output: classified } = await microCall({
6895
+ model,
6896
+ system: classifyPrompt,
6897
+ messages: [{ role: "user", content: question }],
6898
+ schema: import_zod13.z.object({
6899
+ ruleId: import_zod13.z.enum(ruleIds),
6900
+ reason: import_zod13.z.string()
6901
+ })
6902
+ });
6195
6903
  const matchedRule = routingRules.find((r) => r.id === classified.ruleId);
6196
6904
  if (matchedRule) {
6197
6905
  const main = matchedRule.main.filter((id) => enabledIds.has(id));
@@ -6244,14 +6952,13 @@ ${extraInstructions}
6244
6952
  };
6245
6953
  }
6246
6954
  }
6247
- var import_ai5, import_zod11, MAX_USER_PIN_MATCHES, buildDocPagePrompt;
6955
+ var import_zod13, MAX_USER_PIN_MATCHES, buildDocPagePrompt;
6248
6956
  var init_routing = __esm({
6249
6957
  "ee/agentic-retrieval/pipeline/routing.ts"() {
6250
6958
  "use strict";
6251
6959
  init_cjs_shims();
6252
- import_ai5 = require("ai");
6253
- import_zod11 = require("zod");
6254
- init_with_retry();
6960
+ import_zod13 = require("zod");
6961
+ init_micro_call();
6255
6962
  init_prefilter();
6256
6963
  init_text_utils();
6257
6964
  MAX_USER_PIN_MATCHES = 8;
@@ -6506,32 +7213,25 @@ async function runMemoryPhase({
6506
7213
  `;
6507
7214
  let relevantMemoryChunks = [];
6508
7215
  try {
6509
- const { output: output_relevant_memory } = await withRetry(
6510
- () => (0, import_ai6.generateText)({
6511
- model,
6512
- temperature: 0,
6513
- system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
6514
- messages: [
6515
- {
6516
- role: "user",
6517
- content: `
7216
+ const { output: output_relevant_memory } = await microCall({
7217
+ model,
7218
+ system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
7219
+ messages: [
7220
+ {
7221
+ role: "user",
7222
+ content: `
6518
7223
  <user_question>${question}</user_question>
6519
7224
  <relevant_keywords>${keywords.join(", ")}</relevant_keywords>
6520
7225
  <important_keyword>${importantKeyword}</important_keyword>
6521
7226
  `
6522
- }
6523
- ],
6524
- output: import_ai6.Output.object({
6525
- schema: import_zod12.z.object({
6526
- relevantChunkIds: import_zod12.z.array(import_zod12.z.string()).describe(
6527
- "The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
6528
- )
6529
- })
6530
- }),
6531
- maxOutputTokens: 400
6532
- }),
6533
- 3
6534
- );
7227
+ }
7228
+ ],
7229
+ schema: import_zod14.z.object({
7230
+ relevantChunkIds: import_zod14.z.array(import_zod14.z.string()).describe(
7231
+ "The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
7232
+ )
7233
+ })
7234
+ });
6535
7235
  const ids = new Set(output_relevant_memory?.relevantChunkIds ?? []);
6536
7236
  relevantMemoryChunks = ids.size === 0 ? [] : retrieved_memory.filter((c) => ids.has(c.chunk_id));
6537
7237
  } catch (e) {
@@ -6623,41 +7323,34 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
6623
7323
  `;
6624
7324
  const [overrideResult, fileResult, queryResult] = await Promise.all([
6625
7325
  // Override check: strict gate to decide if memory should be authoritative
6626
- memoryConfig.override ? withRetry(
6627
- () => (0, import_ai6.generateText)({
6628
- model,
6629
- temperature: 0,
6630
- system: CHECK_MEMORY_OVERRIDE,
6631
- messages: [
6632
- {
6633
- role: "user",
6634
- content: `
7326
+ memoryConfig.override ? microCall({
7327
+ model,
7328
+ system: CHECK_MEMORY_OVERRIDE,
7329
+ messages: [
7330
+ {
7331
+ role: "user",
7332
+ content: `
6635
7333
  <user_question>${question}</user_question>
6636
7334
  <relevant_keywords>${keywords.join(", ")}</relevant_keywords>
6637
7335
  <important_keyword>${importantKeyword}</important_keyword>
6638
7336
  `
6639
- }
6640
- ],
6641
- output: import_ai6.Output.object({
6642
- schema: import_zod12.z.object({
6643
- overrides: import_zod12.z.boolean().describe(
6644
- "True ONLY if a memory chunk directly and sufficiently answers the user's question and should be authoritative over the documents. Be strict; when unsure, false."
6645
- ),
6646
- confidence: import_zod12.z.enum(["high", "medium", "low"]).describe(
6647
- "Confidence that the selected memory chunk(s) fully and directly answer the question."
6648
- ),
6649
- authoritativeChunkIds: import_zod12.z.array(import_zod12.z.string()).describe(
6650
- "The chunk_ids of the memory chunk(s) that directly answer the question. Empty if overrides is false."
6651
- ),
6652
- reason: import_zod12.z.string().describe(
6653
- "One short sentence: why this memory does or does not directly answer the question."
6654
- )
6655
- })
6656
- }),
6657
- maxOutputTokens: 300
6658
- }),
6659
- 3
6660
- ).catch(() => ({
7337
+ }
7338
+ ],
7339
+ schema: import_zod14.z.object({
7340
+ overrides: import_zod14.z.boolean().describe(
7341
+ "True ONLY if a memory chunk directly and sufficiently answers the user's question and should be authoritative over the documents. Be strict; when unsure, false."
7342
+ ),
7343
+ confidence: import_zod14.z.enum(["high", "medium", "low"]).describe(
7344
+ "Confidence that the selected memory chunk(s) fully and directly answer the question."
7345
+ ),
7346
+ authoritativeChunkIds: import_zod14.z.array(import_zod14.z.string()).describe(
7347
+ "The chunk_ids of the memory chunk(s) that directly answer the question. Empty if overrides is false."
7348
+ ),
7349
+ reason: import_zod14.z.string().describe(
7350
+ "One short sentence: why this memory does or does not directly answer the question."
7351
+ )
7352
+ })
7353
+ }).catch(() => ({
6661
7354
  output: {
6662
7355
  overrides: false,
6663
7356
  confidence: "low",
@@ -6673,44 +7366,30 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
6673
7366
  }
6674
7367
  }),
6675
7368
  // File prioritization: detect explicit document-pinning instructions in memory
6676
- memoryConfig.filePrioritization ? withRetry(
6677
- () => (0, import_ai6.generateText)({
6678
- model,
6679
- temperature: 0,
6680
- system: "You are a helpful assistant that will strictly follow the user's instructions.",
6681
- messages: [{ role: "user", content: PROMPT_EXTRACT_PRIORITIZED_FILES }],
6682
- output: import_ai6.Output.object({
6683
- schema: import_zod12.z.object({
6684
- shouldPrioritizeFiles: import_zod12.z.boolean(),
6685
- fileNameHints: import_zod12.z.array(import_zod12.z.string()).optional()
6686
- })
6687
- }),
6688
- maxOutputTokens: 300
6689
- }),
6690
- 3
6691
- ).catch(() => ({
7369
+ memoryConfig.filePrioritization ? microCall({
7370
+ model,
7371
+ system: "You are a helpful assistant that will strictly follow the user's instructions.",
7372
+ messages: [{ role: "user", content: PROMPT_EXTRACT_PRIORITIZED_FILES }],
7373
+ schema: import_zod14.z.object({
7374
+ shouldPrioritizeFiles: import_zod14.z.boolean(),
7375
+ fileNameHints: import_zod14.z.array(import_zod14.z.string()).optional()
7376
+ })
7377
+ }).catch(() => ({
6692
7378
  output: { shouldPrioritizeFiles: false, fileNameHints: [] }
6693
7379
  })) : Promise.resolve({
6694
7380
  output: { shouldPrioritizeFiles: false, fileNameHints: [] }
6695
7381
  }),
6696
7382
  // Query augmentation: expand keywords with synonyms/abbreviations from memory
6697
- memoryConfig.queryAugmentation && hasAugmentationContent ? withRetry(
6698
- () => (0, import_ai6.generateText)({
6699
- model,
6700
- temperature: 0,
6701
- system: "You are a helpful assistant that will strictly follow the user's instructions.",
6702
- messages: [{ role: "user", content: QUERY_AUGMENTATION_PROMPT }],
6703
- output: import_ai6.Output.object({
6704
- schema: import_zod12.z.object({
6705
- updatedUserQuestion: import_zod12.z.string(),
6706
- updatedRelevantKeywords: import_zod12.z.array(import_zod12.z.string()),
6707
- updatedImportantKeyword: import_zod12.z.string()
6708
- })
6709
- }),
6710
- maxOutputTokens: 600
6711
- }),
6712
- 3
6713
- ).catch(() => ({
7383
+ memoryConfig.queryAugmentation && hasAugmentationContent ? microCall({
7384
+ model,
7385
+ system: "You are a helpful assistant that will strictly follow the user's instructions.",
7386
+ messages: [{ role: "user", content: QUERY_AUGMENTATION_PROMPT }],
7387
+ schema: import_zod14.z.object({
7388
+ updatedUserQuestion: import_zod14.z.string(),
7389
+ updatedRelevantKeywords: import_zod14.z.array(import_zod14.z.string()),
7390
+ updatedImportantKeyword: import_zod14.z.string()
7391
+ })
7392
+ }).catch(() => ({
6714
7393
  output: {
6715
7394
  updatedUserQuestion: question,
6716
7395
  updatedRelevantKeywords: [],
@@ -6790,14 +7469,13 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
6790
7469
  return neutralResult(question, keywords, importantKeyword);
6791
7470
  }
6792
7471
  }
6793
- var import_ai6, import_zod12, MEMORY_OVERRIDE_MIN_CONFIDENCE, MEMORY_SYNTHETIC_RERANK_SCORE, ITEM_CACHE_TTL_MS, memoryItemCache;
7472
+ var import_zod14, MEMORY_OVERRIDE_MIN_CONFIDENCE, MEMORY_SYNTHETIC_RERANK_SCORE, ITEM_CACHE_TTL_MS, memoryItemCache;
6794
7473
  var init_memory = __esm({
6795
7474
  "ee/agentic-retrieval/pipeline/memory.ts"() {
6796
7475
  "use strict";
6797
7476
  init_cjs_shims();
6798
- import_ai6 = require("ai");
6799
- import_zod12 = require("zod");
6800
- init_with_retry();
7477
+ import_zod14 = require("zod");
7478
+ init_micro_call();
6801
7479
  init_multi_query();
6802
7480
  init_prefilter();
6803
7481
  init_text_utils();
@@ -6875,21 +7553,21 @@ IMPORTANT:
6875
7553
  prompt += `
6876
7554
  Question: "${originalQuestion}"
6877
7555
  Relevant keywords: ${relevantKeywords.join(", ")}`;
6878
- const { text } = await (0, import_ai7.generateText)({
7556
+ const { text } = await microCall({
6879
7557
  model,
6880
7558
  prompt,
6881
7559
  temperature: 0.3,
6882
- maxOutputTokens: 500
7560
+ maxAttempts: 1
6883
7561
  });
6884
7562
  const passage = (text || "").trim();
6885
7563
  return passage.length > 0 ? passage : null;
6886
7564
  }
6887
- var import_ai7, hydeCache, HYDE_CACHE_MAX;
7565
+ var hydeCache, HYDE_CACHE_MAX;
6888
7566
  var init_hyde = __esm({
6889
7567
  "ee/agentic-retrieval/pipeline/hyde.ts"() {
6890
7568
  "use strict";
6891
7569
  init_cjs_shims();
6892
- import_ai7 = require("ai");
7570
+ init_micro_call();
6893
7571
  init_text_utils();
6894
7572
  hydeCache = /* @__PURE__ */ new Map();
6895
7573
  HYDE_CACHE_MAX = 200;
@@ -7292,11 +7970,11 @@ function createAgenticRetrievalTool(opts) {
7292
7970
  default: '{"topK":5,"fallbackThreshold":0.95,"pinBoost":0.15,"identifierBoost":0.15,"pageWindow":1,"maxQueriesPerContext":5}'
7293
7971
  }
7294
7972
  ],
7295
- inputSchema: import_zod13.z.object({
7296
- userQuery: import_zod13.z.string().describe("The original unaltered question from the user"),
7297
- relevantKeywords: import_zod13.z.array(import_zod13.z.string()).describe("Keywords extracted from the user's question relevant to the search"),
7298
- importantKeyword: import_zod13.z.string().describe("The single most important keyword from the user's question"),
7299
- confirmedContextIds: import_zod13.z.array(import_zod13.z.string()).optional().describe(
7973
+ inputSchema: import_zod15.z.object({
7974
+ userQuery: import_zod15.z.string().describe("The original unaltered question from the user"),
7975
+ relevantKeywords: import_zod15.z.array(import_zod15.z.string()).describe("Keywords extracted from the user's question relevant to the search"),
7976
+ importantKeyword: import_zod15.z.string().describe("The single most important keyword from the user's question"),
7977
+ confirmedContextIds: import_zod15.z.array(import_zod15.z.string()).optional().describe(
7300
7978
  "Knowledge base IDs explicitly confirmed by the user to be used in the retrieval. When present, only searches these contexts."
7301
7979
  )
7302
7980
  }),
@@ -7687,12 +8365,12 @@ Verified answer:
7687
8365
  }
7688
8366
  });
7689
8367
  }
7690
- var import_zod13;
8368
+ var import_zod15;
7691
8369
  var init_pipeline = __esm({
7692
8370
  "ee/agentic-retrieval/pipeline/index.ts"() {
7693
8371
  "use strict";
7694
8372
  init_cjs_shims();
7695
- import_zod13 = require("zod");
8373
+ import_zod15 = require("zod");
7696
8374
  init_tool();
7697
8375
  init_entitlements();
7698
8376
  init_resolve_reranker();
@@ -7713,6 +8391,7 @@ var init_pipeline = __esm({
7713
8391
  // src/index.ts
7714
8392
  var index_exports = {};
7715
8393
  __export(index_exports, {
8394
+ CredentialInvalidError: () => CredentialInvalidError,
7716
8395
  EXULU_JOB_STATUS_ENUM: () => JOB_STATUS_ENUM,
7717
8396
  EXULU_STATISTICS_TYPE_ENUM: () => STATISTICS_TYPE_ENUM,
7718
8397
  ExuluApp: () => ExuluApp,
@@ -7797,9 +8476,9 @@ async function guardRedisStartup(label, run, source) {
7797
8476
  );
7798
8477
  }, WATCHDOG_INTERVAL_MS);
7799
8478
  watchdog.unref?.();
7800
- let timer2;
8479
+ let timer3;
7801
8480
  const timeout = new Promise((_resolve, reject) => {
7802
- timer2 = setTimeout(() => {
8481
+ timer3 = setTimeout(() => {
7803
8482
  reject(
7804
8483
  new Error(
7805
8484
  `[EXULU-REDIS] Redis unreachable at ${addr} after ${REDIS_STARTUP_TIMEOUT_MS / 1e3}s \u2014 aborting ${label} startup. Last error: ${lastError ? describeError(lastError) : "none surfaced"}. Check REDIS_HOST/REDIS_PORT and that a Redis server is reachable at ${addr}.`
@@ -7816,7 +8495,7 @@ async function guardRedisStartup(label, run, source) {
7816
8495
  return result;
7817
8496
  } finally {
7818
8497
  clearInterval(watchdog);
7819
- if (timer2) clearTimeout(timer2);
8498
+ if (timer3) clearTimeout(timer3);
7820
8499
  source?.off?.("error", onError);
7821
8500
  }
7822
8501
  }
@@ -7873,7 +8552,13 @@ var requestValidators = {
7873
8552
  const { db: db2 } = await postgresClient();
7874
8553
  let authtoken = null;
7875
8554
  if (!apikey) {
7876
- authtoken = await getToken((req.headers["authorization"] || req.headers["x-api-key"]) ?? "");
8555
+ try {
8556
+ authtoken = await getToken(
8557
+ (req.headers["authorization"] || req.headers["x-api-key"]) ?? ""
8558
+ );
8559
+ } catch {
8560
+ authtoken = null;
8561
+ }
7877
8562
  }
7878
8563
  return await authentication({
7879
8564
  authtoken,
@@ -8202,14 +8887,7 @@ var ExuluStorage = class {
8202
8887
 
8203
8888
  // src/exulu/context.ts
8204
8889
  init_sanitize_name();
8205
-
8206
- // src/exulu/table-names.ts
8207
- init_cjs_shims();
8208
- init_sanitize_name();
8209
- var getTableName = (id) => sanitizeName(id) + "_items";
8210
- var getChunksTableName = (id) => sanitizeName(id) + "_chunks";
8211
-
8212
- // src/exulu/context.ts
8890
+ init_table_names();
8213
8891
  var import_knex5 = __toESM(require("pgvector/knex"), 1);
8214
8892
 
8215
8893
  // src/exulu/chunker.ts
@@ -9097,9 +9775,33 @@ function preprocessQuery(query, options = {}) {
9097
9775
 
9098
9776
  // src/graphql/resolvers/apply-sorting.ts
9099
9777
  init_cjs_shims();
9100
- var applySorting = (query, sort, field_prefix) => {
9778
+
9779
+ // src/graphql/resolvers/field-allow-list.ts
9780
+ init_cjs_shims();
9781
+ var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
9782
+ function groupableFields(table) {
9783
+ const allowed = new Set(ALWAYS_ALLOWED);
9784
+ for (const field of table.fields) {
9785
+ if (field.hidden !== true) {
9786
+ allowed.add(field.name);
9787
+ }
9788
+ }
9789
+ return allowed;
9790
+ }
9791
+ function assertAllowedField(table, fieldName, label) {
9792
+ const allowed = groupableFields(table);
9793
+ if (!allowed.has(fieldName)) {
9794
+ throw new Error(`Cannot ${label} by "${fieldName}".`);
9795
+ }
9796
+ }
9797
+
9798
+ // src/graphql/resolvers/apply-sorting.ts
9799
+ var applySorting = (query, sort, field_prefix, table) => {
9101
9800
  const prefix = field_prefix ? field_prefix + "." : "";
9102
9801
  if (sort) {
9802
+ if (table) {
9803
+ assertAllowedField(table, sort.field, "sort");
9804
+ }
9103
9805
  sort.field = prefix + sort.field;
9104
9806
  query = query.orderBy(sort.field, sort.direction.toLowerCase());
9105
9807
  }
@@ -10050,6 +10752,31 @@ var agentsSchema = {
10050
10752
  // (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
10051
10753
  name: "max_tool_steps",
10052
10754
  type: "number"
10755
+ },
10756
+ {
10757
+ name: "guest_access",
10758
+ type: "boolean",
10759
+ default: false
10760
+ },
10761
+ {
10762
+ name: "guest_auth_mode",
10763
+ type: "text",
10764
+ default: "regular"
10765
+ // 'public' | 'password' | 'regular' (= login)
10766
+ },
10767
+ {
10768
+ // bcrypt hash (hashSharePassword); NEVER exposed via GraphQL/REST —
10769
+ // see sanitizeRequestedFields + createExuluContextsTypeDefs filtering.
10770
+ name: "guest_password_hash",
10771
+ type: "text",
10772
+ required: false,
10773
+ hidden: true
10774
+ },
10775
+ {
10776
+ // S3 key of the custom login-page image shown on the public auth page.
10777
+ name: "guest_cover_image",
10778
+ type: "text",
10779
+ required: false
10053
10780
  }
10054
10781
  ]
10055
10782
  };
@@ -10166,7 +10893,8 @@ var usersSchema = {
10166
10893
  },
10167
10894
  {
10168
10895
  name: "temporary_token",
10169
- type: "text"
10896
+ type: "text",
10897
+ hidden: true
10170
10898
  },
10171
10899
  {
10172
10900
  name: "type",
@@ -10192,7 +10920,8 @@ var usersSchema = {
10192
10920
  },
10193
10921
  {
10194
10922
  name: "apikey",
10195
- type: "text"
10923
+ type: "text",
10924
+ hidden: true
10196
10925
  },
10197
10926
  {
10198
10927
  name: "scope_mode",
@@ -10209,11 +10938,13 @@ var usersSchema = {
10209
10938
  },
10210
10939
  {
10211
10940
  name: "password",
10212
- type: "text"
10941
+ type: "text",
10942
+ hidden: true
10213
10943
  },
10214
10944
  {
10215
10945
  name: "anthropic_token",
10216
- type: "text"
10946
+ type: "text",
10947
+ hidden: true
10217
10948
  },
10218
10949
  {
10219
10950
  name: "personal_system_prompt",
@@ -10433,29 +11164,20 @@ var imageGenerationsSchema = {
10433
11164
  { name: "error", type: "text", required: false }
10434
11165
  ]
10435
11166
  };
10436
- var oauthTokensSchema = {
10437
- type: "oauth_tokens",
10438
- name: {
10439
- plural: "oauth_tokens",
10440
- singular: "oauth_token"
10441
- },
10442
- // Rows are only ever read/written by the oauth token store for the owning
10443
- // (provider, user_id) pair — never exposed via GraphQL — so no RBAC fields.
10444
- RBAC: false,
10445
- fields: [
10446
- { name: "provider", type: "text", required: false, index: true },
10447
- { name: "tool_id", type: "text", required: true, index: true },
10448
- { name: "user_id", type: "number", required: true, index: true },
10449
- { name: "access_token", type: "longText", required: true },
10450
- // AES-encrypted
10451
- { name: "refresh_token", type: "longText", required: false },
10452
- // AES-encrypted
10453
- { name: "token_type", type: "text", required: false },
10454
- { name: "scopes", type: "text", required: false },
10455
- { name: "expires_at", type: "date", required: false }
10456
- // null = non-expiring
10457
- ]
10458
- };
11167
+ function userCredentialsSchema() {
11168
+ return `
11169
+ CREATE TABLE IF NOT EXISTS user_credentials (
11170
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
11171
+ provider text NOT NULL,
11172
+ user_id text NOT NULL,
11173
+ auth_type text NOT NULL CHECK (auth_type IN ('oauth', 'user_credentials')),
11174
+ data text NOT NULL,
11175
+ created_at timestamptz NOT NULL DEFAULT now(),
11176
+ updated_at timestamptz NOT NULL DEFAULT now(),
11177
+ UNIQUE (provider, user_id)
11178
+ );
11179
+ `;
11180
+ }
10459
11181
  var sharedArtifactsSchema = {
10460
11182
  type: "shared_artifacts",
10461
11183
  name: {
@@ -10469,7 +11191,7 @@ var sharedArtifactsSchema = {
10469
11191
  { name: "name", type: "text", index: true, unique: true, required: true },
10470
11192
  { name: "s3key", type: "text", required: true },
10471
11193
  { name: "auth_mode", type: "text", default: "regular" },
10472
- { name: "password_hash", type: "text", required: false },
11194
+ { name: "password_hash", type: "text", required: false, hidden: true },
10473
11195
  // bcrypt; password mode only
10474
11196
  { name: "expires_at", type: "date", required: false },
10475
11197
  // null = no expiry
@@ -10567,7 +11289,6 @@ var coreSchemas = {
10567
11289
  entityTypeSettingsSchema: () => addCoreFields(entityTypeSettingsSchema),
10568
11290
  promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
10569
11291
  contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
10570
- oauthTokensSchema: () => addCoreFields(oauthTokensSchema),
10571
11292
  sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
10572
11293
  transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
10573
11294
  imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
@@ -11435,7 +12156,7 @@ var vectorSearch = async ({
11435
12156
  chunksQuery = applyFilters(chunksQuery, itemFilters, table, "items");
11436
12157
  chunksQuery = applyFilters(chunksQuery, chunkFilters, table, "chunks");
11437
12158
  chunksQuery = applyAccessControl(table, chunksQuery, user, "items");
11438
- chunksQuery = applySorting(chunksQuery, sort, "items");
12159
+ chunksQuery = applySorting(chunksQuery, sort, "items", table);
11439
12160
  if (filterEntityIds) {
11440
12161
  applyEntityFilter(chunksQuery, "chunks", context, filterEntityIds, entityFilter?.mode || "any");
11441
12162
  }
@@ -12398,9 +13119,9 @@ var ExuluContext2 = class {
12398
13119
  job
12399
13120
  };
12400
13121
  };
12401
- createItem = async (item, config, user, role, upsert, generateEmbeddingsOverwrite) => {
12402
- console.log("[EXULU] creating item", item, upsert);
12403
- if (upsert && !item.id && !item.external_id) {
13122
+ createItem = async (item, config, user, role, upsert2, generateEmbeddingsOverwrite) => {
13123
+ console.log("[EXULU] creating item", item, upsert2);
13124
+ if (upsert2 && !item.id && !item.external_id) {
12404
13125
  throw new Error("Item id or external id is required for upsert.");
12405
13126
  }
12406
13127
  const { db: db2 } = await postgresClient();
@@ -12416,8 +13137,8 @@ var ExuluContext2 = class {
12416
13137
  ...item,
12417
13138
  tags: item.tags ? Array.isArray(item.tags) ? item.tags.join(",") : item.tags : void 0
12418
13139
  }).returning("id");
12419
- console.log("[EXULU] Upsert", upsert);
12420
- if (upsert) {
13140
+ console.log("[EXULU] Upsert", upsert2);
13141
+ if (upsert2) {
12421
13142
  if (item.external_id) {
12422
13143
  mutation.onConflict("external_id").merge();
12423
13144
  } else if (item.id) {
@@ -12999,6 +13720,7 @@ var queues = new ExuluQueues();
12999
13720
  // src/graphql/schemas/index.ts
13000
13721
  var import_uuid4 = require("uuid");
13001
13722
  init_pipeline();
13723
+ init_context_write_tools();
13002
13724
 
13003
13725
  // src/graphql/types/index.ts
13004
13726
  init_cjs_shims();
@@ -13040,6 +13762,8 @@ var GraphQLDate = new import_graphql.GraphQLScalarType({
13040
13762
 
13041
13763
  // src/graphql/resolvers/utils.ts
13042
13764
  init_cjs_shims();
13765
+ var NON_COLUMN_SELECTIONS = /* @__PURE__ */ new Set(["pageInfo", "items", "RBAC"]);
13766
+ var isSelectableColumn = (field) => !NON_COLUMN_SELECTIONS.has(field) && !field.startsWith("__");
13043
13767
  var getRequestedFields = (info) => {
13044
13768
  const selections = info.operation.selectionSet.selections[0].selectionSet.selections;
13045
13769
  const itemSelection = selections.find((s) => s.name.value === "item");
@@ -13052,7 +13776,7 @@ var getRequestedFields = (info) => {
13052
13776
  return acc;
13053
13777
  }, {})
13054
13778
  );
13055
- return fields.filter((field) => field !== "pageInfo" && field !== "items" && field !== "RBAC");
13779
+ return fields.filter(isSelectableColumn);
13056
13780
  }
13057
13781
  if (itemsSelection) {
13058
13782
  fields = Object.keys(
@@ -13061,7 +13785,7 @@ var getRequestedFields = (info) => {
13061
13785
  return acc;
13062
13786
  }, {})
13063
13787
  );
13064
- return fields.filter((field) => field !== "pageInfo" && field !== "items" && field !== "RBAC");
13788
+ return fields.filter(isSelectableColumn);
13065
13789
  }
13066
13790
  fields = Object.keys(
13067
13791
  selections.reduce((acc, field) => {
@@ -13069,7 +13793,7 @@ var getRequestedFields = (info) => {
13069
13793
  return acc;
13070
13794
  }, {})
13071
13795
  );
13072
- return fields.filter((field) => field !== "pageInfo" && field !== "items" && field !== "RBAC");
13796
+ return fields.filter(isSelectableColumn);
13073
13797
  };
13074
13798
  var contextItemsProcessorHandler = async (context, config, items, user, role) => {
13075
13799
  let jobs = [];
@@ -13150,8 +13874,11 @@ init_client();
13150
13874
  init_singleton();
13151
13875
  init_supervisor();
13152
13876
  init_catalog();
13153
- init_tags();
13877
+
13878
+ // src/graphql/utilities/budget-field.ts
13879
+ init_cjs_shims();
13154
13880
  init_budget_service();
13881
+ init_tags();
13155
13882
  var BUDGET_ENTITY_SINGULARS = /* @__PURE__ */ new Set([
13156
13883
  "user",
13157
13884
  "role",
@@ -13174,22 +13901,37 @@ var BUDGET_ENTITY_TYPE_BY_SINGULAR = {
13174
13901
  };
13175
13902
  var addBudgetField = async (requestedFields, result, tableSingular, user) => {
13176
13903
  if (!requestedFields.includes("budget")) return result;
13904
+ const entityType = BUDGET_ENTITY_TYPE_BY_SINGULAR[tableSingular];
13177
13905
  const scope = user?.role?.budget_management;
13178
- const canRead = !!user?.super_admin || scope === "read" || scope === "write";
13179
- if (!canRead || result?.id == null) {
13906
+ const canReadAll = !!user?.super_admin || scope === "read" || scope === "write";
13907
+ const memberView = !canReadAll && entityType === "project";
13908
+ if (!canReadAll && !memberView || result?.id == null || !entityType) {
13180
13909
  result.budget = null;
13181
13910
  return result;
13182
13911
  }
13183
- const entityType = BUDGET_ENTITY_TYPE_BY_SINGULAR[tableSingular];
13184
- if (!entityType) {
13912
+ const map = await getTagBudgetMap();
13913
+ const tag = budgetTagFor(entityType, result.id);
13914
+ const info = tag ? map[tag] ?? null : null;
13915
+ if (!memberView) {
13916
+ result.budget = info;
13917
+ return result;
13918
+ }
13919
+ if (!info) {
13185
13920
  result.budget = null;
13186
13921
  return result;
13187
13922
  }
13188
- const map = await getTagBudgetMap();
13189
- const tag = budgetTagFor(entityType, result.id);
13190
- result.budget = tag ? map[tag] ?? null : null;
13923
+ const settings = await getBudgetSettings();
13924
+ result.budget = {
13925
+ spend: info.spend,
13926
+ max_budget: info.max_budget,
13927
+ budget_duration: info.budget_duration,
13928
+ budget_reset_at: info.budget_reset_at,
13929
+ display: settings.user_budget_display
13930
+ };
13191
13931
  return result;
13192
13932
  };
13933
+
13934
+ // src/graphql/utilities/sanitize-and-hydrate-fields.ts
13193
13935
  var addProviderFields = async (args, requestedFields, providers, result, tools, user, contexts) => {
13194
13936
  let provider;
13195
13937
  let modelRow;
@@ -13439,6 +14181,14 @@ var finalizeRequestedFields = async ({
13439
14181
  if (!requestedFields.includes("provider")) {
13440
14182
  delete result.provider;
13441
14183
  }
14184
+ if (requestedFields.includes("guest_has_password")) {
14185
+ result.guest_has_password = !!result.guest_password_hash;
14186
+ }
14187
+ }
14188
+ for (const field of table.fields) {
14189
+ if (field.hidden === true) {
14190
+ delete result[field.name];
14191
+ }
13442
14192
  }
13443
14193
  if (BUDGET_ENTITY_SINGULARS.has(table.name.singular)) {
13444
14194
  result = await addBudgetField(requestedFields, result, table.name.singular, user);
@@ -13510,7 +14260,7 @@ var itemsPaginationRequest = async ({
13510
14260
  let dataQuery = db2(tableName);
13511
14261
  dataQuery = applyFilters(dataQuery, filters, table);
13512
14262
  dataQuery = applyAccessControl(table, dataQuery, user);
13513
- dataQuery = applySorting(dataQuery, sort);
14263
+ dataQuery = applySorting(dataQuery, sort, void 0, table);
13514
14264
  if (page > 1) {
13515
14265
  dataQuery = dataQuery.offset((page - 1) * limit);
13516
14266
  }
@@ -13533,8 +14283,20 @@ var removeProviderFields = (requestedFields) => {
13533
14283
  return filtered;
13534
14284
  };
13535
14285
  var sanitizeRequestedFields = (table, requestedFields) => {
14286
+ const hiddenNames = new Set(
14287
+ table.fields.filter((f) => f.hidden === true).map((f) => f.name)
14288
+ );
14289
+ if (hiddenNames.size > 0) {
14290
+ requestedFields = requestedFields.filter((f) => !hiddenNames.has(f));
14291
+ }
13536
14292
  if (table.name.singular === "agent") {
13537
14293
  requestedFields = removeProviderFields(requestedFields);
14294
+ if (requestedFields.includes("guest_has_password")) {
14295
+ requestedFields = requestedFields.filter(
14296
+ (field) => field !== "guest_has_password"
14297
+ );
14298
+ requestedFields.push("guest_password_hash");
14299
+ }
13538
14300
  }
13539
14301
  if (["user", "role", "team", "project", "agent"].includes(table.name.singular)) {
13540
14302
  requestedFields = requestedFields.filter((field) => field !== "budget");
@@ -13632,7 +14394,7 @@ function createQueries(table, providers, tools, contexts) {
13632
14394
  let query = db2.from(tableNamePlural).select(sanitizedFields);
13633
14395
  query = applyFilters(query, filters, table);
13634
14396
  query = applyAccessControl(table, query, context.user);
13635
- query = applySorting(query, sort);
14397
+ query = applySorting(query, sort, void 0, table);
13636
14398
  let result = await query.first();
13637
14399
  return finalizeRequestedFields({
13638
14400
  args,
@@ -13683,6 +14445,7 @@ function createQueries(table, providers, tools, contexts) {
13683
14445
  query = applyAccessControl(table, query, context.user);
13684
14446
  query = query.limit(limit);
13685
14447
  if (groupBy) {
14448
+ assertAllowedField(table, groupBy, "group");
13686
14449
  query = query.select(groupBy).groupBy(groupBy);
13687
14450
  if (tableNamePlural === "tracking") {
13688
14451
  query = query.sum("total as count");
@@ -13919,7 +14682,7 @@ var encryptSensitiveFields = (input) => {
13919
14682
  };
13920
14683
 
13921
14684
  // src/graphql/mutations/index.ts
13922
- var import_bcryptjs3 = __toESM(require("bcryptjs"), 1);
14685
+ var import_bcryptjs4 = __toESM(require("bcryptjs"), 1);
13923
14686
  init_statistics();
13924
14687
 
13925
14688
  // src/exulu/routines/run-state.ts
@@ -14207,7 +14970,77 @@ var handleRBACUpdate = async (db2, entityName, resourceId, rbacData, existingRba
14207
14970
  }
14208
14971
  };
14209
14972
 
14973
+ // src/graphql/utilities/agent-guest-fields.ts
14974
+ init_cjs_shims();
14975
+
14976
+ // src/exulu/shared-artifacts.ts
14977
+ init_cjs_shims();
14978
+ var import_bcryptjs3 = __toESM(require("bcryptjs"), 1);
14979
+ var normalizeS3Key = (key, bucket) => {
14980
+ const segments = key.split("/").filter((s, i) => !(i === 0 && s === "")).map((s) => decodeURIComponent(s));
14981
+ if (segments[0] === bucket) segments.shift();
14982
+ return segments.join("/");
14983
+ };
14984
+ var isHtmlKey = (key) => /\.html?$/i.test(key);
14985
+ var deriveFilename = (key) => {
14986
+ const base = key.split("/").pop() ?? key;
14987
+ return base.split("_EXULU_").pop() ?? base;
14988
+ };
14989
+ var slugifyShareName = (input) => deriveFilename(input).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
14990
+ var isExpired = (expiresAt, now) => {
14991
+ if (!expiresAt) return false;
14992
+ return new Date(expiresAt).getTime() <= now.getTime();
14993
+ };
14994
+ var validateCreateInput = (input, now) => {
14995
+ if (!input.s3key) return { ok: false, message: "s3key is required." };
14996
+ if (!input.name) return { ok: false, message: "name is required." };
14997
+ const mode = input.auth_mode;
14998
+ if (mode !== "public" && mode !== "password" && mode !== "regular") {
14999
+ return { ok: false, message: "auth_mode must be public, password, or regular." };
15000
+ }
15001
+ if (mode === "password" && !input.password) {
15002
+ return { ok: false, message: "A password is required for password mode." };
15003
+ }
15004
+ if (input.expires_at && Number.isNaN(new Date(input.expires_at).getTime())) {
15005
+ return { ok: false, message: "expires_at is not a valid date." };
15006
+ }
15007
+ if (input.expires_at && isExpired(input.expires_at, now)) {
15008
+ return { ok: false, message: "expires_at must be in the future." };
15009
+ }
15010
+ return { ok: true };
15011
+ };
15012
+ var hashSharePassword = (password) => import_bcryptjs3.default.hash(password, 10);
15013
+ var verifySharePassword = (password, hash) => import_bcryptjs3.default.compare(password, hash);
15014
+ var contentHeadersFor = (key, contentType, filename) => {
15015
+ if (isHtmlKey(key)) return { contentType: "text/html; charset=utf-8" };
15016
+ return {
15017
+ contentType: contentType || "application/octet-stream",
15018
+ disposition: `attachment; filename="${filename.replace(/"/g, "")}"`
15019
+ };
15020
+ };
15021
+ var getSharedArtifactByName = (db2, name) => db2("shared_artifacts").where({ name }).first();
15022
+
15023
+ // src/graphql/utilities/agent-guest-fields.ts
15024
+ var VALID_GUEST_AUTH_MODES = /* @__PURE__ */ new Set(["public", "password", "regular"]);
15025
+ var applyAgentGuestFieldTransforms = async (input) => {
15026
+ if (input.guest_auth_mode !== void 0 && !VALID_GUEST_AUTH_MODES.has(input.guest_auth_mode)) {
15027
+ throw new Error(
15028
+ 'guest_auth_mode must be "public", "password", or "regular".'
15029
+ );
15030
+ }
15031
+ delete input.guest_password_hash;
15032
+ if (typeof input.guest_password === "string" && input.guest_password.length > 0) {
15033
+ input.guest_password_hash = await hashSharePassword(input.guest_password);
15034
+ }
15035
+ delete input.guest_password;
15036
+ if (input.guest_auth_mode !== void 0 && input.guest_auth_mode !== "password") {
15037
+ input.guest_password_hash = null;
15038
+ }
15039
+ return input;
15040
+ };
15041
+
14210
15042
  // src/graphql/mutations/index.ts
15043
+ var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
14211
15044
  var postprocessDeletion = async ({
14212
15045
  table,
14213
15046
  requestedFields,
@@ -14401,7 +15234,9 @@ function createMutations(table, providers, contexts, tools, config) {
14401
15234
  entity: table.name.singular,
14402
15235
  target_resource_id: id,
14403
15236
  access_type: "Role",
14404
- role_id: user.role,
15237
+ // auth.ts hydrates user.role into the full roles row when it
15238
+ // exists; unhydrated it is still the uuid string.
15239
+ role_id: user.role?.id ?? user.role,
14405
15240
  rights: "write"
14406
15241
  }).first();
14407
15242
  if (rbacRecord) {
@@ -14414,7 +15249,8 @@ function createMutations(table, providers, contexts, tools, config) {
14414
15249
  entity: table.name.singular,
14415
15250
  target_resource_id: id,
14416
15251
  access_type: "Team",
14417
- team_id: user.team,
15252
+ // Same best-effort hydration as user.role above.
15253
+ team_id: user.team?.id ?? user.team,
14418
15254
  rights: "write"
14419
15255
  }).first();
14420
15256
  if (rbacRecord) {
@@ -14444,6 +15280,9 @@ function createMutations(table, providers, contexts, tools, config) {
14444
15280
  if (item.rights_mode) {
14445
15281
  item.rights_mode = "private";
14446
15282
  }
15283
+ if (tableNamePlural === "agents" && "guest_access" in item) {
15284
+ item.guest_access = false;
15285
+ }
14447
15286
  if (item.created_at) {
14448
15287
  item.created_at = /* @__PURE__ */ new Date();
14449
15288
  }
@@ -14512,9 +15351,12 @@ function createMutations(table, providers, contexts, tools, config) {
14512
15351
  }
14513
15352
  if (table.name.singular === "user" && input.password) {
14514
15353
  console.log("[EXULU] Hashing password", input.password);
14515
- input.password = await import_bcryptjs3.default.hash(input.password, SALT_ROUNDS);
15354
+ input.password = await import_bcryptjs4.default.hash(input.password, SALT_ROUNDS);
14516
15355
  console.log("[EXULU] Hashed password", input.password);
14517
15356
  }
15357
+ if (table.name.singular === "agent") {
15358
+ input = await applyAgentGuestFieldTransforms(input);
15359
+ }
14518
15360
  Object.keys(input).forEach((key) => {
14519
15361
  if (table.fields.find((field) => field.name === key)?.type === "json") {
14520
15362
  if (typeof input[key] === "object" || Array.isArray(input[key])) {
@@ -14528,10 +15370,15 @@ function createMutations(table, providers, contexts, tools, config) {
14528
15370
  input.id = db2.fn.uuid();
14529
15371
  }
14530
15372
  }
15373
+ if (table.RBAC && input.rights_mode != null && !VALID_RIGHTS_MODES.includes(input.rights_mode)) {
15374
+ throw new Error(
15375
+ `Invalid rights_mode "${input.rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES.join(", ")}`
15376
+ );
15377
+ }
14531
15378
  const columns = await db2(tableNamePlural).columnInfo();
14532
15379
  const insert = db2(tableNamePlural).insert({
14533
15380
  ...input,
14534
- ...table.RBAC ? { rights_mode: "private" } : {}
15381
+ ...table.RBAC ? { rights_mode: input.rights_mode ?? "private" } : {}
14535
15382
  }).returning(Object.keys(columns));
14536
15383
  if (args.upsert) {
14537
15384
  insert.onConflict().merge();
@@ -14579,9 +15426,12 @@ function createMutations(table, providers, contexts, tools, config) {
14579
15426
  }
14580
15427
  if (table.name.singular === "user" && input.password) {
14581
15428
  console.log("[EXULU] Hashing password", input.password);
14582
- input.password = await import_bcryptjs3.default.hash(input.password, SALT_ROUNDS);
15429
+ input.password = await import_bcryptjs4.default.hash(input.password, SALT_ROUNDS);
14583
15430
  console.log("[EXULU] Hashed password", input.password);
14584
15431
  }
15432
+ if (table.name.singular === "agent") {
15433
+ input = await applyAgentGuestFieldTransforms(input);
15434
+ }
14585
15435
  Object.keys(input).forEach((key) => {
14586
15436
  if (table.fields.find((field) => field.name === key)?.type === "json") {
14587
15437
  if (typeof input[key] === "object" || Array.isArray(input[key])) {
@@ -14652,9 +15502,12 @@ function createMutations(table, providers, contexts, tools, config) {
14652
15502
  }
14653
15503
  if (table.name.singular === "user" && input.password) {
14654
15504
  console.log("[EXULU] Hashing password", input.password);
14655
- input.password = await import_bcryptjs3.default.hash(input.password, SALT_ROUNDS);
15505
+ input.password = await import_bcryptjs4.default.hash(input.password, SALT_ROUNDS);
14656
15506
  console.log("[EXULU] Hashed password", input.password);
14657
15507
  }
15508
+ if (table.name.singular === "agent") {
15509
+ input = await applyAgentGuestFieldTransforms(input);
15510
+ }
14658
15511
  Object.keys(input).forEach((key) => {
14659
15512
  if (table.fields.find((field) => field.name === key)?.type === "json") {
14660
15513
  if (typeof input[key] === "object" || Array.isArray(input[key])) {
@@ -15138,6 +15991,7 @@ init_cjs_shims();
15138
15991
  init_pipeline();
15139
15992
  init_check_record_access();
15140
15993
  init_singleton();
15994
+ init_kb_editor_config();
15141
15995
  var getEnabledTools = async (agent, allExuluTools, allContexts, disabledTools = [], providers, user) => {
15142
15996
  let enabledTools = [];
15143
15997
  if (agent.tools) {
@@ -15155,6 +16009,9 @@ var getEnabledTools = async (agent, allExuluTools, allContexts, disabledTools =
15155
16009
  model: void 0
15156
16010
  });
15157
16011
  }
16012
+ if (id === KB_EDITOR_TOOL_ID) {
16013
+ return null;
16014
+ }
15158
16015
  if (type === "agent") {
15159
16016
  if (id === agent.id) {
15160
16017
  return null;
@@ -15195,7 +16052,7 @@ init_resolve_model();
15195
16052
  init_client();
15196
16053
  var import_api = require("@opentelemetry/api");
15197
16054
  var import_uuid3 = require("uuid");
15198
- var import_ai10 = require("ai");
16055
+ var import_ai7 = require("ai");
15199
16056
  var import_crypto_js7 = require("crypto-js");
15200
16057
  init_statistics();
15201
16058
  init_sanitize_tool_name();
@@ -15364,11 +16221,11 @@ var autoDeclineStaleApprovals = (messages) => {
15364
16221
  };
15365
16222
 
15366
16223
  // src/exulu/provider.ts
15367
- var import_zod14 = require("zod");
16224
+ var import_zod16 = require("zod");
15368
16225
  init_tool();
15369
16226
  init_resolve_model();
15370
16227
  init_statistics2();
15371
- var import_ai9 = require("ai");
16228
+ var import_ai6 = require("ai");
15372
16229
 
15373
16230
  // src/utils/generate-slug.ts
15374
16231
  init_cjs_shims();
@@ -15390,7 +16247,7 @@ init_entitlements();
15390
16247
 
15391
16248
  // src/exulu/task-description.ts
15392
16249
  init_cjs_shims();
15393
- var import_ai8 = require("ai");
16250
+ var import_ai5 = require("ai");
15394
16251
  init_client();
15395
16252
  var AGENT_VISUALIZATION_ENABLED = process.env.NEXT_PUBLIC_AGENT_VISUALIZATION === "true";
15396
16253
  async function setSessionCurrentTask({
@@ -15405,7 +16262,7 @@ async function setSessionCurrentTask({
15405
16262
  }
15406
16263
  const truncated = userMessage.slice(0, 500);
15407
16264
  console.log("[EXULU] Generating text for session current task: " + truncated);
15408
- const { text } = await (0, import_ai8.generateText)({
16265
+ const { text } = await (0, import_ai5.generateText)({
15409
16266
  model,
15410
16267
  prompt: `You are a task labeler for an agent monitoring dashboard visible to all employees.
15411
16268
  Given the user message below, write a 4\u20138 word present-tense description of what the AI assistant is working on.
@@ -15489,6 +16346,38 @@ var isRunSessionMetadata = (metadata) => {
15489
16346
  return typeof parsed === "object" && parsed !== null && !!parsed.job_result_id;
15490
16347
  };
15491
16348
 
16349
+ // src/exulu/auth/sanitize-ui-messages.ts
16350
+ init_cjs_shims();
16351
+ init_scrub_text();
16352
+ var sanitizeAuthPayloadsInUiMessages = (messages) => messages.map((message) => {
16353
+ if (message.role !== "assistant" || !Array.isArray(message.parts)) {
16354
+ return message;
16355
+ }
16356
+ let changed = false;
16357
+ const parts = message.parts.map((part) => {
16358
+ const output = part?.output;
16359
+ if (output && typeof output === "object" && output.credentialRequest) {
16360
+ changed = true;
16361
+ return { ...part, output: { result: SCRUBBED_CREDENTIAL_TEXT } };
16362
+ }
16363
+ if (output && typeof output === "object" && output.oauth?.authorizationUrl) {
16364
+ changed = true;
16365
+ return { ...part, output: { result: SCRUBBED_OAUTH_TEXT } };
16366
+ }
16367
+ return part;
16368
+ });
16369
+ return changed ? { ...message, parts } : message;
16370
+ });
16371
+
16372
+ // src/exulu/auth/guardrail.ts
16373
+ init_cjs_shims();
16374
+ var CREDENTIAL_GUARDRAIL = `Credential safety:
16375
+ Some tools collect credentials (API keys, passwords, tokens) through a secure form shown directly to the user in the chat UI. Credentials are never entered in the conversation itself.
16376
+ - Never ask the user to type credential values into the chat.
16377
+ - If the user pastes a credential value into the chat anyway, do not repeat it, do not store it, and do not pass it to any tool. Tell them to use the secure form instead (calling the tool again shows the form if it is no longer visible).
16378
+ - After the user confirms they submitted the form, call the tool again.`;
16379
+ var credentialGuardrailBlock = (currentTools) => currentTools?.some((t) => t.authentication?.authType === "user_credentials") ? CREDENTIAL_GUARDRAIL : null;
16380
+
15492
16381
  // src/exulu/provider.ts
15493
16382
  var ExuluProvider = class {
15494
16383
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
@@ -15568,9 +16457,9 @@ var ExuluProvider = class {
15568
16457
  name: `${agent.name}`,
15569
16458
  type: "agent",
15570
16459
  category: "agents",
15571
- inputSchema: import_zod14.z.object({
15572
- prompt: import_zod14.z.string().describe("The prompt (usually a question for the agent) to send to the agent."),
15573
- information: import_zod14.z.string().describe("A summary of relevant context / information from the current session")
16460
+ inputSchema: import_zod16.z.object({
16461
+ prompt: import_zod16.z.string().describe("The prompt (usually a question for the agent) to send to the agent."),
16462
+ information: import_zod16.z.string().describe("A summary of relevant context / information from the current session")
15574
16463
  }),
15575
16464
  description: `This tool calls an agent named: ${agent.name}. The agent does the following: ${agent.description}.`,
15576
16465
  config: [],
@@ -15695,7 +16584,7 @@ var ExuluProvider = class {
15695
16584
  const previousMessagesContent = previousMessages.map(
15696
16585
  (message) => JSON.parse(message.content)
15697
16586
  );
15698
- messages = await (0, import_ai9.validateUIMessages)({
16587
+ messages = await (0, import_ai6.validateUIMessages)({
15699
16588
  // append the new message to the previous messages:
15700
16589
  messages: [...previousMessagesContent, ...messages]
15701
16590
  });
@@ -15858,6 +16747,10 @@ var ExuluProvider = class {
15858
16747
 
15859
16748
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
15860
16749
  'Inform the user that the action was not performed.`;
16750
+ const credentialGuardrail = credentialGuardrailBlock(currentTools);
16751
+ if (credentialGuardrail) {
16752
+ system += "\n\n" + credentialGuardrail;
16753
+ }
15861
16754
  if (prompt) {
15862
16755
  let result = { object: null, text: "" };
15863
16756
  let inputTokens = 0;
@@ -15866,7 +16759,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
15866
16759
  "[EXULU] Generating text for agent: " + this.name,
15867
16760
  "with prompt: " + prompt?.slice(0, 100) + "..."
15868
16761
  );
15869
- const output = await (0, import_ai9.generateText)({
16762
+ const output = await (0, import_ai6.generateText)({
15870
16763
  temperature: 0,
15871
16764
  // TODO Make this configurable
15872
16765
  model,
@@ -15878,7 +16771,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
15878
16771
  // assistant's response, no follow-up text turn is wanted (same
15879
16772
  // reasoning as question_ask: the UI artifact is the message).
15880
16773
  prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
15881
- stopWhen: [(0, import_ai9.stepCountIs)(turnBudget), (0, import_ai9.hasToolCall)("image_generation")]
16774
+ stopWhen: [(0, import_ai6.stepCountIs)(turnBudget), (0, import_ai6.hasToolCall)("image_generation")]
15882
16775
  });
15883
16776
  console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
15884
16777
  const {
@@ -15929,19 +16822,23 @@ When a tool execution is not approved by the user, do not retry it unless explic
15929
16822
  "[EXULU] Generating text for agent: " + this.name,
15930
16823
  "with messages: " + messages.length
15931
16824
  );
15932
- const { text, totalUsage } = await (0, import_ai9.generateText)({
16825
+ const { text, totalUsage } = await (0, import_ai6.generateText)({
15933
16826
  temperature: 0,
15934
16827
  // TODO Make this configurable
15935
16828
  model,
15936
16829
  // Should be a LanguageModelV1
15937
16830
  system,
15938
- messages: await (0, import_ai9.convertToModelMessages)(messages, {
15939
- ignoreIncompleteToolCalls: true
16831
+ // tools: applies each tool's toModelOutput to historical tool results;
16832
+ // sanitize: guarantees auth payloads never reach the model regardless
16833
+ // of part encoding (spec 2026-07-22 §1.2).
16834
+ messages: await (0, import_ai6.convertToModelMessages)(sanitizeAuthPayloadsInUiMessages(messages), {
16835
+ ignoreIncompleteToolCalls: true,
16836
+ tools
15940
16837
  }),
15941
16838
  maxRetries: 2,
15942
16839
  tools,
15943
16840
  prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
15944
- stopWhen: [(0, import_ai9.stepCountIs)(turnBudget), (0, import_ai9.hasToolCall)("image_generation")]
16841
+ stopWhen: [(0, import_ai6.stepCountIs)(turnBudget), (0, import_ai6.hasToolCall)("image_generation")]
15945
16842
  });
15946
16843
  if (statistics) {
15947
16844
  await Promise.all([
@@ -16112,7 +17009,7 @@ ${guardedText}
16112
17009
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
16113
17010
  }
16114
17011
  const model = languageModel;
16115
- messages = await (0, import_ai9.validateUIMessages)({
17012
+ messages = await (0, import_ai6.validateUIMessages)({
16116
17013
  // append the new message to the previous messages:
16117
17014
  messages: [...previousMessagesContent, message]
16118
17015
  });
@@ -16256,6 +17153,10 @@ ${skillsList}
16256
17153
 
16257
17154
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
16258
17155
  'Inform the user that the action was not performed.`;
17156
+ const credentialGuardrail = credentialGuardrailBlock(currentTools);
17157
+ if (credentialGuardrail) {
17158
+ system += "\n\n" + credentialGuardrail;
17159
+ }
16259
17160
  console.log("[EXULU] Tools", currentTools?.map((x) => x.name));
16260
17161
  console.log("[EXULU] Skills", currentSkills?.map((x) => x.name));
16261
17162
  const tools = await convertExuluToolsToAiSdkTools(
@@ -16334,13 +17235,17 @@ When a tool execution is not approved by the user, do not retry it unless explic
16334
17235
  Object.keys(tools)
16335
17236
  );
16336
17237
  const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
16337
- const result = (0, import_ai9.streamText)({
17238
+ const result = (0, import_ai6.streamText)({
16338
17239
  temperature: 0,
16339
17240
  // TODO Make this configurable
16340
17241
  model,
16341
17242
  // Should be a LanguageModelV1
16342
- messages: await (0, import_ai9.convertToModelMessages)(messages, {
16343
- ignoreIncompleteToolCalls: true
17243
+ // tools: applies each tool's toModelOutput to historical tool results;
17244
+ // sanitize: guarantees auth payloads never reach the model regardless
17245
+ // of part encoding (spec 2026-07-22 §1.2).
17246
+ messages: await (0, import_ai6.convertToModelMessages)(sanitizeAuthPayloadsInUiMessages(messages), {
17247
+ ignoreIncompleteToolCalls: true,
17248
+ tools
16344
17249
  }),
16345
17250
  // PrepareStep could be used here to set the model
16346
17251
  // for the first step or change other parameters.
@@ -16360,7 +17265,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
16360
17265
  },
16361
17266
  // todo allow configuring the step budget per skill
16362
17267
  prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
16363
- stopWhen: [(0, import_ai9.stepCountIs)(turnBudget), (0, import_ai9.hasToolCall)("image_generation")]
17268
+ stopWhen: [(0, import_ai6.stepCountIs)(turnBudget), (0, import_ai6.hasToolCall)("image_generation")]
16364
17269
  });
16365
17270
  return {
16366
17271
  stream: result,
@@ -18614,7 +19519,7 @@ var transcriptionService = {
18614
19519
 
18615
19520
  // src/exulu/recall/service.ts
18616
19521
  init_cjs_shims();
18617
- var import_ai11 = require("ai");
19522
+ var import_ai8 = require("ai");
18618
19523
  init_singleton();
18619
19524
  init_client();
18620
19525
  init_resolve_model();
@@ -18688,10 +19593,25 @@ var RecallApiError = class extends Error {
18688
19593
  }
18689
19594
  };
18690
19595
  var jitterMs = () => 1e3 * Math.ceil(Math.random() * 5);
19596
+ var API_TIMEOUT_MS = 6e4;
19597
+ var DOWNLOAD_TIMEOUT_MS = 3e5;
18691
19598
  async function fetch_with_retry(args) {
18692
- const { url, options, max_attempts = 6 } = args;
19599
+ const { url, options, max_attempts = 6, timeout_ms, retry_on_reject } = args;
18693
19600
  for (let attempt = 1; attempt <= max_attempts; attempt++) {
18694
- const response = await fetch(url, options);
19601
+ let response;
19602
+ try {
19603
+ response = await fetch(
19604
+ url,
19605
+ timeout_ms ? { ...options, signal: AbortSignal.timeout(timeout_ms) } : options
19606
+ );
19607
+ } catch (err) {
19608
+ if (!retry_on_reject || attempt === max_attempts) throw err;
19609
+ console.log(
19610
+ `[EXULU-RECALL] fetch error from ${url} (${err.message}); retrying in ~5s (attempt ${attempt}/${max_attempts})`
19611
+ );
19612
+ await new Promise((resolve8) => setTimeout(resolve8, 5e3 + jitterMs()));
19613
+ continue;
19614
+ }
18695
19615
  let wait_for = null;
18696
19616
  switch (response.status) {
18697
19617
  case 429:
@@ -18732,7 +19652,9 @@ var request2 = async (path2, init = {}) => {
18732
19652
  ...init.body ? { "content-type": "application/json" } : {},
18733
19653
  ...init.headers ?? {}
18734
19654
  }
18735
- }
19655
+ },
19656
+ timeout_ms: API_TIMEOUT_MS,
19657
+ retry_on_reject: (init.method ?? "GET").toUpperCase() === "GET"
18736
19658
  });
18737
19659
  if (!response.ok) {
18738
19660
  const body = await response.text();
@@ -18804,7 +19726,9 @@ var recallClient = {
18804
19726
  downloadTranscript: async (downloadUrl) => {
18805
19727
  const response = await fetch_with_retry({
18806
19728
  url: downloadUrl,
18807
- options: { method: "GET" }
19729
+ options: { method: "GET" },
19730
+ timeout_ms: DOWNLOAD_TIMEOUT_MS,
19731
+ retry_on_reject: true
18808
19732
  });
18809
19733
  if (!response.ok) {
18810
19734
  const body = await response.text();
@@ -18858,6 +19782,12 @@ var durationFromSegments = (segments) => {
18858
19782
  // src/exulu/recall/service.ts
18859
19783
  var TABLE3 = "transcription_jobs";
18860
19784
  var DEFAULT_BOT_NAME = "Company Notetaker";
19785
+ var RECONCILE_STALE_MS = 10 * 60 * 1e3;
19786
+ var RECONCILE_PROBE_QUIET_MS = 60 * 60 * 1e3;
19787
+ var RECONCILE_GIVE_UP_MS = 24 * 60 * 60 * 1e3;
19788
+ var POST_PROCESSING_REDO_STALE_MS = 30 * 60 * 1e3;
19789
+ var POST_PROCESSING_PROMPT_TIMEOUT_MS = 10 * 60 * 1e3;
19790
+ var BOT_ENDED_STATUSES = ["done", "call_ended"];
18861
19791
  var log4 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
18862
19792
  var parseJson = (v) => {
18863
19793
  if (v == null) return null;
@@ -18934,7 +19864,9 @@ var recallService = {
18934
19864
  target_rights_mode: input.target_rights_mode ?? "private",
18935
19865
  target_rbac_users: input.target_rbac_users ? JSON.stringify(input.target_rbac_users) : null,
18936
19866
  target_rbac_roles: input.target_rbac_roles ? JSON.stringify(input.target_rbac_roles) : null,
18937
- post_processing_prompts: input.post_processing_prompts ? JSON.stringify(input.post_processing_prompts) : null,
19867
+ // Normalized to NULL when empty so the reconcile sweep's redo select
19868
+ // ("prompts configured but never ran") can never match a no-prompt row.
19869
+ post_processing_prompts: input.post_processing_prompts?.length ? JSON.stringify(input.post_processing_prompts) : null,
18938
19870
  rights_mode: "private",
18939
19871
  created_by: input.userId,
18940
19872
  createdAt: now,
@@ -19003,7 +19935,13 @@ var recallService = {
19003
19935
  return;
19004
19936
  }
19005
19937
  const { db: db2 } = await postgresClient();
19006
- const claimed = await db2(TABLE3).where({ id: jobId }).whereNotIn("status", ["transcribing", "awaiting_review", "saved", "failed"]).update({
19938
+ const claimed = await db2(TABLE3).where({ id: jobId }).whereNotIn("status", [
19939
+ "transcribing",
19940
+ "awaiting_review",
19941
+ "saved",
19942
+ "failed",
19943
+ "cancelled"
19944
+ ]).update({
19007
19945
  recall_recording_id: recordingId,
19008
19946
  status: "transcribing",
19009
19947
  updatedAt: /* @__PURE__ */ new Date()
@@ -19095,14 +20033,55 @@ var recallService = {
19095
20033
  log4(`post-processing for job ${jobId} skipped: no transcript.`);
19096
20034
  return [];
19097
20035
  }
20036
+ const claimed = await db2(TABLE3).where({ id: jobId }).whereRaw(
20037
+ `(post_processing_outputs IS NULL OR (post_processing_outputs::text = '[]' AND "updatedAt" < ?))`,
20038
+ [new Date(Date.now() - POST_PROCESSING_REDO_STALE_MS)]
20039
+ ).update({ post_processing_outputs: "[]", updatedAt: /* @__PURE__ */ new Date() });
20040
+ if (!claimed) {
20041
+ log4(`post-processing for job ${jobId} already in flight; skipping.`);
20042
+ return [];
20043
+ }
19098
20044
  const outputs = [];
19099
20045
  for (const p of prompts) {
19100
20046
  outputs.push(await this._runOnePrompt(job, p.prompt_id, p.agent_id));
20047
+ await this._update(jobId, {});
19101
20048
  }
19102
- await this._update(jobId, {
19103
- post_processing_outputs: JSON.stringify(outputs)
19104
- });
19105
- return outputs;
20049
+ return this._mergeOutputs(jobId, outputs);
20050
+ },
20051
+ /**
20052
+ * Upsert entries into post_processing_outputs without clobbering concurrent
20053
+ * writers: optimistic-concurrency loop — read, merge by {prompt_id,
20054
+ * agent_id}, write only while the column still matches the snapshot.
20055
+ */
20056
+ async _mergeOutputs(jobId, entries) {
20057
+ const { db: db2 } = await postgresClient();
20058
+ for (let attempt = 1; attempt <= 5; attempt++) {
20059
+ const row = await db2(TABLE3).where({ id: jobId }).first();
20060
+ if (!row) return entries;
20061
+ const raw = row.post_processing_outputs ?? null;
20062
+ const snapshot = raw == null ? null : typeof raw === "string" ? raw : JSON.stringify(raw);
20063
+ const current = parseJson(raw) ?? [];
20064
+ const merged = [
20065
+ ...current.filter(
20066
+ (o) => !entries.some(
20067
+ (e) => e.prompt_id === o.prompt_id && e.agent_id === o.agent_id
20068
+ )
20069
+ ),
20070
+ ...entries
20071
+ ];
20072
+ const updated = await db2(TABLE3).where({ id: jobId }).whereRaw(
20073
+ "post_processing_outputs::jsonb IS NOT DISTINCT FROM ?::jsonb",
20074
+ [snapshot]
20075
+ ).update({
20076
+ post_processing_outputs: JSON.stringify(merged),
20077
+ updatedAt: /* @__PURE__ */ new Date()
20078
+ });
20079
+ if (updated) return merged;
20080
+ }
20081
+ log4(
20082
+ `post-processing outputs for job ${jobId} kept changing under the merge; leaving the concurrent writer's data in place.`
20083
+ );
20084
+ return entries;
19106
20085
  },
19107
20086
  /**
19108
20087
  * Manual single-prompt run (re-run from the review sheet). Upserts the matching
@@ -19113,13 +20092,14 @@ var recallService = {
19113
20092
  const dbRow = await db2(TABLE3).where({ id: jobId }).first();
19114
20093
  if (!dbRow) throw new Error(`transcription_job ${jobId} not found`);
19115
20094
  const job = this._row(dbRow);
20095
+ const claimInFlight = dbRow.post_processing_outputs != null && (job.post_processing_outputs?.length ?? 0) === 0 && Date.now() - new Date(job.updatedAt).getTime() < POST_PROCESSING_REDO_STALE_MS;
20096
+ if (claimInFlight) {
20097
+ throw new Error(
20098
+ "POST_PROCESSING_IN_FLIGHT: the automatic post-processing run is still in progress; its results will appear shortly."
20099
+ );
20100
+ }
19116
20101
  const result = await this._runOnePrompt(job, promptId, agentId);
19117
- const existing = job.post_processing_outputs ?? [];
19118
- const next = existing.filter(
19119
- (o) => !(o.prompt_id === promptId && o.agent_id === agentId)
19120
- );
19121
- next.push(result);
19122
- await this._update(jobId, { post_processing_outputs: JSON.stringify(next) });
20102
+ await this._mergeOutputs(jobId, [result]);
19123
20103
  return result;
19124
20104
  },
19125
20105
  async _runOnePrompt(job, promptId, agentId) {
@@ -19144,7 +20124,7 @@ var recallService = {
19144
20124
  job.raw_segments ?? [],
19145
20125
  job.speakers ?? {}
19146
20126
  );
19147
- const { text } = await (0, import_ai11.generateText)({
20127
+ const { text } = await (0, import_ai8.generateText)({
19148
20128
  model: resolved.languageModel,
19149
20129
  system: agent.instructions || void 0,
19150
20130
  prompt: `${prompt.content}
@@ -19153,7 +20133,8 @@ var recallService = {
19153
20133
  Meeting transcript:
19154
20134
 
19155
20135
  ${transcriptText}`,
19156
- maxRetries: 3
20136
+ maxRetries: 3,
20137
+ abortSignal: AbortSignal.timeout(POST_PROCESSING_PROMPT_TIMEOUT_MS)
19157
20138
  });
19158
20139
  return {
19159
20140
  prompt_id: promptId,
@@ -19177,6 +20158,206 @@ ${transcriptText}`,
19177
20158
  };
19178
20159
  }
19179
20160
  },
20161
+ /**
20162
+ * Reconciliation sweep: re-drive recall jobs whose webhook event was lost
20163
+ * (ACK-first delivery + crash/restart = no redelivery). Called periodically
20164
+ * from the reconcile loop. Returns the number of jobs it acted on.
20165
+ *
20166
+ * Idempotent by construction: every recovery path funnels into the same
20167
+ * guarded transitions the webhook handlers use (_onRecordingDone's atomic
20168
+ * claim, _onTranscriptDone's already-processed check, runPostProcessing's
20169
+ * skip-if-outputs-exist), so a webhook racing the sweep is harmless.
20170
+ */
20171
+ async reconcileOnce(limit = 10) {
20172
+ if (!recallEnabled()) return 0;
20173
+ const { db: db2 } = await postgresClient();
20174
+ const now = Date.now();
20175
+ const stuck = await db2(TABLE3).where({ source: "recall" }).whereIn("status", ["queued", "transcribing"]).whereRaw(`("join_at" IS NULL OR "join_at" < ?)`, [
20176
+ new Date(now - RECONCILE_STALE_MS)
20177
+ ]).where("updatedAt", "<", new Date(now - RECONCILE_STALE_MS)).orderBy("updatedAt", "asc").limit(limit);
20178
+ const redo = await db2(TABLE3).where({ source: "recall", status: "awaiting_review" }).whereNotNull("post_processing_prompts").whereRaw("post_processing_prompts::text <> '[]'").whereRaw("(raw_segments IS NOT NULL AND raw_segments::text <> '[]')").whereRaw(
20179
+ "(post_processing_outputs IS NULL OR post_processing_outputs::text = '[]')"
20180
+ ).where("updatedAt", "<", new Date(now - POST_PROCESSING_REDO_STALE_MS)).orderBy("updatedAt", "asc").limit(limit);
20181
+ let acted = 0;
20182
+ for (const dbRow of stuck) {
20183
+ const job = this._row(dbRow);
20184
+ try {
20185
+ if (await this._reconcileStuckJob(job)) acted++;
20186
+ } catch (err) {
20187
+ if (await this._reconcileError(job, err)) acted++;
20188
+ }
20189
+ }
20190
+ for (const dbRow of redo) {
20191
+ try {
20192
+ log4(`reconcile: re-running lost post-processing for job ${dbRow.id}`);
20193
+ const outputs = await this.runPostProcessing(dbRow.id);
20194
+ if (outputs.length > 0) acted++;
20195
+ } catch (err) {
20196
+ log4(
20197
+ `post-processing redo failed for job ${dbRow.id}: ${err.message}`
20198
+ );
20199
+ }
20200
+ }
20201
+ return acted;
20202
+ },
20203
+ async _reconcileStuckJob(job) {
20204
+ if (!job.recall_bot_id) {
20205
+ await this._fail(job.id, "bot was never launched (lost during creation)");
20206
+ return true;
20207
+ }
20208
+ if (job.status === "queued") return this._reconcileQueued(job);
20209
+ if (job.status === "transcribing") return this._reconcileTranscribing(job);
20210
+ return false;
20211
+ },
20212
+ /**
20213
+ * A recovery step threw. 404 means the Recall object is gone — terminal.
20214
+ * Otherwise apply the 24h give-up (API errors must not defer it forever),
20215
+ * and touch the row so it rotates to the back of the sweep window.
20216
+ */
20217
+ async _reconcileError(job, err) {
20218
+ const message = err.message ?? String(err);
20219
+ if (err.status === 404) {
20220
+ await this._fail(
20221
+ job.id,
20222
+ `Recall no longer knows this recording: ${message}`
20223
+ );
20224
+ return true;
20225
+ }
20226
+ if (this._pastGiveUp(job)) {
20227
+ await this._fail(
20228
+ job.id,
20229
+ `still stuck 24 hours after the meeting start (last error: ${message})`
20230
+ );
20231
+ return true;
20232
+ }
20233
+ log4(`reconcile failed for job ${job.id}: ${message}`);
20234
+ await this._update(job.id, {});
20235
+ return false;
20236
+ },
20237
+ /** A queued row: the recording.done event (or the whole bot lifecycle) was lost. */
20238
+ async _reconcileQueued(job) {
20239
+ const ended = !!job.bot_status && BOT_ENDED_STATUSES.includes(job.bot_status);
20240
+ const quietMs = Date.now() - new Date(job.updatedAt).getTime();
20241
+ if (!ended && quietMs < RECONCILE_PROBE_QUIET_MS) return false;
20242
+ const bot = await recallClient.retrieveBot(job.recall_bot_id);
20243
+ const code = bot?.status_changes?.at(-1)?.code ?? null;
20244
+ if (code === "fatal") {
20245
+ await this._fail(job.id, "bot fatal (recovered by reconciliation)");
20246
+ return true;
20247
+ }
20248
+ const recording = bot?.recordings?.[0] ?? null;
20249
+ if (recording?.id) {
20250
+ const readiness = await this._recordingReadiness(recording);
20251
+ if (readiness === "failed") {
20252
+ await this._fail(job.id, "recording failed (found by reconciliation)");
20253
+ return true;
20254
+ }
20255
+ if (readiness === "done") {
20256
+ log4(`reconcile: driving lost recording.done for job ${job.id}`);
20257
+ await this._onRecordingDone(job.id, recording.id);
20258
+ return true;
20259
+ }
20260
+ } else if (code === "done") {
20261
+ await this._fail(job.id, "bot finished without a recording");
20262
+ return true;
20263
+ }
20264
+ if (this._pastGiveUp(job)) {
20265
+ await this._fail(
20266
+ job.id,
20267
+ "no recording within 24 hours of the meeting start"
20268
+ );
20269
+ return true;
20270
+ }
20271
+ await this._update(
20272
+ job.id,
20273
+ code && code !== job.bot_status ? { bot_status: code } : {}
20274
+ );
20275
+ return false;
20276
+ },
20277
+ /** Whether a recording is safe to transcribe, checking the full recording
20278
+ * object when the bot payload carries no status. */
20279
+ async _recordingReadiness(recording) {
20280
+ const codeOf = (c) => c === "done" ? "done" : c === "failed" ? "failed" : c ? "processing" : null;
20281
+ const fromBot = codeOf(recording.status?.code);
20282
+ if (fromBot) return fromBot;
20283
+ if (recording.completed_at) return "done";
20284
+ const full = await recallClient.retrieveRecording(recording.id);
20285
+ const fromFull = codeOf(full?.status?.code);
20286
+ if (fromFull) return fromFull;
20287
+ return full?.completed_at || recordingDurationSeconds(full) != null ? "done" : "processing";
20288
+ },
20289
+ /**
20290
+ * A transcribing row: transcript.done was lost, or the original
20291
+ * recording.done handling crashed between the status claim and
20292
+ * createAsyncTranscript (in which case Recall was never asked to
20293
+ * transcribe and no transcript.* event will ever arrive).
20294
+ */
20295
+ async _reconcileTranscribing(job) {
20296
+ let recordingId = job.recall_recording_id;
20297
+ if (!recordingId && !job.recall_transcript_id) {
20298
+ const bot = await recallClient.retrieveBot(job.recall_bot_id);
20299
+ recordingId = bot?.recordings?.[0]?.id ?? null;
20300
+ }
20301
+ let transcriptId = job.recall_transcript_id;
20302
+ if (!transcriptId) {
20303
+ if (!recordingId) return this._transcribingNotReady(job);
20304
+ const rec = await recallClient.retrieveRecording(recordingId);
20305
+ const shortcut = rec?.media_shortcuts?.transcript;
20306
+ if (shortcut?.id) {
20307
+ transcriptId = shortcut.id;
20308
+ await this._update(job.id, {
20309
+ recall_recording_id: recordingId,
20310
+ recall_transcript_id: transcriptId
20311
+ });
20312
+ } else {
20313
+ const { db: db2 } = await postgresClient();
20314
+ const claimed = await db2(TABLE3).where({ id: job.id, status: "transcribing" }).whereNull("recall_transcript_id").where("updatedAt", "<", new Date(Date.now() - RECONCILE_STALE_MS)).update({ recall_recording_id: recordingId, updatedAt: /* @__PURE__ */ new Date() });
20315
+ if (!claimed) return false;
20316
+ log4(
20317
+ `reconcile: re-requesting transcript for job ${job.id} (lost before createAsyncTranscript)`
20318
+ );
20319
+ const transcript2 = await recallClient.createAsyncTranscript(
20320
+ recordingId,
20321
+ job.language || "auto"
20322
+ );
20323
+ await this._update(job.id, {
20324
+ recall_transcript_id: transcript2.id ?? null
20325
+ });
20326
+ return true;
20327
+ }
20328
+ }
20329
+ const transcript = await recallClient.retrieveTranscript(transcriptId);
20330
+ const transcriptCode = transcript?.status?.code ?? null;
20331
+ if (transcriptCode === "error" || transcriptCode === "failed") {
20332
+ await this._fail(
20333
+ job.id,
20334
+ `transcript failed at Recall: ${transcript?.status?.sub_code || transcriptCode}`
20335
+ );
20336
+ return true;
20337
+ }
20338
+ if (transcript?.data?.download_url) {
20339
+ log4(`reconcile: driving lost transcript.done for job ${job.id}`);
20340
+ await this._onTranscriptDone(job.id, transcriptId, recordingId);
20341
+ return true;
20342
+ }
20343
+ return this._transcribingNotReady(job);
20344
+ },
20345
+ /** Transcript not ready yet: wait (touch) or give up after the hard cap. */
20346
+ async _transcribingNotReady(job) {
20347
+ if (this._pastGiveUp(job)) {
20348
+ await this._fail(
20349
+ job.id,
20350
+ "transcript was not ready within 24 hours of the meeting start"
20351
+ );
20352
+ return true;
20353
+ }
20354
+ await this._update(job.id, {});
20355
+ return false;
20356
+ },
20357
+ _pastGiveUp(job) {
20358
+ const startedAt = new Date(job.join_at ?? job.createdAt).getTime();
20359
+ return Number.isFinite(startedAt) && Date.now() - startedAt > RECONCILE_GIVE_UP_MS;
20360
+ },
19180
20361
  async _findJob(botId, recordingId, transcriptId) {
19181
20362
  const { db: db2 } = await postgresClient();
19182
20363
  let dbRow;
@@ -19211,7 +20392,7 @@ ${transcriptText}`,
19211
20392
 
19212
20393
  // src/exulu/email-inbound/config.ts
19213
20394
  init_cjs_shims();
19214
- init_token_store();
20395
+ init_credential_store();
19215
20396
  var EMAIL_INBOUND_CONFIG_KEY = "email_inbound";
19216
20397
  var parseValue = (value) => {
19217
20398
  if (!value) return {};
@@ -19410,7 +20591,8 @@ function createExuluContextsTypeDefs(table) {
19410
20591
  ${enumValues}
19411
20592
  }`;
19412
20593
  }).filter((enumDef) => enumDef !== null).join("\n");
19413
- let fields = table.fields.map((field) => {
20594
+ const graphqlFields = table.fields.filter((field) => field.hidden !== true);
20595
+ let fields = graphqlFields.map((field) => {
19414
20596
  let type;
19415
20597
  type = mapExuluFieldTypesToGraphqlTypes(field);
19416
20598
  const required = field.required ? "!" : "";
@@ -19431,6 +20613,7 @@ function createExuluContextsTypeDefs(table) {
19431
20613
  fields.push(" systemInstructions: String");
19432
20614
  fields.push(" workflows: AgentWorkflows");
19433
20615
  fields.push(" slug: String");
20616
+ fields.push(" guest_has_password: Boolean");
19434
20617
  }
19435
20618
  if (table.name.singular === "workflow_template") {
19436
20619
  fields.push(" variables: [String]");
@@ -19447,16 +20630,20 @@ function createExuluContextsTypeDefs(table) {
19447
20630
  }
19448
20631
  `;
19449
20632
  const rbacInputField = table.RBAC ? " RBAC: RBACInput" : "";
20633
+ const inputFields = table.fields.filter((f) => f.name !== "guest_password_hash");
20634
+ const inputExtra = table.name.singular === "agent" ? " guest_password: String" : "";
19450
20635
  const inputDef = `
19451
20636
  input ${table.name.singular}Input {
19452
- ${table.fields.map((f) => ` ${f.name}: ${mapExuluFieldTypesToGraphqlTypes(f)}`).join("\n")}
20637
+ ${inputFields.map((f) => ` ${f.name}: ${mapExuluFieldTypesToGraphqlTypes(f)}`).join("\n")}
20638
+ ${inputExtra}
19453
20639
  ${rbacInputField}
19454
20640
  }
19455
20641
  `;
19456
20642
  return enumDefs + typeDef + inputDef;
19457
20643
  }
19458
20644
  function createExuluContextsFilterTypeDefs(table) {
19459
- const fieldFilters = table.fields.map((field) => {
20645
+ const filterFields = table.fields.filter((field) => field.hidden !== true);
20646
+ const fieldFilters = filterFields.map((field) => {
19460
20647
  let type;
19461
20648
  if (field.type === "enum" && field.enumValues) {
19462
20649
  type = `${field.name}Enum`;
@@ -19469,7 +20656,7 @@ function createExuluContextsFilterTypeDefs(table) {
19469
20656
  let operatorTypes = "";
19470
20657
  let enumFilterOperators = [];
19471
20658
  const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
19472
- enumFilterOperators = table.fields.filter((field) => field.type === "enum" && field.enumValues).map((field) => {
20659
+ enumFilterOperators = filterFields.filter((field) => field.type === "enum" && field.enumValues).map((field) => {
19473
20660
  const enumTypeName = `${field.name}Enum`;
19474
20661
  return `
19475
20662
  input FilterOperator${enumTypeName} {
@@ -21028,12 +22215,12 @@ type LiteLLMModel {
21028
22215
  nonArchived().andWhere((b) => b.whereNull("chunks_count").orWhere("chunks_count", "<=", 0)).count("* as c").first(),
21029
22216
  nonArchived().where("embeddings_updated_at", "<=", staleCutoff).count("* as c").first()
21030
22217
  ]);
21031
- const num = (v) => v == null ? 0 : Number(v);
22218
+ const num2 = (v) => v == null ? 0 : Number(v);
21032
22219
  return {
21033
- item_count: num(itemRow?.c),
21034
- chunk_total: num(chunkRow?.s),
21035
- stuck_count: num(stuckRow?.c),
21036
- stale_count: num(staleRow?.c)
22220
+ item_count: num2(itemRow?.c),
22221
+ chunk_total: num2(chunkRow?.s),
22222
+ stuck_count: num2(stuckRow?.c),
22223
+ stale_count: num2(staleRow?.c)
21037
22224
  };
21038
22225
  } catch (err) {
21039
22226
  console.error("[EXULU] computeContextAggregates failed for", contextId, err);
@@ -21239,6 +22426,7 @@ type LiteLLMModel {
21239
22426
  if (agenticRetrievalTool) {
21240
22427
  allTools.push(agenticRetrievalTool);
21241
22428
  }
22429
+ allTools.push(createKbEditorPickerTool());
21242
22430
  }
21243
22431
  if (search && search.trim()) {
21244
22432
  const searchTerm = search.toLowerCase().trim();
@@ -21808,19 +22996,19 @@ var import_node_crypto14 = require("crypto");
21808
22996
  var import_api2 = require("@opentelemetry/api");
21809
22997
  init_check_record_access();
21810
22998
  var import_jszip3 = __toESM(require("jszip"), 1);
21811
- var import_ai15 = require("ai");
22999
+ var import_ai12 = require("ai");
21812
23000
  var import_cookie_parser = __toESM(require("cookie-parser"), 1);
21813
23001
  init_statistics2();
21814
23002
 
21815
23003
  // src/exulu/suggestions.ts
21816
23004
  init_cjs_shims();
21817
- var import_ai12 = require("ai");
21818
- var import_zod15 = require("zod");
23005
+ var import_ai9 = require("ai");
23006
+ var import_zod17 = require("zod");
21819
23007
  var SUGGESTIONS_SYSTEM_PROMPT = "You generate short follow-up message suggestions for the user. You are NOT continuing the conversation as the assistant \u2014 you are predicting what the user might want to say next. Suggest up to 3 short follow-up questions or messages the user might want to send next. Each suggestion must be written from the user's perspective (first person) and be 12 words or fewer. You MUST submit your answer by calling the `submit_suggestions` tool exactly once. Do not emit any plain text \u2014 only the tool call.";
21820
- var submitSuggestionsTool = (0, import_ai12.tool)({
23008
+ var submitSuggestionsTool = (0, import_ai9.tool)({
21821
23009
  description: "Submit the final list of follow-up message suggestions for the user. Must be called exactly once. Each suggestion is written from the user's perspective (first person) and is 12 words or fewer.",
21822
- inputSchema: import_zod15.z.object({
21823
- suggestions: import_zod15.z.array(import_zod15.z.string()).max(3)
23010
+ inputSchema: import_zod17.z.object({
23011
+ suggestions: import_zod17.z.array(import_zod17.z.string()).max(3)
21824
23012
  })
21825
23013
  });
21826
23014
  var MAX_CHARS_PER_MESSAGE = 1e4;
@@ -21856,11 +23044,11 @@ var generateSuggestions = async ({
21856
23044
 
21857
23045
  ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
21858
23046
  const trimmed = trimMessagesForSuggestions(messages);
21859
- const { toolCalls, totalUsage } = await (0, import_ai12.generateText)({
23047
+ const { toolCalls, totalUsage } = await (0, import_ai9.generateText)({
21860
23048
  temperature: 0,
21861
23049
  model: languageModel,
21862
23050
  system,
21863
- messages: await (0, import_ai12.convertToModelMessages)(trimmed, {
23051
+ messages: await (0, import_ai9.convertToModelMessages)(trimmed, {
21864
23052
  ignoreIncompleteToolCalls: true
21865
23053
  }),
21866
23054
  tools: { submit_suggestions: submitSuggestionsTool },
@@ -21914,7 +23102,7 @@ init_context_budget();
21914
23102
  // src/exulu/compact-session.ts
21915
23103
  init_cjs_shims();
21916
23104
  var import_node_crypto9 = require("crypto");
21917
- var import_ai13 = require("ai");
23105
+ var import_ai10 = require("ai");
21918
23106
  init_truncate_tool_output();
21919
23107
  init_context_budget();
21920
23108
  var CompactionInsufficientError = class extends Error {
@@ -21973,7 +23161,7 @@ var compactSession = async ({
21973
23161
  }) => {
21974
23162
  const budget = deriveContextBudget(contextWindow);
21975
23163
  const rows = await getAgentMessages({ session: sessionID, user: user.id });
21976
- const all = await (0, import_ai13.validateUIMessages)({ messages: rows.map((r) => JSON.parse(r.content)) });
23164
+ const all = await (0, import_ai10.validateUIMessages)({ messages: rows.map((r) => JSON.parse(r.content)) });
21977
23165
  const history = sliceHistoryAtCheckpoint(all);
21978
23166
  const { head, tail } = splitTail(history, budget.compactionTailTokens);
21979
23167
  if (head.length === 0) {
@@ -21988,7 +23176,7 @@ var compactSession = async ({
21988
23176
 
21989
23177
  Focus especially on: ${steer.trim()}` : SUMMARY_SYSTEM;
21990
23178
  const doSummarize = summarize ?? (async ({ system: sys, prompt, maxOutputTokens }) => {
21991
- const { text } = await (0, import_ai13.generateText)({
23179
+ const { text } = await (0, import_ai10.generateText)({
21992
23180
  model: languageModel,
21993
23181
  system: sys,
21994
23182
  prompt,
@@ -22377,6 +23565,129 @@ init_admin_client();
22377
23565
  init_env();
22378
23566
  init_activity_client();
22379
23567
  init_budget_service();
23568
+
23569
+ // src/exulu/litellm/usage-view.ts
23570
+ init_cjs_shims();
23571
+ init_tags();
23572
+ init_activity_client();
23573
+ init_budget_service();
23574
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
23575
+ var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
23576
+ var MAX_WINDOW_DAYS = 92;
23577
+ var DEFAULT_WINDOW_DAYS = 30;
23578
+ var parseDateParam = (raw) => {
23579
+ if (raw === void 0 || raw === null || raw === "") return void 0;
23580
+ if (typeof raw !== "string") return null;
23581
+ if (DATE_ONLY_RE.test(raw)) return raw;
23582
+ const dt = new Date(raw);
23583
+ if (Number.isNaN(dt.getTime())) return null;
23584
+ return dt.toISOString().slice(0, 10);
23585
+ };
23586
+ var ymdShift = (ymd2, days) => new Date(Date.parse(ymd2) + days * DAY_MS2).toISOString().slice(0, 10);
23587
+ function resolveUsageWindow(startRaw, endRaw, now = /* @__PURE__ */ new Date()) {
23588
+ const start = parseDateParam(startRaw);
23589
+ const end = parseDateParam(endRaw);
23590
+ if (start === null || end === null) return null;
23591
+ const end_date = end ?? now.toISOString().slice(0, 10);
23592
+ let start_date = start ?? ymdShift(end_date, -(DEFAULT_WINDOW_DAYS - 1));
23593
+ if (start_date > end_date) return null;
23594
+ const days = Math.round((Date.parse(end_date) - Date.parse(start_date)) / DAY_MS2) + 1;
23595
+ if (days > MAX_WINDOW_DAYS) {
23596
+ start_date = ymdShift(end_date, -(MAX_WINDOW_DAYS - 1));
23597
+ }
23598
+ return { start_date, end_date };
23599
+ }
23600
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
23601
+ var zeroMetrics = () => ({
23602
+ spend: 0,
23603
+ prompt_tokens: 0,
23604
+ completion_tokens: 0,
23605
+ total_tokens: 0,
23606
+ successful_requests: 0,
23607
+ failed_requests: 0,
23608
+ api_requests: 0
23609
+ });
23610
+ var readMetrics = (source) => {
23611
+ const m = source?.metrics ?? {};
23612
+ return {
23613
+ spend: num(m.spend ?? source?.spend),
23614
+ prompt_tokens: num(m.prompt_tokens ?? source?.prompt_tokens),
23615
+ completion_tokens: num(m.completion_tokens ?? source?.completion_tokens),
23616
+ total_tokens: num(m.total_tokens ?? source?.total_tokens),
23617
+ successful_requests: num(
23618
+ m.successful_requests ?? source?.successful_requests
23619
+ ),
23620
+ failed_requests: num(m.failed_requests ?? source?.failed_requests),
23621
+ api_requests: num(m.api_requests ?? source?.api_requests)
23622
+ };
23623
+ };
23624
+ var addMetrics = (into, add) => {
23625
+ into.spend += add.spend;
23626
+ into.prompt_tokens += add.prompt_tokens;
23627
+ into.completion_tokens += add.completion_tokens;
23628
+ into.total_tokens += add.total_tokens;
23629
+ into.successful_requests += add.successful_requests;
23630
+ into.failed_requests += add.failed_requests;
23631
+ into.api_requests += add.api_requests;
23632
+ };
23633
+ function projectMyUsage(raw) {
23634
+ const results = Array.isArray(raw?.results) ? raw.results : Array.isArray(raw) ? raw : [];
23635
+ const totals = zeroMetrics();
23636
+ const byDate = /* @__PURE__ */ new Map();
23637
+ const byModelMap = /* @__PURE__ */ new Map();
23638
+ for (const result of results) {
23639
+ const date = typeof result?.date === "string" ? result.date : null;
23640
+ if (!date) continue;
23641
+ const metrics = readMetrics(result);
23642
+ addMetrics(totals, metrics);
23643
+ const day = byDate.get(date) ?? zeroMetrics();
23644
+ addMetrics(day, metrics);
23645
+ byDate.set(date, day);
23646
+ const models2 = result?.breakdown && typeof result.breakdown.models === "object" ? result.breakdown.models : {};
23647
+ for (const [model, entry] of Object.entries(models2 ?? {})) {
23648
+ const acc = byModelMap.get(model) ?? zeroMetrics();
23649
+ addMetrics(acc, readMetrics(entry));
23650
+ byModelMap.set(model, acc);
23651
+ }
23652
+ }
23653
+ const daily = [...byDate.entries()].map(([date, m]) => ({ date, ...m })).sort((a, b) => a.date.localeCompare(b.date));
23654
+ const byModel = [...byModelMap.entries()].map(([model, m]) => ({
23655
+ model,
23656
+ spend: m.spend,
23657
+ prompt_tokens: m.prompt_tokens,
23658
+ completion_tokens: m.completion_tokens,
23659
+ total_tokens: m.total_tokens,
23660
+ successful_requests: m.successful_requests,
23661
+ failed_requests: m.failed_requests
23662
+ })).sort((a, b) => b.spend - a.spend);
23663
+ return { totals, daily, byModel };
23664
+ }
23665
+ async function getMyUsageView(userId, window) {
23666
+ const settings = await getBudgetSettings();
23667
+ if (!settings.show_user_budget_in_chat) return null;
23668
+ const tag = budgetTagFor("user", userId);
23669
+ if (!tag) return null;
23670
+ const daysInRange = Math.round(
23671
+ (Date.parse(window.end_date) - Date.parse(window.start_date)) / DAY_MS2
23672
+ ) + 1;
23673
+ const raw = await getTagDailyActivity({
23674
+ startDate: window.start_date,
23675
+ endDate: window.end_date,
23676
+ tags: [tag],
23677
+ page: 1,
23678
+ pageSize: Math.min(daysInRange + 100, 1e4)
23679
+ });
23680
+ const { totals, daily, byModel } = projectMyUsage(raw);
23681
+ return {
23682
+ window,
23683
+ display: settings.user_budget_display,
23684
+ totals,
23685
+ daily,
23686
+ byModel
23687
+ };
23688
+ }
23689
+
23690
+ // src/exulu/routes.ts
22380
23691
  var import_multer = __toESM(require("multer"), 1);
22381
23692
  var import_busboy = __toESM(require("busboy"), 1);
22382
23693
 
@@ -22521,7 +23832,7 @@ function checkApiKeyScope(user, agentId) {
22521
23832
  // src/exulu/openai-gateway.ts
22522
23833
  init_cjs_shims();
22523
23834
  var import_express2 = require("express");
22524
- var import_ai14 = require("ai");
23835
+ var import_ai11 = require("ai");
22525
23836
 
22526
23837
  // src/exulu/openai-transformer.ts
22527
23838
  init_cjs_shims();
@@ -22629,7 +23940,7 @@ function convertOpenAIToolsToAiSdkTools(tools) {
22629
23940
  t.function.name,
22630
23941
  {
22631
23942
  description: t.function.description ?? "",
22632
- inputSchema: (0, import_ai14.jsonSchema)({
23943
+ inputSchema: (0, import_ai11.jsonSchema)({
22633
23944
  type: "object",
22634
23945
  properties: params.properties ?? {},
22635
23946
  ...params.required ? { required: params.required } : {}
@@ -23021,14 +24332,14 @@ ${project.description}` : ""}` : "",
23021
24332
  res.setHeader("Content-Type", "text/event-stream");
23022
24333
  res.setHeader("Cache-Control", "no-cache");
23023
24334
  res.setHeader("Connection", "keep-alive");
23024
- const result = (0, import_ai14.streamText)({
24335
+ const result = (0, import_ai11.streamText)({
23025
24336
  model: languageModel,
23026
24337
  system: systemPrompt || void 0,
23027
24338
  messages: coreMessages,
23028
24339
  tools: hasTools ? activeTools : void 0,
23029
24340
  maxRetries: 2,
23030
24341
  prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
23031
- stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)],
24342
+ stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai11.stepCountIs)(turnBudget)],
23032
24343
  onError: (error) => {
23033
24344
  console.error("[OPENAI GATEWAY] stream error:", error);
23034
24345
  }
@@ -23061,14 +24372,14 @@ ${project.description}` : ""}` : "",
23061
24372
  const usage = await result.usage;
23062
24373
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
23063
24374
  } else {
23064
- const { text, usage } = await (0, import_ai14.generateText)({
24375
+ const { text, usage } = await (0, import_ai11.generateText)({
23065
24376
  model: languageModel,
23066
24377
  system: systemPrompt || void 0,
23067
24378
  messages: coreMessages,
23068
24379
  tools: hasTools ? activeTools : void 0,
23069
24380
  maxRetries: 2,
23070
24381
  prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
23071
- stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)]
24382
+ stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai11.stepCountIs)(turnBudget)]
23072
24383
  });
23073
24384
  res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
23074
24385
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
@@ -23100,11 +24411,11 @@ var getEnabledSkills = async (agent, disabledSkills = []) => {
23100
24411
  return enabledSkills;
23101
24412
  };
23102
24413
 
23103
- // src/exulu/oauth/callback-handler.ts
24414
+ // src/exulu/auth/callback-handler.ts
23104
24415
  init_cjs_shims();
23105
24416
  init_registry();
23106
24417
  init_flow();
23107
- init_token_store();
24418
+ init_credential_store();
23108
24419
  var escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
23109
24420
  var renderResultPage = ({ success, message }) => `<!doctype html>
23110
24421
  <html lang="en">
@@ -23156,7 +24467,7 @@ var handleOauthCallback = async (req, res) => {
23156
24467
  "This authorization link is invalid or has expired. Return to your chat and run the tool again to get a fresh link."
23157
24468
  );
23158
24469
  }
23159
- const config = oauthRegistry.getByProvider(parsed.provider);
24470
+ const config = authRegistry.getByProvider(parsed.provider);
23160
24471
  if (!config) {
23161
24472
  return send(
23162
24473
  404,
@@ -23164,13 +24475,31 @@ var handleOauthCallback = async (req, res) => {
23164
24475
  `No OAuth configuration is registered for provider "${parsed.provider}".`
23165
24476
  );
23166
24477
  }
24478
+ if (config.authType !== "oauth") {
24479
+ return send(
24480
+ 500,
24481
+ false,
24482
+ `Provider "${parsed.provider}" is not configured for OAuth. This callback only handles OAuth flows.`
24483
+ );
24484
+ }
23167
24485
  try {
23168
24486
  const record = await exchangeCodeForTokens({
23169
24487
  config,
23170
24488
  code,
23171
24489
  codeVerifier: parsed.codeVerifier
23172
24490
  });
23173
- await oauthTokenStore.upsert(parsed.provider, parsed.userId, parsed.toolId, record);
24491
+ await credentialStore.upsert({
24492
+ provider: parsed.provider,
24493
+ userId: parsed.userId,
24494
+ authType: "oauth",
24495
+ data: {
24496
+ accessToken: record.accessToken,
24497
+ refreshToken: record.refreshToken ?? null,
24498
+ tokenType: record.tokenType ?? null,
24499
+ scopes: record.scopes ?? null,
24500
+ expiresAt: record.expiresAt ? record.expiresAt.toISOString() : null
24501
+ }
24502
+ });
23174
24503
  } catch (caught) {
23175
24504
  console.error("[EXULU] OAuth code exchange failed:", caught);
23176
24505
  return send(
@@ -23182,6 +24511,113 @@ var handleOauthCallback = async (req, res) => {
23182
24511
  return send(200, true, "You can close this tab and return to your chat.");
23183
24512
  };
23184
24513
 
24514
+ // src/exulu/auth/submit-handler.ts
24515
+ init_cjs_shims();
24516
+ var import_zod18 = require("zod");
24517
+ init_registry();
24518
+ init_credential_store();
24519
+ init_credentials_request();
24520
+ var bodySchema = import_zod18.z.object({
24521
+ nonce: import_zod18.z.string().min(1),
24522
+ values: import_zod18.z.record(import_zod18.z.string(), import_zod18.z.string())
24523
+ });
24524
+ async function handleCredentialSubmit(req, res) {
24525
+ const authResult = await requestValidators.authenticate(req);
24526
+ if (!authResult.user?.id) {
24527
+ res.status(401).json({ ok: false, error: "authentication required" });
24528
+ return;
24529
+ }
24530
+ const sessionUserId = authResult.user.id;
24531
+ let parsed;
24532
+ try {
24533
+ parsed = bodySchema.parse(req.body);
24534
+ } catch (caught) {
24535
+ res.status(400).json({ ok: false, error: "invalid body" });
24536
+ return;
24537
+ }
24538
+ const { nonce, values } = parsed;
24539
+ let nonceData;
24540
+ try {
24541
+ nonceData = verifyCredentialNonce(nonce);
24542
+ } catch (e) {
24543
+ res.status(401).json({
24544
+ ok: false,
24545
+ error: /expired/i.test(e.message) ? "nonce expired" : "nonce invalid"
24546
+ });
24547
+ return;
24548
+ }
24549
+ if (sessionUserId !== Number(nonceData.userId)) {
24550
+ res.status(403).json({ ok: false, error: "userId mismatch" });
24551
+ return;
24552
+ }
24553
+ const config = authRegistry.getByProvider(nonceData.provider);
24554
+ if (!config || config.authType !== "user_credentials") {
24555
+ res.status(400).json({
24556
+ ok: false,
24557
+ error: "provider is not a user_credentials provider"
24558
+ });
24559
+ return;
24560
+ }
24561
+ const expectedFields = new Set(config.fields.map((f) => f.name));
24562
+ const submittedFields = new Set(Object.keys(values));
24563
+ if (expectedFields.size !== submittedFields.size || [...expectedFields].some((f) => !submittedFields.has(f))) {
24564
+ res.status(400).json({
24565
+ ok: false,
24566
+ error: "field set mismatch"
24567
+ });
24568
+ return;
24569
+ }
24570
+ if (config.validate) {
24571
+ try {
24572
+ await config.validate(values);
24573
+ } catch (e) {
24574
+ res.status(400).json({
24575
+ ok: false,
24576
+ error: `validation failed: ${e.message}`
24577
+ });
24578
+ return;
24579
+ }
24580
+ }
24581
+ if (!Number.isInteger(sessionUserId) || sessionUserId <= 0) {
24582
+ res.status(401).json({ ok: false, error: "session invalid" });
24583
+ return;
24584
+ }
24585
+ await credentialStore.upsert({
24586
+ provider: nonceData.provider,
24587
+ userId: sessionUserId,
24588
+ authType: "user_credentials",
24589
+ data: values
24590
+ });
24591
+ res.status(200).json({ ok: true });
24592
+ }
24593
+
24594
+ // src/exulu/auth/manage-handlers.ts
24595
+ init_cjs_shims();
24596
+ init_credential_store();
24597
+ var handleCredentialList = async (req, res) => {
24598
+ const authResult = await requestValidators.authenticate(req);
24599
+ if (!authResult.user?.id) {
24600
+ res.status(authResult.code ?? 401).json({ ok: false, error: "authentication required" });
24601
+ return;
24602
+ }
24603
+ const credentials = await credentialStore.listByUser(authResult.user.id);
24604
+ res.status(200).json({ ok: true, credentials });
24605
+ };
24606
+ var handleCredentialDelete = async (req, res) => {
24607
+ const authResult = await requestValidators.authenticate(req);
24608
+ if (!authResult.user?.id) {
24609
+ res.status(authResult.code ?? 401).json({ ok: false, error: "authentication required" });
24610
+ return;
24611
+ }
24612
+ const provider = req.params.provider;
24613
+ if (!provider) {
24614
+ res.status(400).json({ ok: false, error: "provider is required" });
24615
+ return;
24616
+ }
24617
+ await credentialStore.delete(provider, authResult.user.id);
24618
+ res.status(200).json({ ok: true });
24619
+ };
24620
+
23185
24621
  // src/exulu/routes.ts
23186
24622
  init_flow();
23187
24623
 
@@ -23235,52 +24671,125 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
23235
24671
  return passed ? { ok: true } : { ok: false, reason: "signature mismatch" };
23236
24672
  };
23237
24673
 
23238
- // src/exulu/shared-artifacts.ts
24674
+ // src/exulu/public-agents.ts
23239
24675
  init_cjs_shims();
23240
- var import_bcryptjs4 = __toESM(require("bcryptjs"), 1);
23241
- var normalizeS3Key = (key, bucket) => {
23242
- const segments = key.split("/").filter((s, i) => !(i === 0 && s === "")).map((s) => decodeURIComponent(s));
23243
- if (segments[0] === bucket) segments.shift();
23244
- return segments.join("/");
23245
- };
23246
- var isHtmlKey = (key) => /\.html?$/i.test(key);
23247
- var deriveFilename = (key) => {
23248
- const base = key.split("/").pop() ?? key;
23249
- return base.split("_EXULU_").pop() ?? base;
23250
- };
23251
- var slugifyShareName = (input) => deriveFilename(input).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
23252
- var isExpired = (expiresAt, now) => {
23253
- if (!expiresAt) return false;
23254
- return new Date(expiresAt).getTime() <= now.getTime();
23255
- };
23256
- var validateCreateInput = (input, now) => {
23257
- if (!input.s3key) return { ok: false, message: "s3key is required." };
23258
- if (!input.name) return { ok: false, message: "name is required." };
23259
- const mode = input.auth_mode;
23260
- if (mode !== "public" && mode !== "password" && mode !== "regular") {
23261
- return { ok: false, message: "auth_mode must be public, password, or regular." };
23262
- }
23263
- if (mode === "password" && !input.password) {
23264
- return { ok: false, message: "A password is required for password mode." };
24676
+ var publicAgentView = (row, slug) => ({
24677
+ id: row.id,
24678
+ name: row.name ?? "",
24679
+ description: row.description ?? "",
24680
+ image: row.image ?? null,
24681
+ welcomemessage: row.welcomemessage ?? "",
24682
+ slug,
24683
+ guest_auth_mode: row.guest_auth_mode || "regular",
24684
+ guest_has_cover: !!row.guest_cover_image
24685
+ });
24686
+ var evaluateGuestChatAccess = async (agent, userId, guestPassword) => {
24687
+ if (agent.guest_access) {
24688
+ if (userId != null) return { allowed: true, via: "guest" };
24689
+ const mode = agent.guest_auth_mode || "regular";
24690
+ if (mode === "public") return { allowed: true, via: "guest" };
24691
+ if (mode === "password") {
24692
+ if (agent.guest_password_hash && guestPassword) {
24693
+ const ok = await verifySharePassword(guestPassword, agent.guest_password_hash);
24694
+ if (ok) return { allowed: true, via: "guest" };
24695
+ return { allowed: false, status: 401, message: "Incorrect password." };
24696
+ }
24697
+ return { allowed: false, status: 401, message: "Password required." };
24698
+ }
24699
+ return { allowed: false, status: 401, message: "Authentication required." };
23265
24700
  }
23266
- if (input.expires_at && Number.isNaN(new Date(input.expires_at).getTime())) {
23267
- return { ok: false, message: "expires_at is not a valid date." };
24701
+ if (userId == null && agent.rights_mode === "public") {
24702
+ return { allowed: true, via: "rbac-public" };
23268
24703
  }
23269
- if (input.expires_at && isExpired(input.expires_at, now)) {
23270
- return { ok: false, message: "expires_at must be in the future." };
24704
+ return { allowed: false, status: 401, message: "Authentication required." };
24705
+ };
24706
+
24707
+ // src/exulu/guest-rate-limit.ts
24708
+ init_cjs_shims();
24709
+ var MINUTE_MS = 6e4;
24710
+ var HOUR_MS2 = 36e5;
24711
+ var perMinuteLimit = () => parseInt(process.env.EXULU_GUEST_RATE_PER_MINUTE || "10", 10);
24712
+ var perHourLimit = () => parseInt(process.env.EXULU_GUEST_RATE_PER_HOUR || "60", 10);
24713
+ var maxMessageChars = () => parseInt(process.env.EXULU_GUEST_MAX_MESSAGE_CHARS || "8000", 10);
24714
+ var maxTotalChars = () => parseInt(process.env.EXULU_GUEST_MAX_TOTAL_CHARS || "32000", 10);
24715
+ var MAX_PART_COUNT = 100;
24716
+ var windows = /* @__PURE__ */ new Map();
24717
+ var guestRateLimitExceeded = (ip, now = Date.now()) => {
24718
+ const state = windows.get(ip) ?? {
24719
+ minuteStart: now,
24720
+ minuteCount: 0,
24721
+ hourStart: now,
24722
+ hourCount: 0,
24723
+ lastSeen: now
24724
+ };
24725
+ if (now - state.minuteStart >= MINUTE_MS) {
24726
+ state.minuteStart = now;
24727
+ state.minuteCount = 0;
24728
+ }
24729
+ if (now - state.hourStart >= HOUR_MS2) {
24730
+ state.hourStart = now;
24731
+ state.hourCount = 0;
24732
+ }
24733
+ state.minuteCount += 1;
24734
+ state.hourCount += 1;
24735
+ state.lastSeen = now;
24736
+ windows.set(ip, state);
24737
+ if (windows.size > 1e4) {
24738
+ for (const [key, value] of windows) {
24739
+ if (now - value.lastSeen >= HOUR_MS2) windows.delete(key);
24740
+ }
24741
+ if (windows.size > 1e4) {
24742
+ const sorted = [...windows.entries()].sort(
24743
+ (a, b) => a[1].lastSeen - b[1].lastSeen
24744
+ );
24745
+ for (const [key] of sorted) {
24746
+ if (windows.size <= 1e4) break;
24747
+ windows.delete(key);
24748
+ }
24749
+ }
23271
24750
  }
23272
- return { ok: true };
24751
+ return state.minuteCount > perMinuteLimit() || state.hourCount > perHourLimit();
23273
24752
  };
23274
- var hashSharePassword = (password) => import_bcryptjs4.default.hash(password, 10);
23275
- var verifySharePassword = (password, hash) => import_bcryptjs4.default.compare(password, hash);
23276
- var contentHeadersFor = (key, contentType, filename) => {
23277
- if (isHtmlKey(key)) return { contentType: "text/html; charset=utf-8" };
23278
- return {
23279
- contentType: contentType || "application/octet-stream",
23280
- disposition: `attachment; filename="${filename.replace(/"/g, "")}"`
24753
+ var partsTooLong = (parts) => Array.isArray(parts) && parts.some(
24754
+ (p) => typeof p?.text === "string" && p.text.length > maxMessageChars()
24755
+ );
24756
+ var collectTextParts = (b) => {
24757
+ const parts = [];
24758
+ const addFromParts = (ps) => {
24759
+ if (!Array.isArray(ps)) return;
24760
+ for (const p of ps) {
24761
+ if (typeof p?.text === "string") parts.push(p.text);
24762
+ }
23281
24763
  };
24764
+ if (b.message) addFromParts(b.message.parts);
24765
+ if (Array.isArray(b.messages)) {
24766
+ for (const m of b.messages) addFromParts(m?.parts);
24767
+ }
24768
+ return parts;
24769
+ };
24770
+ var guestMessageTooLong = (body) => {
24771
+ const b = body;
24772
+ if (!b) return false;
24773
+ if (b.message && partsTooLong(b.message.parts)) return true;
24774
+ if (Array.isArray(b.messages)) {
24775
+ if (b.messages.some((m) => partsTooLong(m?.parts))) return true;
24776
+ }
24777
+ const allParts = collectTextParts(b);
24778
+ if (allParts.length > MAX_PART_COUNT) return true;
24779
+ const totalChars = allParts.reduce((sum, t) => sum + t.length, 0);
24780
+ if (totalChars > maxTotalChars()) return true;
24781
+ return false;
24782
+ };
24783
+ var extractClientIp = (req) => {
24784
+ const forwarded = req.headers["x-forwarded-for"];
24785
+ if (process.env.EXULU_TRUST_PROXY === "true") {
24786
+ if (typeof forwarded === "string" && forwarded.length > 0) {
24787
+ const parts = forwarded.split(",");
24788
+ return parts[parts.length - 1].trim();
24789
+ }
24790
+ }
24791
+ return req.ip || req.socket?.remoteAddress || "unknown";
23282
24792
  };
23283
- var getSharedArtifactByName = (db2, name) => db2("shared_artifacts").where({ name }).first();
23284
24793
 
23285
24794
  // src/skills/skill-access.ts
23286
24795
  init_cjs_shims();
@@ -23645,6 +25154,9 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
23645
25154
  })
23646
25155
  );
23647
25156
  app.get(OAUTH_CALLBACK_PATH, handleOauthCallback);
25157
+ app.post("/credentials/submit", handleCredentialSubmit);
25158
+ app.get("/credentials", handleCredentialList);
25159
+ app.delete("/credentials/:provider", handleCredentialDelete);
23648
25160
  app.post("/test", async (req, res) => {
23649
25161
  const { item_name, context_id } = req.body;
23650
25162
  let itemFilters = [];
@@ -23988,17 +25500,33 @@ Mood: friendly and intelligent.
23988
25500
  }
23989
25501
  console.log("[EXULU] agent.rights_mode", agent.rights_mode);
23990
25502
  const authenticationResult = await requestValidators.authenticate(req);
23991
- if (!authenticationResult.user?.id && agent.rights_mode !== "public") {
23992
- res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
25503
+ const user = authenticationResult.user;
25504
+ if (!user?.id) {
25505
+ const ip = extractClientIp(req);
25506
+ if (guestRateLimitExceeded(ip)) {
25507
+ res.status(429).json({ detail: "Too many requests. Try again later." });
25508
+ return;
25509
+ }
25510
+ if (guestMessageTooLong(req.body)) {
25511
+ res.status(413).json({ detail: "Message too long." });
25512
+ return;
25513
+ }
25514
+ }
25515
+ const guestGate = await evaluateGuestChatAccess(
25516
+ agent,
25517
+ user?.id,
25518
+ req.headers["x-guest-password"]
25519
+ );
25520
+ if (!user?.id && !guestGate.allowed) {
25521
+ res.status(guestGate.status).json({ detail: guestGate.message });
23993
25522
  return;
23994
25523
  }
23995
- const user = authenticationResult.user;
23996
25524
  const scopeCheck = checkApiKeyScope(user, instance2);
23997
25525
  if (!scopeCheck.allowed) {
23998
25526
  res.status(scopeCheck.code).json({ detail: scopeCheck.reason });
23999
25527
  return;
24000
25528
  }
24001
- const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
25529
+ const hasAccessToAgent = guestGate.allowed || await checkRecordAccess(agent, "read", user);
24002
25530
  if (!hasAccessToAgent) {
24003
25531
  res.status(401).json({
24004
25532
  message: "You don't have access to this agent."
@@ -24148,7 +25676,7 @@ ${customInstructions}` : agent.instructions;
24148
25676
  else message2 = JSON.stringify(error);
24149
25677
  return mapStreamErrorMessage(message2);
24150
25678
  },
24151
- generateMessageId: (0, import_ai15.createIdGenerator)({
25679
+ generateMessageId: (0, import_ai12.createIdGenerator)({
24152
25680
  prefix: "msg_",
24153
25681
  size: 16
24154
25682
  }),
@@ -25356,6 +26884,32 @@ ${style.markdown}` : params.prompt;
25356
26884
  }
25357
26885
  res.status(200).json({ budget: await getUserBudgetView(authResult.user.id) });
25358
26886
  });
26887
+ app.get("/me/usage", async (req, res) => {
26888
+ const authResult = await requestValidators.authenticate(req);
26889
+ if (!authResult.user?.id) {
26890
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
26891
+ return;
26892
+ }
26893
+ const window = resolveUsageWindow(req.query.start_date, req.query.end_date);
26894
+ if (!window) {
26895
+ res.status(400).json({
26896
+ detail: "start_date and end_date must be YYYY-MM-DD or ISO datetimes, with start_date <= end_date."
26897
+ });
26898
+ return;
26899
+ }
26900
+ try {
26901
+ res.status(200).json({ usage: await getMyUsageView(authResult.user.id, window) });
26902
+ } catch (err) {
26903
+ if (err instanceof LiteLLMAdminError) {
26904
+ res.status(502).json({ detail: err.message });
26905
+ return;
26906
+ }
26907
+ console.error("[EXULU] /me/usage failed", err);
26908
+ res.status(500).json({
26909
+ detail: err instanceof Error ? err.message : "Usage query failed."
26910
+ });
26911
+ }
26912
+ });
25359
26913
  app.put(
25360
26914
  "/admin/budgets/:entityType/bulk",
25361
26915
  async (req, res) => {
@@ -25469,10 +27023,10 @@ ${style.markdown}` : params.prompt;
25469
27023
  }
25470
27024
  return { user: authResult.user };
25471
27025
  };
25472
- const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
27026
+ const DATE_ONLY_RE2 = /^\d{4}-\d{2}-\d{2}$/;
25473
27027
  const normaliseDateParam = (raw) => {
25474
27028
  if (typeof raw !== "string" || raw.length === 0) return null;
25475
- if (DATE_ONLY_RE.test(raw)) return raw;
27029
+ if (DATE_ONLY_RE2.test(raw)) return raw;
25476
27030
  const dt = new Date(raw);
25477
27031
  if (Number.isNaN(dt.getTime())) return null;
25478
27032
  return dt.toISOString().slice(0, 10);
@@ -27069,6 +28623,93 @@ ${style.markdown}` : params.prompt;
27069
28623
  if (headers.disposition) res.setHeader("Content-Disposition", headers.disposition);
27070
28624
  res.send(bytes);
27071
28625
  });
28626
+ const resolvePublicAgentSlug = async (agentModel) => {
28627
+ if (isLiteLLMEnabled()) return "/agents/litellm/run";
28628
+ if (!agentModel) return "";
28629
+ const { db: db2 } = await postgresClient();
28630
+ const modelRow = await db2.from("models").where({ id: agentModel }).first();
28631
+ const provider = modelRow?.provider ? providers.find((a) => a.id === modelRow.provider) : void 0;
28632
+ return provider?.slug || "";
28633
+ };
28634
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28635
+ const getGuestAgentById = async (id) => {
28636
+ if (!UUID_RE.test(id)) return void 0;
28637
+ const { db: db2 } = await postgresClient();
28638
+ return db2.from("agents").where({ id, guest_access: true, active: true }).first();
28639
+ };
28640
+ app.get("/public-agents", async (_req, res) => {
28641
+ const { db: db2 } = await postgresClient();
28642
+ const rows = await db2.from("agents").where({ guest_access: true, active: true }).select(
28643
+ "id",
28644
+ "name",
28645
+ "description",
28646
+ "image",
28647
+ "welcomemessage",
28648
+ "model",
28649
+ "guest_auth_mode",
28650
+ "guest_cover_image"
28651
+ );
28652
+ const views = await Promise.all(
28653
+ rows.map(
28654
+ async (row) => publicAgentView(row, await resolvePublicAgentSlug(row.model))
28655
+ )
28656
+ );
28657
+ res.json(views);
28658
+ });
28659
+ app.get("/public-agents/:id/meta", async (req, res) => {
28660
+ const row = await getGuestAgentById(req.params.id ?? "");
28661
+ if (!row) {
28662
+ res.status(404).json({ detail: "Not found." });
28663
+ return;
28664
+ }
28665
+ res.json(publicAgentView(row, await resolvePublicAgentSlug(row.model)));
28666
+ });
28667
+ app.get("/public-agents/:id/cover", async (req, res) => {
28668
+ const row = await getGuestAgentById(req.params.id ?? "");
28669
+ if (!row?.guest_cover_image) {
28670
+ res.status(404).json({ detail: "Not found." });
28671
+ return;
28672
+ }
28673
+ let bytes;
28674
+ try {
28675
+ bytes = await getS3ObjectBytes(row.guest_cover_image, config);
28676
+ } catch (e) {
28677
+ if (e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.$metadata?.httpStatusCode === 404) {
28678
+ res.status(404).json({ detail: "Cover not found." });
28679
+ return;
28680
+ }
28681
+ console.error("[EXULU] public-agent cover read failed", e);
28682
+ res.status(500).json({ detail: "Failed to read cover." });
28683
+ return;
28684
+ }
28685
+ const ext = row.guest_cover_image.split(".").pop()?.toLowerCase();
28686
+ const contentType = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
28687
+ res.setHeader("Content-Type", contentType);
28688
+ res.setHeader("X-Content-Type-Options", "nosniff");
28689
+ res.setHeader("Cache-Control", "public, max-age=300");
28690
+ res.send(bytes);
28691
+ });
28692
+ app.post(
28693
+ "/public-agents/:id/verify-password",
28694
+ async (req, res) => {
28695
+ const ip = extractClientIp(req);
28696
+ if (guestRateLimitExceeded(ip)) {
28697
+ res.status(429).json({ detail: "Too many requests. Try again later." });
28698
+ return;
28699
+ }
28700
+ const row = await getGuestAgentById(req.params.id ?? "");
28701
+ if (!row || row.guest_auth_mode !== "password") {
28702
+ res.status(404).json({ detail: "Not found." });
28703
+ return;
28704
+ }
28705
+ const password = typeof req.body?.password === "string" ? req.body.password : "";
28706
+ if (!row.guest_password_hash || !await verifySharePassword(password, row.guest_password_hash)) {
28707
+ res.status(401).json({ detail: "Incorrect password." });
28708
+ return;
28709
+ }
28710
+ res.status(204).end();
28711
+ }
28712
+ );
27072
28713
  app.use(import_express5.default.static("public"));
27073
28714
  await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config);
27074
28715
  return app;
@@ -27147,7 +28788,7 @@ init_check_record_access();
27147
28788
  init_resolve_model();
27148
28789
  init_supervisor();
27149
28790
  init_client();
27150
- var import_zod16 = require("zod");
28791
+ var import_zod19 = require("zod");
27151
28792
  init_convert_exulu_tools_to_ai_sdk_tools();
27152
28793
  init_singleton();
27153
28794
  var SESSION_ID_HEADER = "mcp-session-id";
@@ -27221,7 +28862,7 @@ var ExuluMCP = class {
27221
28862
  title: tool4.name + " agent",
27222
28863
  description: tool4.description,
27223
28864
  inputSchema: {
27224
- inputs: tool4.inputSchema || import_zod16.z.object({})
28865
+ inputs: tool4.inputSchema || import_zod19.z.object({})
27225
28866
  }
27226
28867
  },
27227
28868
  async ({ inputs }, args) => {
@@ -27273,7 +28914,7 @@ var ExuluMCP = class {
27273
28914
  title: "Get List of Prompt Templates",
27274
28915
  description: "Retrieves a list of prompt templates available for this agent. Returns the name, description, and ID of each template.",
27275
28916
  inputSchema: {
27276
- inputs: import_zod16.z.object({})
28917
+ inputs: import_zod19.z.object({})
27277
28918
  }
27278
28919
  },
27279
28920
  async ({ inputs }, args) => {
@@ -27319,8 +28960,8 @@ var ExuluMCP = class {
27319
28960
  title: "Get Prompt Template Details",
27320
28961
  description: "Retrieves the full details of a specific prompt template by ID, including the actual template content with variables.",
27321
28962
  inputSchema: {
27322
- inputs: import_zod16.z.object({
27323
- id: import_zod16.z.string().describe("The ID of the prompt template to retrieve")
28963
+ inputs: import_zod19.z.object({
28964
+ id: import_zod19.z.string().describe("The ID of the prompt template to retrieve")
27324
28965
  })
27325
28966
  }
27326
28967
  },
@@ -28395,7 +30036,7 @@ init_cjs_shims();
28395
30036
 
28396
30037
  // src/exulu/evals.ts
28397
30038
  init_cjs_shims();
28398
- var import_ai16 = require("ai");
30039
+ var import_ai13 = require("ai");
28399
30040
  init_entitlements();
28400
30041
  var ExuluEval = class {
28401
30042
  id;
@@ -28435,8 +30076,8 @@ var ExuluEval = class {
28435
30076
  // src/templates/evals/index.ts
28436
30077
  init_resolve_model();
28437
30078
  init_singleton();
28438
- var import_zod17 = require("zod");
28439
- var import_ai17 = require("ai");
30079
+ var import_zod20 = require("zod");
30080
+ var import_ai14 = require("ai");
28440
30081
  var llmAsJudgeEval = () => {
28441
30082
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
28442
30083
  return new ExuluEval({
@@ -28481,15 +30122,15 @@ var llmAsJudgeEval = () => {
28481
30122
  rbacBypass: true
28482
30123
  });
28483
30124
  console.log("[EXULU] prompt", prompt);
28484
- const { output } = await (0, import_ai17.generateText)({
30125
+ const { output } = await (0, import_ai14.generateText)({
28485
30126
  temperature: 0,
28486
30127
  model: resolved.languageModel,
28487
30128
  system: "",
28488
30129
  prompt,
28489
30130
  maxRetries: 2,
28490
- output: import_ai17.Output.object({
28491
- schema: import_zod17.z.object({
28492
- score: import_zod17.z.number().min(0).max(100).describe("The score between 0 and 100.")
30131
+ output: import_ai14.Output.object({
30132
+ schema: import_zod20.z.object({
30133
+ score: import_zod20.z.number().min(0).max(100).describe("The score between 0 and 100.")
28493
30134
  })
28494
30135
  })
28495
30136
  });
@@ -28720,15 +30361,15 @@ Usage:
28720
30361
  - If no todos exist yet, an empty list will be returned`;
28721
30362
 
28722
30363
  // src/templates/tools/todo/todo.ts
28723
- var import_zod18 = __toESM(require("zod"), 1);
30364
+ var import_zod21 = __toESM(require("zod"), 1);
28724
30365
  init_tool();
28725
30366
  init_check_record_access();
28726
30367
  init_client();
28727
- var TodoSchema = import_zod18.default.object({
28728
- content: import_zod18.default.string().describe("Brief description of the task"),
28729
- status: import_zod18.default.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
28730
- priority: import_zod18.default.string().describe("Priority level of the task: high, medium, low"),
28731
- id: import_zod18.default.string().describe("Unique identifier for the todo item")
30368
+ var TodoSchema = import_zod21.default.object({
30369
+ content: import_zod21.default.string().describe("Brief description of the task"),
30370
+ status: import_zod21.default.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
30371
+ priority: import_zod21.default.string().describe("Priority level of the task: high, medium, low"),
30372
+ id: import_zod21.default.string().describe("Unique identifier for the todo item")
28732
30373
  });
28733
30374
  var TodoWriteTool = new ExuluTool({
28734
30375
  id: "todo_write",
@@ -28744,8 +30385,8 @@ var TodoWriteTool = new ExuluTool({
28744
30385
  default: todowrite_default
28745
30386
  }
28746
30387
  ],
28747
- inputSchema: import_zod18.default.object({
28748
- todos: import_zod18.default.array(TodoSchema).describe("The updated todo list")
30388
+ inputSchema: import_zod21.default.object({
30389
+ todos: import_zod21.default.array(TodoSchema).describe("The updated todo list")
28749
30390
  }),
28750
30391
  execute: async (inputs) => {
28751
30392
  const { sessionID, todos, user } = inputs;
@@ -28780,7 +30421,7 @@ var TodoReadTool = new ExuluTool({
28780
30421
  id: "todo_read",
28781
30422
  name: "Todo Read",
28782
30423
  description: "Use this tool to read your todo list",
28783
- inputSchema: import_zod18.default.object({}),
30424
+ inputSchema: import_zod21.default.object({}),
28784
30425
  type: "function",
28785
30426
  category: "todo",
28786
30427
  config: [
@@ -28825,7 +30466,7 @@ init_cjs_shims();
28825
30466
  var questionread_default = 'Use this tool to read questions you\'ve asked and check if they\'ve been answered by the user. This tool helps you track the status of questions and retrieve the user\'s selected answers.\n\n## When to Use This Tool\n\nUse this tool proactively in these situations:\n- After asking a question to check if the user has responded\n- To retrieve the user\'s answer before proceeding with implementation\n- To review all questions and answers in the current session\n- When you need to reference a previous answer\n\n## How It Works\n\n- This tool takes no parameters (leave the input blank or empty)\n- Returns an array of all questions in the session\n- Each question includes:\n - `id`: Unique identifier for the question\n - `question`: The question text\n - `answerOptions`: Array of answer options with their IDs and text\n - `status`: Either "pending" (not answered) or "answered"\n - `selectedAnswerId`: The ID of the chosen answer (only present if answered)\n\n## Usage Pattern\n\nTypically you\'ll:\n1. Use Question Ask to pose a question\n2. Wait for the user to respond\n3. Use Question Read to check the answer\n4. Find the selected answer by matching the `selectedAnswerId` with an option in `answerOptions`\n5. Proceed with implementation based on the user\'s choice\n\n## Example Response\n\n```json\n[\n {\n "id": "question123",\n "question": "Which authentication method would you like to implement?",\n "answerOptions": [\n { "id": "ans1", "text": "JWT tokens" },\n { "id": "ans2", "text": "OAuth 2.0" },\n { "id": "ans3", "text": "Session-based auth" },\n { "id": "ans4", "text": "None of the above..." }\n ],\n "status": "answered",\n "selectedAnswerId": "ans1"\n }\n]\n```\n\nIn this example, the user selected "JWT tokens" (id: ans1).\n\n## Important Notes\n\n- If no questions exist in the session, an empty array will be returned\n- Questions remain in the session even after being answered for reference\n- Use the `selectedAnswerId` to find which answer option the user chose by matching it against the `id` field in `answerOptions`\n';
28826
30467
 
28827
30468
  // src/templates/tools/question/question.ts
28828
- var import_zod20 = __toESM(require("zod"), 1);
30469
+ var import_zod23 = __toESM(require("zod"), 1);
28829
30470
  init_tool();
28830
30471
  init_client();
28831
30472
 
@@ -28917,21 +30558,21 @@ After asking a question, use the Question Read tool to check if the user has ans
28917
30558
  `;
28918
30559
 
28919
30560
  // src/templates/tools/question/question-ask.ts
28920
- var import_zod19 = __toESM(require("zod"), 1);
30561
+ var import_zod22 = __toESM(require("zod"), 1);
28921
30562
  init_tool();
28922
30563
  init_check_record_access();
28923
30564
  init_client();
28924
30565
  var import_node_crypto16 = require("crypto");
28925
- var AnswerOptionSchema = import_zod19.default.object({
28926
- id: import_zod19.default.string().describe("Unique identifier for the answer option"),
28927
- text: import_zod19.default.string().describe("The text of the answer option")
30566
+ var AnswerOptionSchema = import_zod22.default.object({
30567
+ id: import_zod22.default.string().describe("Unique identifier for the answer option"),
30568
+ text: import_zod22.default.string().describe("The text of the answer option")
28928
30569
  });
28929
- var _QuestionSchema = import_zod19.default.object({
28930
- id: import_zod19.default.string().describe("Unique identifier for the question"),
28931
- question: import_zod19.default.string().describe("The question to ask the user"),
28932
- answerOptions: import_zod19.default.array(AnswerOptionSchema).describe("Array of possible answer options"),
28933
- selectedAnswerId: import_zod19.default.string().optional().describe("The ID of the answer option selected by the user"),
28934
- status: import_zod19.default.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
30570
+ var _QuestionSchema = import_zod22.default.object({
30571
+ id: import_zod22.default.string().describe("Unique identifier for the question"),
30572
+ question: import_zod22.default.string().describe("The question to ask the user"),
30573
+ answerOptions: import_zod22.default.array(AnswerOptionSchema).describe("Array of possible answer options"),
30574
+ selectedAnswerId: import_zod22.default.string().optional().describe("The ID of the answer option selected by the user"),
30575
+ status: import_zod22.default.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
28935
30576
  });
28936
30577
  var QuestionAskTool = new ExuluTool({
28937
30578
  id: "question_ask",
@@ -28948,9 +30589,9 @@ var QuestionAskTool = new ExuluTool({
28948
30589
  default: questionask_default
28949
30590
  }
28950
30591
  ],
28951
- inputSchema: import_zod19.default.object({
28952
- question: import_zod19.default.string().describe("The question to ask the user"),
28953
- answerOptions: import_zod19.default.array(import_zod19.default.string()).describe("Array of possible answer options (strings)")
30592
+ inputSchema: import_zod22.default.object({
30593
+ question: import_zod22.default.string().describe("The question to ask the user"),
30594
+ answerOptions: import_zod22.default.array(import_zod22.default.string()).describe("Array of possible answer options (strings)")
28954
30595
  }),
28955
30596
  execute: async (inputs) => {
28956
30597
  const { sessionID, question, answerOptions, user } = inputs;
@@ -29023,7 +30664,7 @@ var QuestionReadTool = new ExuluTool({
29023
30664
  name: "Question Read",
29024
30665
  needsApproval: false,
29025
30666
  description: "Use this tool to read questions and their answers",
29026
- inputSchema: import_zod20.default.object({}),
30667
+ inputSchema: import_zod23.default.object({}),
29027
30668
  type: "function",
29028
30669
  category: "question",
29029
30670
  config: [
@@ -29055,15 +30696,15 @@ var questionTools = [QuestionAskTool, QuestionReadTool];
29055
30696
  // src/templates/tools/perplexity.ts
29056
30697
  init_cjs_shims();
29057
30698
  init_tool();
29058
- var import_zod21 = __toESM(require("zod"), 1);
30699
+ var import_zod24 = __toESM(require("zod"), 1);
29059
30700
  var import_perplexity_ai = __toESM(require("@perplexity-ai/perplexity_ai"), 1);
29060
30701
  var internetSearchTool = new ExuluTool({
29061
30702
  id: "internet_search",
29062
30703
  name: "Internet Search",
29063
30704
  description: "Search the internet for information.",
29064
- inputSchema: import_zod21.default.object({
29065
- query: import_zod21.default.string().describe("The query to the tool."),
29066
- search_recency_filter: import_zod21.default.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
30705
+ inputSchema: import_zod24.default.object({
30706
+ query: import_zod24.default.string().describe("The query to the tool."),
30707
+ search_recency_filter: import_zod24.default.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
29067
30708
  }),
29068
30709
  category: "internet_search",
29069
30710
  type: "web_search",
@@ -29158,7 +30799,7 @@ var perplexityTools = [internetSearchTool];
29158
30799
  init_cjs_shims();
29159
30800
  init_tool();
29160
30801
  var nodemailer = __toESM(require("nodemailer"), 1);
29161
- var import_zod22 = require("zod");
30802
+ var import_zod25 = require("zod");
29162
30803
  var transporter = null;
29163
30804
  function getTransporter(config) {
29164
30805
  if (!transporter) {
@@ -29182,11 +30823,11 @@ var emailTool = new ExuluTool({
29182
30823
  id: "email",
29183
30824
  name: "Email",
29184
30825
  description: "Send an email.",
29185
- inputSchema: import_zod22.z.object({
29186
- recipient: import_zod22.z.string().describe("The recipient of the email."),
29187
- subject: import_zod22.z.string().describe("The subject of the email."),
29188
- html: import_zod22.z.string().describe("The HTML body of the email."),
29189
- text: import_zod22.z.string().describe("The text body of the email.")
30826
+ inputSchema: import_zod25.z.object({
30827
+ recipient: import_zod25.z.string().describe("The recipient of the email."),
30828
+ subject: import_zod25.z.string().describe("The subject of the email."),
30829
+ html: import_zod25.z.string().describe("The HTML body of the email."),
30830
+ text: import_zod25.z.string().describe("The text body of the email.")
29190
30831
  }),
29191
30832
  type: "function",
29192
30833
  config: [{
@@ -29257,7 +30898,7 @@ init_tool();
29257
30898
  init_supervisor();
29258
30899
  init_client();
29259
30900
  init_check_record_access();
29260
- var import_zod23 = require("zod");
30901
+ var import_zod26 = require("zod");
29261
30902
  var _cachedImageModels;
29262
30903
  var setCachedImageModels = (models2) => {
29263
30904
  _cachedImageModels = models2;
@@ -29318,8 +30959,8 @@ var createImageGenerationWidgetTool = (models2) => {
29318
30959
  needsApproval: false,
29319
30960
  type: "function",
29320
30961
  config: [],
29321
- inputSchema: import_zod23.z.object({
29322
- prompt: import_zod23.z.string().describe(
30962
+ inputSchema: import_zod26.z.object({
30963
+ prompt: import_zod26.z.string().describe(
29323
30964
  "Initial image prompt. The user can edit it before generating."
29324
30965
  )
29325
30966
  }),
@@ -29646,6 +31287,43 @@ var startTranscriptionPollingLoop = () => {
29646
31287
  process.on("SIGTERM", stop);
29647
31288
  };
29648
31289
 
31290
+ // src/exulu/recall/reconcile-loop.ts
31291
+ init_cjs_shims();
31292
+ var RECONCILE_INTERVAL_MS = 6e4;
31293
+ var timer2 = null;
31294
+ var stopped2 = false;
31295
+ var tick2 = async () => {
31296
+ if (stopped2) return;
31297
+ try {
31298
+ const acted = await recallService.reconcileOnce();
31299
+ if (acted > 0) {
31300
+ console.log(`[EXULU-RECALL] reconcile tick recovered ${acted} job(s)`);
31301
+ }
31302
+ } catch (err) {
31303
+ console.error(
31304
+ `[EXULU-RECALL] reconcile tick failed: ${err.message}`
31305
+ );
31306
+ } finally {
31307
+ if (!stopped2) {
31308
+ timer2 = setTimeout(tick2, RECONCILE_INTERVAL_MS);
31309
+ }
31310
+ }
31311
+ };
31312
+ var startRecallReconcileLoop = () => {
31313
+ if (timer2) return;
31314
+ stopped2 = false;
31315
+ timer2 = setTimeout(tick2, RECONCILE_INTERVAL_MS);
31316
+ const stop = () => {
31317
+ stopped2 = true;
31318
+ if (timer2) {
31319
+ clearTimeout(timer2);
31320
+ timer2 = null;
31321
+ }
31322
+ };
31323
+ process.on("SIGINT", stop);
31324
+ process.on("SIGTERM", stop);
31325
+ };
31326
+
29649
31327
  // src/exulu/app/index.ts
29650
31328
  var isDev = process.env.NODE_ENV !== "production";
29651
31329
  var lineLimitFormat = import_winston2.default.format((info) => {
@@ -29880,6 +31558,9 @@ var ExuluApp = class {
29880
31558
  );
29881
31559
  }
29882
31560
  logRecallStartup();
31561
+ if (recallEnabled()) {
31562
+ startRecallReconcileLoop();
31563
+ }
29883
31564
  return this._expressApp;
29884
31565
  }
29885
31566
  };
@@ -30834,6 +32515,7 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
30834
32515
 
30835
32516
  // src/exulu/read-api.ts
30836
32517
  init_cjs_shims();
32518
+ init_table_names();
30837
32519
  init_client();
30838
32520
  var authorizedRead = async (context, user, role, opts = {}) => {
30839
32521
  if (!opts.itemIds?.length && !opts.externalIds?.length) {
@@ -30937,7 +32619,6 @@ var {
30937
32619
  promptFavoritesSchema: promptFavoritesSchema3,
30938
32620
  transcriptionJobsSchema: transcriptionJobsSchema3,
30939
32621
  imageGenerationsSchema: imageGenerationsSchema2,
30940
- oauthTokensSchema: oauthTokensSchema2,
30941
32622
  sharedArtifactsSchema: sharedArtifactsSchema2
30942
32623
  } = coreSchemas.get();
30943
32624
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
@@ -30960,6 +32641,18 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
30960
32641
  console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
30961
32642
  }
30962
32643
  };
32644
+ var migrateUserCredentialsDataColumn = async (knex) => {
32645
+ const dataType = await knex.raw(
32646
+ `SELECT data_type FROM information_schema.columns
32647
+ WHERE table_name = 'user_credentials' AND column_name = 'data'`
32648
+ );
32649
+ if (dataType.rows?.[0]?.data_type === "jsonb") {
32650
+ console.log("[EXULU] Migrating user_credentials.data jsonb -> text.");
32651
+ await knex.raw(
32652
+ "ALTER TABLE user_credentials ALTER COLUMN data TYPE text USING data::text"
32653
+ );
32654
+ }
32655
+ };
30963
32656
  var up = async function(knex) {
30964
32657
  console.log("[EXULU] Database up.");
30965
32658
  const schemas = [
@@ -30981,7 +32674,6 @@ var up = async function(knex) {
30981
32674
  promptFavoritesSchema3(),
30982
32675
  transcriptionJobsSchema3(),
30983
32676
  imageGenerationsSchema2(),
30984
- oauthTokensSchema2(),
30985
32677
  sharedArtifactsSchema2(),
30986
32678
  rbacSchema3(),
30987
32679
  agentsSchema3(),
@@ -31015,10 +32707,9 @@ var up = async function(knex) {
31015
32707
  console.log(`[EXULU] Creating ${schema.name.plural} table.`, schema.fields);
31016
32708
  await createTable(schema);
31017
32709
  }
31018
- await knex.raw("DROP INDEX IF EXISTS oauth_tokens_tool_id_user_id_unique");
31019
- await knex.raw(
31020
- "CREATE UNIQUE INDEX IF NOT EXISTS oauth_tokens_provider_user_id_unique ON oauth_tokens (provider, user_id)"
31021
- );
32710
+ await knex.raw("DROP TABLE IF EXISTS oauth_tokens CASCADE;");
32711
+ await knex.raw(userCredentialsSchema());
32712
+ await migrateUserCredentialsDataColumn(knex);
31022
32713
  if (await knex.schema.hasColumn("job_results", "workflow") && await knex.schema.hasColumn("job_results", "trigger_metadata")) {
31023
32714
  await knex.raw(
31024
32715
  "CREATE INDEX IF NOT EXISTS job_results_email_dedup_idx ON job_results (workflow, (trigger_metadata->>'message_id'))"
@@ -31212,6 +32903,11 @@ var execute = async ({ contexts }) => {
31212
32903
  evals: "read"
31213
32904
  }).returning("id");
31214
32905
  }
32906
+ const existingExternalRole = await db2.from("roles").where({ name: "external" }).first();
32907
+ if (!existingExternalRole) {
32908
+ console.log("[EXULU] Creating external role.");
32909
+ await db2.from("roles").insert({ name: "external" }).returning("id");
32910
+ }
31215
32911
  const existingUser = await db2.from("users").where({ email: "admin@exulu.com" }).first();
31216
32912
  if (!existingUser) {
31217
32913
  const password = await encryptString("admin");
@@ -32055,8 +33751,8 @@ var MarkdownChunker = class {
32055
33751
  init_cjs_shims();
32056
33752
  var fs4 = __toESM(require("fs"), 1);
32057
33753
  var path = __toESM(require("path"), 1);
32058
- var import_ai18 = require("ai");
32059
- var import_zod24 = require("zod");
33754
+ var import_ai15 = require("ai");
33755
+ var import_zod27 = require("zod");
32060
33756
  var import_p_limit = __toESM(require("p-limit"), 1);
32061
33757
  var import_crypto2 = require("crypto");
32062
33758
  init_with_retry();
@@ -32520,18 +34216,18 @@ If the page contains a flow-chart, schematic, technical drawing or control board
32520
34216
 
32521
34217
  ### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
32522
34218
  `;
32523
- const result = await (0, import_ai18.generateText)({
34219
+ const result = await (0, import_ai15.generateText)({
32524
34220
  model,
32525
- output: import_ai18.Output.object({
32526
- schema: import_zod24.z.object({
32527
- needs_correction: import_zod24.z.boolean(),
32528
- corrected_text: import_zod24.z.string().nullable(),
32529
- current_page_table: import_zod24.z.object({
32530
- headers: import_zod24.z.array(import_zod24.z.string()),
32531
- is_continuation: import_zod24.z.boolean()
34221
+ output: import_ai15.Output.object({
34222
+ schema: import_zod27.z.object({
34223
+ needs_correction: import_zod27.z.boolean(),
34224
+ corrected_text: import_zod27.z.string().nullable(),
34225
+ current_page_table: import_zod27.z.object({
34226
+ headers: import_zod27.z.array(import_zod27.z.string()),
34227
+ is_continuation: import_zod27.z.boolean()
32532
34228
  }).nullable(),
32533
- confidence: import_zod24.z.enum(["high", "medium", "low"]),
32534
- reasoning: import_zod24.z.string()
34229
+ confidence: import_zod27.z.enum(["high", "medium", "low"]),
34230
+ reasoning: import_zod27.z.string()
32535
34231
  })
32536
34232
  }),
32537
34233
  messages: [
@@ -33042,6 +34738,7 @@ async function rerank(input) {
33042
34738
  // src/index.ts
33043
34739
  init_pipeline();
33044
34740
  init_statistics();
34741
+ init_errors();
33045
34742
  var ExuluJobs = {
33046
34743
  redis: redisClient
33047
34744
  };
@@ -33156,6 +34853,7 @@ var ExuluPython = {
33156
34853
  };
33157
34854
  // Annotate the CommonJS export names for ESM import in node:
33158
34855
  0 && (module.exports = {
34856
+ CredentialInvalidError,
33159
34857
  EXULU_JOB_STATUS_ENUM,
33160
34858
  EXULU_STATISTICS_TYPE_ENUM,
33161
34859
  ExuluApp,