@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/{chunk-KFL7HIID.js → chunk-ZDH5S2WF.js} +873 -277
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-YY2WIMMJ.js → convert-exulu-tools-to-ai-sdk-tools-FN6WZSIQ.js} +1 -1
- package/dist/index.cjs +2306 -608
- package/dist/index.d.cts +41 -8
- package/dist/index.d.ts +41 -8
- package/dist/index.js +1203 -202
- package/ee/agentic-retrieval/pipeline/hyde.test.ts +1 -1
- package/ee/agentic-retrieval/pipeline/hyde.ts +6 -3
- package/ee/agentic-retrieval/pipeline/memory.test.ts +5 -1
- package/ee/agentic-retrieval/pipeline/memory.ts +71 -104
- package/ee/agentic-retrieval/pipeline/micro-call.test.ts +112 -0
- package/ee/agentic-retrieval/pipeline/micro-call.ts +98 -0
- package/ee/agentic-retrieval/pipeline/prefilter.test.ts +1 -0
- package/ee/agentic-retrieval/pipeline/prefilter.ts +11 -20
- package/ee/agentic-retrieval/pipeline/routing.test.ts +44 -1
- package/ee/agentic-retrieval/pipeline/routing.ts +31 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,14 +9,19 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
COMPACTION_INSUFFICIENT,
|
|
11
11
|
ContextCompactionRequiredError,
|
|
12
|
+
CredentialInvalidError,
|
|
12
13
|
DEFAULT_CONTEXT_WINDOW,
|
|
13
14
|
ExuluTool,
|
|
15
|
+
KB_EDITOR_TOOL_ID,
|
|
14
16
|
LITELLM_UI_PATH,
|
|
15
17
|
LiteLLMAdminError,
|
|
16
18
|
OAUTH_CALLBACK_PATH,
|
|
17
19
|
PreviewRenderError,
|
|
18
20
|
ResolveModelError,
|
|
21
|
+
SCRUBBED_CREDENTIAL_TEXT,
|
|
22
|
+
SCRUBBED_OAUTH_TEXT,
|
|
19
23
|
STATISTICS_TYPE_ENUM,
|
|
24
|
+
authRegistry,
|
|
20
25
|
authentication,
|
|
21
26
|
budgetTagFor,
|
|
22
27
|
buildTags,
|
|
@@ -26,8 +31,10 @@ import {
|
|
|
26
31
|
convertExuluToolsToAiSdkTools,
|
|
27
32
|
copyS3Object,
|
|
28
33
|
createAgenticRetrievalTool,
|
|
34
|
+
createKbEditorPickerTool,
|
|
29
35
|
createTaggedFetch,
|
|
30
36
|
createUppyRoutes,
|
|
37
|
+
credentialStore,
|
|
31
38
|
decrypt,
|
|
32
39
|
decryptOauthState,
|
|
33
40
|
deleteS3Object,
|
|
@@ -40,12 +47,14 @@ import {
|
|
|
40
47
|
exchangeCodeForTokens,
|
|
41
48
|
exuluApp,
|
|
42
49
|
getBudgetSettings,
|
|
50
|
+
getChunksTableName,
|
|
43
51
|
getPdfPreviewBytes,
|
|
44
52
|
getPresignedUrl,
|
|
45
53
|
getS3ObjectBytes,
|
|
46
54
|
getS3ObjectContent,
|
|
47
55
|
getS3ObjectEtag,
|
|
48
56
|
getS3SignedUploadUrl,
|
|
57
|
+
getTableName,
|
|
49
58
|
getTagBudgetMap,
|
|
50
59
|
getTagDailyActivity,
|
|
51
60
|
getToken,
|
|
@@ -57,8 +66,6 @@ import {
|
|
|
57
66
|
listS3ObjectsByPrefix,
|
|
58
67
|
listTagsByPrefix,
|
|
59
68
|
mapStreamErrorMessage,
|
|
60
|
-
oauthRegistry,
|
|
61
|
-
oauthTokenStore,
|
|
62
69
|
parseResetAt,
|
|
63
70
|
postgresClient,
|
|
64
71
|
provisionDefaultUserBudget,
|
|
@@ -77,9 +84,10 @@ import {
|
|
|
77
84
|
updateStatistic,
|
|
78
85
|
uploadFile,
|
|
79
86
|
upsertBudget,
|
|
87
|
+
verifyCredentialNonce,
|
|
80
88
|
waitForLiteLLMReady,
|
|
81
89
|
withRetry
|
|
82
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-ZDH5S2WF.js";
|
|
83
91
|
import {
|
|
84
92
|
findLiteLLMModel
|
|
85
93
|
} from "./chunk-7CCMW3IW.js";
|
|
@@ -140,9 +148,9 @@ async function guardRedisStartup(label, run, source) {
|
|
|
140
148
|
);
|
|
141
149
|
}, WATCHDOG_INTERVAL_MS);
|
|
142
150
|
watchdog.unref?.();
|
|
143
|
-
let
|
|
151
|
+
let timer3;
|
|
144
152
|
const timeout = new Promise((_resolve, reject) => {
|
|
145
|
-
|
|
153
|
+
timer3 = setTimeout(() => {
|
|
146
154
|
reject(
|
|
147
155
|
new Error(
|
|
148
156
|
`[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}.`
|
|
@@ -159,7 +167,7 @@ async function guardRedisStartup(label, run, source) {
|
|
|
159
167
|
return result;
|
|
160
168
|
} finally {
|
|
161
169
|
clearInterval(watchdog);
|
|
162
|
-
if (
|
|
170
|
+
if (timer3) clearTimeout(timer3);
|
|
163
171
|
source?.off?.("error", onError);
|
|
164
172
|
}
|
|
165
173
|
}
|
|
@@ -209,7 +217,13 @@ var requestValidators = {
|
|
|
209
217
|
const { db } = await postgresClient();
|
|
210
218
|
let authtoken = null;
|
|
211
219
|
if (!apikey) {
|
|
212
|
-
|
|
220
|
+
try {
|
|
221
|
+
authtoken = await getToken(
|
|
222
|
+
(req.headers["authorization"] || req.headers["x-api-key"]) ?? ""
|
|
223
|
+
);
|
|
224
|
+
} catch {
|
|
225
|
+
authtoken = null;
|
|
226
|
+
}
|
|
213
227
|
}
|
|
214
228
|
return await authentication({
|
|
215
229
|
authtoken,
|
|
@@ -523,10 +537,6 @@ var ExuluStorage = class {
|
|
|
523
537
|
// todo add upload and delete methods
|
|
524
538
|
};
|
|
525
539
|
|
|
526
|
-
// src/exulu/table-names.ts
|
|
527
|
-
var getTableName = (id) => sanitizeName(id) + "_items";
|
|
528
|
-
var getChunksTableName = (id) => sanitizeName(id) + "_chunks";
|
|
529
|
-
|
|
530
540
|
// src/exulu/context.ts
|
|
531
541
|
import pgvector from "pgvector/knex";
|
|
532
542
|
|
|
@@ -1388,10 +1398,31 @@ function preprocessQuery(query, options = {}) {
|
|
|
1388
1398
|
};
|
|
1389
1399
|
}
|
|
1390
1400
|
|
|
1401
|
+
// src/graphql/resolvers/field-allow-list.ts
|
|
1402
|
+
var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
|
|
1403
|
+
function groupableFields(table) {
|
|
1404
|
+
const allowed = new Set(ALWAYS_ALLOWED);
|
|
1405
|
+
for (const field of table.fields) {
|
|
1406
|
+
if (field.hidden !== true) {
|
|
1407
|
+
allowed.add(field.name);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return allowed;
|
|
1411
|
+
}
|
|
1412
|
+
function assertAllowedField(table, fieldName, label) {
|
|
1413
|
+
const allowed = groupableFields(table);
|
|
1414
|
+
if (!allowed.has(fieldName)) {
|
|
1415
|
+
throw new Error(`Cannot ${label} by "${fieldName}".`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1391
1419
|
// src/graphql/resolvers/apply-sorting.ts
|
|
1392
|
-
var applySorting = (query, sort, field_prefix) => {
|
|
1420
|
+
var applySorting = (query, sort, field_prefix, table) => {
|
|
1393
1421
|
const prefix = field_prefix ? field_prefix + "." : "";
|
|
1394
1422
|
if (sort) {
|
|
1423
|
+
if (table) {
|
|
1424
|
+
assertAllowedField(table, sort.field, "sort");
|
|
1425
|
+
}
|
|
1395
1426
|
sort.field = prefix + sort.field;
|
|
1396
1427
|
query = query.orderBy(sort.field, sort.direction.toLowerCase());
|
|
1397
1428
|
}
|
|
@@ -2323,6 +2354,31 @@ var agentsSchema = {
|
|
|
2323
2354
|
// (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
|
|
2324
2355
|
name: "max_tool_steps",
|
|
2325
2356
|
type: "number"
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
name: "guest_access",
|
|
2360
|
+
type: "boolean",
|
|
2361
|
+
default: false
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
name: "guest_auth_mode",
|
|
2365
|
+
type: "text",
|
|
2366
|
+
default: "regular"
|
|
2367
|
+
// 'public' | 'password' | 'regular' (= login)
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
// bcrypt hash (hashSharePassword); NEVER exposed via GraphQL/REST —
|
|
2371
|
+
// see sanitizeRequestedFields + createExuluContextsTypeDefs filtering.
|
|
2372
|
+
name: "guest_password_hash",
|
|
2373
|
+
type: "text",
|
|
2374
|
+
required: false,
|
|
2375
|
+
hidden: true
|
|
2376
|
+
},
|
|
2377
|
+
{
|
|
2378
|
+
// S3 key of the custom login-page image shown on the public auth page.
|
|
2379
|
+
name: "guest_cover_image",
|
|
2380
|
+
type: "text",
|
|
2381
|
+
required: false
|
|
2326
2382
|
}
|
|
2327
2383
|
]
|
|
2328
2384
|
};
|
|
@@ -2439,7 +2495,8 @@ var usersSchema = {
|
|
|
2439
2495
|
},
|
|
2440
2496
|
{
|
|
2441
2497
|
name: "temporary_token",
|
|
2442
|
-
type: "text"
|
|
2498
|
+
type: "text",
|
|
2499
|
+
hidden: true
|
|
2443
2500
|
},
|
|
2444
2501
|
{
|
|
2445
2502
|
name: "type",
|
|
@@ -2465,7 +2522,8 @@ var usersSchema = {
|
|
|
2465
2522
|
},
|
|
2466
2523
|
{
|
|
2467
2524
|
name: "apikey",
|
|
2468
|
-
type: "text"
|
|
2525
|
+
type: "text",
|
|
2526
|
+
hidden: true
|
|
2469
2527
|
},
|
|
2470
2528
|
{
|
|
2471
2529
|
name: "scope_mode",
|
|
@@ -2482,11 +2540,13 @@ var usersSchema = {
|
|
|
2482
2540
|
},
|
|
2483
2541
|
{
|
|
2484
2542
|
name: "password",
|
|
2485
|
-
type: "text"
|
|
2543
|
+
type: "text",
|
|
2544
|
+
hidden: true
|
|
2486
2545
|
},
|
|
2487
2546
|
{
|
|
2488
2547
|
name: "anthropic_token",
|
|
2489
|
-
type: "text"
|
|
2548
|
+
type: "text",
|
|
2549
|
+
hidden: true
|
|
2490
2550
|
},
|
|
2491
2551
|
{
|
|
2492
2552
|
name: "personal_system_prompt",
|
|
@@ -2706,29 +2766,20 @@ var imageGenerationsSchema = {
|
|
|
2706
2766
|
{ name: "error", type: "text", required: false }
|
|
2707
2767
|
]
|
|
2708
2768
|
};
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
// AES-encrypted
|
|
2724
|
-
{ name: "refresh_token", type: "longText", required: false },
|
|
2725
|
-
// AES-encrypted
|
|
2726
|
-
{ name: "token_type", type: "text", required: false },
|
|
2727
|
-
{ name: "scopes", type: "text", required: false },
|
|
2728
|
-
{ name: "expires_at", type: "date", required: false }
|
|
2729
|
-
// null = non-expiring
|
|
2730
|
-
]
|
|
2731
|
-
};
|
|
2769
|
+
function userCredentialsSchema() {
|
|
2770
|
+
return `
|
|
2771
|
+
CREATE TABLE IF NOT EXISTS user_credentials (
|
|
2772
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
2773
|
+
provider text NOT NULL,
|
|
2774
|
+
user_id text NOT NULL,
|
|
2775
|
+
auth_type text NOT NULL CHECK (auth_type IN ('oauth', 'user_credentials')),
|
|
2776
|
+
data text NOT NULL,
|
|
2777
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2778
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
2779
|
+
UNIQUE (provider, user_id)
|
|
2780
|
+
);
|
|
2781
|
+
`;
|
|
2782
|
+
}
|
|
2732
2783
|
var sharedArtifactsSchema = {
|
|
2733
2784
|
type: "shared_artifacts",
|
|
2734
2785
|
name: {
|
|
@@ -2742,7 +2793,7 @@ var sharedArtifactsSchema = {
|
|
|
2742
2793
|
{ name: "name", type: "text", index: true, unique: true, required: true },
|
|
2743
2794
|
{ name: "s3key", type: "text", required: true },
|
|
2744
2795
|
{ name: "auth_mode", type: "text", default: "regular" },
|
|
2745
|
-
{ name: "password_hash", type: "text", required: false },
|
|
2796
|
+
{ name: "password_hash", type: "text", required: false, hidden: true },
|
|
2746
2797
|
// bcrypt; password mode only
|
|
2747
2798
|
{ name: "expires_at", type: "date", required: false },
|
|
2748
2799
|
// null = no expiry
|
|
@@ -2840,7 +2891,6 @@ var coreSchemas = {
|
|
|
2840
2891
|
entityTypeSettingsSchema: () => addCoreFields(entityTypeSettingsSchema),
|
|
2841
2892
|
promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
|
|
2842
2893
|
contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
|
|
2843
|
-
oauthTokensSchema: () => addCoreFields(oauthTokensSchema),
|
|
2844
2894
|
sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
|
|
2845
2895
|
transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
|
|
2846
2896
|
imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
|
|
@@ -3689,7 +3739,7 @@ var vectorSearch = async ({
|
|
|
3689
3739
|
chunksQuery = applyFilters(chunksQuery, itemFilters, table, "items");
|
|
3690
3740
|
chunksQuery = applyFilters(chunksQuery, chunkFilters, table, "chunks");
|
|
3691
3741
|
chunksQuery = applyAccessControl(table, chunksQuery, user, "items");
|
|
3692
|
-
chunksQuery = applySorting(chunksQuery, sort, "items");
|
|
3742
|
+
chunksQuery = applySorting(chunksQuery, sort, "items", table);
|
|
3693
3743
|
if (filterEntityIds) {
|
|
3694
3744
|
applyEntityFilter(chunksQuery, "chunks", context, filterEntityIds, entityFilter?.mode || "any");
|
|
3695
3745
|
}
|
|
@@ -5274,6 +5324,8 @@ var GraphQLDate = new GraphQLScalarType({
|
|
|
5274
5324
|
});
|
|
5275
5325
|
|
|
5276
5326
|
// src/graphql/resolvers/utils.ts
|
|
5327
|
+
var NON_COLUMN_SELECTIONS = /* @__PURE__ */ new Set(["pageInfo", "items", "RBAC"]);
|
|
5328
|
+
var isSelectableColumn = (field) => !NON_COLUMN_SELECTIONS.has(field) && !field.startsWith("__");
|
|
5277
5329
|
var getRequestedFields = (info) => {
|
|
5278
5330
|
const selections = info.operation.selectionSet.selections[0].selectionSet.selections;
|
|
5279
5331
|
const itemSelection = selections.find((s) => s.name.value === "item");
|
|
@@ -5286,7 +5338,7 @@ var getRequestedFields = (info) => {
|
|
|
5286
5338
|
return acc;
|
|
5287
5339
|
}, {})
|
|
5288
5340
|
);
|
|
5289
|
-
return fields.filter(
|
|
5341
|
+
return fields.filter(isSelectableColumn);
|
|
5290
5342
|
}
|
|
5291
5343
|
if (itemsSelection) {
|
|
5292
5344
|
fields = Object.keys(
|
|
@@ -5295,7 +5347,7 @@ var getRequestedFields = (info) => {
|
|
|
5295
5347
|
return acc;
|
|
5296
5348
|
}, {})
|
|
5297
5349
|
);
|
|
5298
|
-
return fields.filter(
|
|
5350
|
+
return fields.filter(isSelectableColumn);
|
|
5299
5351
|
}
|
|
5300
5352
|
fields = Object.keys(
|
|
5301
5353
|
selections.reduce((acc, field) => {
|
|
@@ -5303,7 +5355,7 @@ var getRequestedFields = (info) => {
|
|
|
5303
5355
|
return acc;
|
|
5304
5356
|
}, {})
|
|
5305
5357
|
);
|
|
5306
|
-
return fields.filter(
|
|
5358
|
+
return fields.filter(isSelectableColumn);
|
|
5307
5359
|
};
|
|
5308
5360
|
var contextItemsProcessorHandler = async (context, config, items, user, role) => {
|
|
5309
5361
|
let jobs = [];
|
|
@@ -5370,7 +5422,7 @@ var exuluProviderFields = [
|
|
|
5370
5422
|
"workflows"
|
|
5371
5423
|
];
|
|
5372
5424
|
|
|
5373
|
-
// src/graphql/utilities/
|
|
5425
|
+
// src/graphql/utilities/budget-field.ts
|
|
5374
5426
|
var BUDGET_ENTITY_SINGULARS = /* @__PURE__ */ new Set([
|
|
5375
5427
|
"user",
|
|
5376
5428
|
"role",
|
|
@@ -5393,22 +5445,37 @@ var BUDGET_ENTITY_TYPE_BY_SINGULAR = {
|
|
|
5393
5445
|
};
|
|
5394
5446
|
var addBudgetField = async (requestedFields, result, tableSingular, user) => {
|
|
5395
5447
|
if (!requestedFields.includes("budget")) return result;
|
|
5448
|
+
const entityType = BUDGET_ENTITY_TYPE_BY_SINGULAR[tableSingular];
|
|
5396
5449
|
const scope = user?.role?.budget_management;
|
|
5397
|
-
const
|
|
5398
|
-
|
|
5450
|
+
const canReadAll = !!user?.super_admin || scope === "read" || scope === "write";
|
|
5451
|
+
const memberView = !canReadAll && entityType === "project";
|
|
5452
|
+
if (!canReadAll && !memberView || result?.id == null || !entityType) {
|
|
5399
5453
|
result.budget = null;
|
|
5400
5454
|
return result;
|
|
5401
5455
|
}
|
|
5402
|
-
const
|
|
5403
|
-
|
|
5456
|
+
const map = await getTagBudgetMap();
|
|
5457
|
+
const tag = budgetTagFor(entityType, result.id);
|
|
5458
|
+
const info = tag ? map[tag] ?? null : null;
|
|
5459
|
+
if (!memberView) {
|
|
5460
|
+
result.budget = info;
|
|
5461
|
+
return result;
|
|
5462
|
+
}
|
|
5463
|
+
if (!info) {
|
|
5404
5464
|
result.budget = null;
|
|
5405
5465
|
return result;
|
|
5406
5466
|
}
|
|
5407
|
-
const
|
|
5408
|
-
|
|
5409
|
-
|
|
5467
|
+
const settings = await getBudgetSettings();
|
|
5468
|
+
result.budget = {
|
|
5469
|
+
spend: info.spend,
|
|
5470
|
+
max_budget: info.max_budget,
|
|
5471
|
+
budget_duration: info.budget_duration,
|
|
5472
|
+
budget_reset_at: info.budget_reset_at,
|
|
5473
|
+
display: settings.user_budget_display
|
|
5474
|
+
};
|
|
5410
5475
|
return result;
|
|
5411
5476
|
};
|
|
5477
|
+
|
|
5478
|
+
// src/graphql/utilities/sanitize-and-hydrate-fields.ts
|
|
5412
5479
|
var addProviderFields = async (args, requestedFields, providers, result, tools, user, contexts) => {
|
|
5413
5480
|
let provider;
|
|
5414
5481
|
let modelRow;
|
|
@@ -5658,6 +5725,14 @@ var finalizeRequestedFields = async ({
|
|
|
5658
5725
|
if (!requestedFields.includes("provider")) {
|
|
5659
5726
|
delete result.provider;
|
|
5660
5727
|
}
|
|
5728
|
+
if (requestedFields.includes("guest_has_password")) {
|
|
5729
|
+
result.guest_has_password = !!result.guest_password_hash;
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
for (const field of table.fields) {
|
|
5733
|
+
if (field.hidden === true) {
|
|
5734
|
+
delete result[field.name];
|
|
5735
|
+
}
|
|
5661
5736
|
}
|
|
5662
5737
|
if (BUDGET_ENTITY_SINGULARS.has(table.name.singular)) {
|
|
5663
5738
|
result = await addBudgetField(requestedFields, result, table.name.singular, user);
|
|
@@ -5727,7 +5802,7 @@ var itemsPaginationRequest = async ({
|
|
|
5727
5802
|
let dataQuery = db(tableName);
|
|
5728
5803
|
dataQuery = applyFilters(dataQuery, filters, table);
|
|
5729
5804
|
dataQuery = applyAccessControl(table, dataQuery, user);
|
|
5730
|
-
dataQuery = applySorting(dataQuery, sort);
|
|
5805
|
+
dataQuery = applySorting(dataQuery, sort, void 0, table);
|
|
5731
5806
|
if (page > 1) {
|
|
5732
5807
|
dataQuery = dataQuery.offset((page - 1) * limit);
|
|
5733
5808
|
}
|
|
@@ -5750,8 +5825,20 @@ var removeProviderFields = (requestedFields) => {
|
|
|
5750
5825
|
return filtered;
|
|
5751
5826
|
};
|
|
5752
5827
|
var sanitizeRequestedFields = (table, requestedFields) => {
|
|
5828
|
+
const hiddenNames = new Set(
|
|
5829
|
+
table.fields.filter((f) => f.hidden === true).map((f) => f.name)
|
|
5830
|
+
);
|
|
5831
|
+
if (hiddenNames.size > 0) {
|
|
5832
|
+
requestedFields = requestedFields.filter((f) => !hiddenNames.has(f));
|
|
5833
|
+
}
|
|
5753
5834
|
if (table.name.singular === "agent") {
|
|
5754
5835
|
requestedFields = removeProviderFields(requestedFields);
|
|
5836
|
+
if (requestedFields.includes("guest_has_password")) {
|
|
5837
|
+
requestedFields = requestedFields.filter(
|
|
5838
|
+
(field) => field !== "guest_has_password"
|
|
5839
|
+
);
|
|
5840
|
+
requestedFields.push("guest_password_hash");
|
|
5841
|
+
}
|
|
5755
5842
|
}
|
|
5756
5843
|
if (["user", "role", "team", "project", "agent"].includes(table.name.singular)) {
|
|
5757
5844
|
requestedFields = requestedFields.filter((field) => field !== "budget");
|
|
@@ -5849,7 +5936,7 @@ function createQueries(table, providers, tools, contexts) {
|
|
|
5849
5936
|
let query = db.from(tableNamePlural).select(sanitizedFields);
|
|
5850
5937
|
query = applyFilters(query, filters, table);
|
|
5851
5938
|
query = applyAccessControl(table, query, context.user);
|
|
5852
|
-
query = applySorting(query, sort);
|
|
5939
|
+
query = applySorting(query, sort, void 0, table);
|
|
5853
5940
|
let result = await query.first();
|
|
5854
5941
|
return finalizeRequestedFields({
|
|
5855
5942
|
args,
|
|
@@ -5900,6 +5987,7 @@ function createQueries(table, providers, tools, contexts) {
|
|
|
5900
5987
|
query = applyAccessControl(table, query, context.user);
|
|
5901
5988
|
query = query.limit(limit);
|
|
5902
5989
|
if (groupBy) {
|
|
5990
|
+
assertAllowedField(table, groupBy, "group");
|
|
5903
5991
|
query = query.select(groupBy).groupBy(groupBy);
|
|
5904
5992
|
if (tableNamePlural === "tracking") {
|
|
5905
5993
|
query = query.sum("total as count");
|
|
@@ -6124,7 +6212,7 @@ var encryptSensitiveFields = (input) => {
|
|
|
6124
6212
|
};
|
|
6125
6213
|
|
|
6126
6214
|
// src/graphql/mutations/index.ts
|
|
6127
|
-
import
|
|
6215
|
+
import bcrypt3 from "bcryptjs";
|
|
6128
6216
|
|
|
6129
6217
|
// src/exulu/routines/run-state.ts
|
|
6130
6218
|
import { v4 as uuidv42 } from "uuid";
|
|
@@ -6407,7 +6495,73 @@ var handleRBACUpdate = async (db, entityName, resourceId, rbacData, existingRbac
|
|
|
6407
6495
|
}
|
|
6408
6496
|
};
|
|
6409
6497
|
|
|
6498
|
+
// src/exulu/shared-artifacts.ts
|
|
6499
|
+
import bcrypt2 from "bcryptjs";
|
|
6500
|
+
var normalizeS3Key = (key, bucket) => {
|
|
6501
|
+
const segments = key.split("/").filter((s, i) => !(i === 0 && s === "")).map((s) => decodeURIComponent(s));
|
|
6502
|
+
if (segments[0] === bucket) segments.shift();
|
|
6503
|
+
return segments.join("/");
|
|
6504
|
+
};
|
|
6505
|
+
var isHtmlKey = (key) => /\.html?$/i.test(key);
|
|
6506
|
+
var deriveFilename = (key) => {
|
|
6507
|
+
const base = key.split("/").pop() ?? key;
|
|
6508
|
+
return base.split("_EXULU_").pop() ?? base;
|
|
6509
|
+
};
|
|
6510
|
+
var slugifyShareName = (input) => deriveFilename(input).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6511
|
+
var isExpired = (expiresAt, now) => {
|
|
6512
|
+
if (!expiresAt) return false;
|
|
6513
|
+
return new Date(expiresAt).getTime() <= now.getTime();
|
|
6514
|
+
};
|
|
6515
|
+
var validateCreateInput = (input, now) => {
|
|
6516
|
+
if (!input.s3key) return { ok: false, message: "s3key is required." };
|
|
6517
|
+
if (!input.name) return { ok: false, message: "name is required." };
|
|
6518
|
+
const mode = input.auth_mode;
|
|
6519
|
+
if (mode !== "public" && mode !== "password" && mode !== "regular") {
|
|
6520
|
+
return { ok: false, message: "auth_mode must be public, password, or regular." };
|
|
6521
|
+
}
|
|
6522
|
+
if (mode === "password" && !input.password) {
|
|
6523
|
+
return { ok: false, message: "A password is required for password mode." };
|
|
6524
|
+
}
|
|
6525
|
+
if (input.expires_at && Number.isNaN(new Date(input.expires_at).getTime())) {
|
|
6526
|
+
return { ok: false, message: "expires_at is not a valid date." };
|
|
6527
|
+
}
|
|
6528
|
+
if (input.expires_at && isExpired(input.expires_at, now)) {
|
|
6529
|
+
return { ok: false, message: "expires_at must be in the future." };
|
|
6530
|
+
}
|
|
6531
|
+
return { ok: true };
|
|
6532
|
+
};
|
|
6533
|
+
var hashSharePassword = (password) => bcrypt2.hash(password, 10);
|
|
6534
|
+
var verifySharePassword = (password, hash) => bcrypt2.compare(password, hash);
|
|
6535
|
+
var contentHeadersFor = (key, contentType, filename) => {
|
|
6536
|
+
if (isHtmlKey(key)) return { contentType: "text/html; charset=utf-8" };
|
|
6537
|
+
return {
|
|
6538
|
+
contentType: contentType || "application/octet-stream",
|
|
6539
|
+
disposition: `attachment; filename="${filename.replace(/"/g, "")}"`
|
|
6540
|
+
};
|
|
6541
|
+
};
|
|
6542
|
+
var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
|
|
6543
|
+
|
|
6544
|
+
// src/graphql/utilities/agent-guest-fields.ts
|
|
6545
|
+
var VALID_GUEST_AUTH_MODES = /* @__PURE__ */ new Set(["public", "password", "regular"]);
|
|
6546
|
+
var applyAgentGuestFieldTransforms = async (input) => {
|
|
6547
|
+
if (input.guest_auth_mode !== void 0 && !VALID_GUEST_AUTH_MODES.has(input.guest_auth_mode)) {
|
|
6548
|
+
throw new Error(
|
|
6549
|
+
'guest_auth_mode must be "public", "password", or "regular".'
|
|
6550
|
+
);
|
|
6551
|
+
}
|
|
6552
|
+
delete input.guest_password_hash;
|
|
6553
|
+
if (typeof input.guest_password === "string" && input.guest_password.length > 0) {
|
|
6554
|
+
input.guest_password_hash = await hashSharePassword(input.guest_password);
|
|
6555
|
+
}
|
|
6556
|
+
delete input.guest_password;
|
|
6557
|
+
if (input.guest_auth_mode !== void 0 && input.guest_auth_mode !== "password") {
|
|
6558
|
+
input.guest_password_hash = null;
|
|
6559
|
+
}
|
|
6560
|
+
return input;
|
|
6561
|
+
};
|
|
6562
|
+
|
|
6410
6563
|
// src/graphql/mutations/index.ts
|
|
6564
|
+
var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
|
|
6411
6565
|
var postprocessDeletion = async ({
|
|
6412
6566
|
table,
|
|
6413
6567
|
requestedFields,
|
|
@@ -6601,7 +6755,9 @@ function createMutations(table, providers, contexts, tools, config) {
|
|
|
6601
6755
|
entity: table.name.singular,
|
|
6602
6756
|
target_resource_id: id,
|
|
6603
6757
|
access_type: "Role",
|
|
6604
|
-
|
|
6758
|
+
// auth.ts hydrates user.role into the full roles row when it
|
|
6759
|
+
// exists; unhydrated it is still the uuid string.
|
|
6760
|
+
role_id: user.role?.id ?? user.role,
|
|
6605
6761
|
rights: "write"
|
|
6606
6762
|
}).first();
|
|
6607
6763
|
if (rbacRecord) {
|
|
@@ -6614,7 +6770,8 @@ function createMutations(table, providers, contexts, tools, config) {
|
|
|
6614
6770
|
entity: table.name.singular,
|
|
6615
6771
|
target_resource_id: id,
|
|
6616
6772
|
access_type: "Team",
|
|
6617
|
-
|
|
6773
|
+
// Same best-effort hydration as user.role above.
|
|
6774
|
+
team_id: user.team?.id ?? user.team,
|
|
6618
6775
|
rights: "write"
|
|
6619
6776
|
}).first();
|
|
6620
6777
|
if (rbacRecord) {
|
|
@@ -6644,6 +6801,9 @@ function createMutations(table, providers, contexts, tools, config) {
|
|
|
6644
6801
|
if (item.rights_mode) {
|
|
6645
6802
|
item.rights_mode = "private";
|
|
6646
6803
|
}
|
|
6804
|
+
if (tableNamePlural === "agents" && "guest_access" in item) {
|
|
6805
|
+
item.guest_access = false;
|
|
6806
|
+
}
|
|
6647
6807
|
if (item.created_at) {
|
|
6648
6808
|
item.created_at = /* @__PURE__ */ new Date();
|
|
6649
6809
|
}
|
|
@@ -6712,9 +6872,12 @@ function createMutations(table, providers, contexts, tools, config) {
|
|
|
6712
6872
|
}
|
|
6713
6873
|
if (table.name.singular === "user" && input.password) {
|
|
6714
6874
|
console.log("[EXULU] Hashing password", input.password);
|
|
6715
|
-
input.password = await
|
|
6875
|
+
input.password = await bcrypt3.hash(input.password, SALT_ROUNDS);
|
|
6716
6876
|
console.log("[EXULU] Hashed password", input.password);
|
|
6717
6877
|
}
|
|
6878
|
+
if (table.name.singular === "agent") {
|
|
6879
|
+
input = await applyAgentGuestFieldTransforms(input);
|
|
6880
|
+
}
|
|
6718
6881
|
Object.keys(input).forEach((key) => {
|
|
6719
6882
|
if (table.fields.find((field) => field.name === key)?.type === "json") {
|
|
6720
6883
|
if (typeof input[key] === "object" || Array.isArray(input[key])) {
|
|
@@ -6728,10 +6891,15 @@ function createMutations(table, providers, contexts, tools, config) {
|
|
|
6728
6891
|
input.id = db.fn.uuid();
|
|
6729
6892
|
}
|
|
6730
6893
|
}
|
|
6894
|
+
if (table.RBAC && input.rights_mode != null && !VALID_RIGHTS_MODES.includes(input.rights_mode)) {
|
|
6895
|
+
throw new Error(
|
|
6896
|
+
`Invalid rights_mode "${input.rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES.join(", ")}`
|
|
6897
|
+
);
|
|
6898
|
+
}
|
|
6731
6899
|
const columns = await db(tableNamePlural).columnInfo();
|
|
6732
6900
|
const insert = db(tableNamePlural).insert({
|
|
6733
6901
|
...input,
|
|
6734
|
-
...table.RBAC ? { rights_mode: "private" } : {}
|
|
6902
|
+
...table.RBAC ? { rights_mode: input.rights_mode ?? "private" } : {}
|
|
6735
6903
|
}).returning(Object.keys(columns));
|
|
6736
6904
|
if (args.upsert) {
|
|
6737
6905
|
insert.onConflict().merge();
|
|
@@ -6779,9 +6947,12 @@ function createMutations(table, providers, contexts, tools, config) {
|
|
|
6779
6947
|
}
|
|
6780
6948
|
if (table.name.singular === "user" && input.password) {
|
|
6781
6949
|
console.log("[EXULU] Hashing password", input.password);
|
|
6782
|
-
input.password = await
|
|
6950
|
+
input.password = await bcrypt3.hash(input.password, SALT_ROUNDS);
|
|
6783
6951
|
console.log("[EXULU] Hashed password", input.password);
|
|
6784
6952
|
}
|
|
6953
|
+
if (table.name.singular === "agent") {
|
|
6954
|
+
input = await applyAgentGuestFieldTransforms(input);
|
|
6955
|
+
}
|
|
6785
6956
|
Object.keys(input).forEach((key) => {
|
|
6786
6957
|
if (table.fields.find((field) => field.name === key)?.type === "json") {
|
|
6787
6958
|
if (typeof input[key] === "object" || Array.isArray(input[key])) {
|
|
@@ -6852,9 +7023,12 @@ function createMutations(table, providers, contexts, tools, config) {
|
|
|
6852
7023
|
}
|
|
6853
7024
|
if (table.name.singular === "user" && input.password) {
|
|
6854
7025
|
console.log("[EXULU] Hashing password", input.password);
|
|
6855
|
-
input.password = await
|
|
7026
|
+
input.password = await bcrypt3.hash(input.password, SALT_ROUNDS);
|
|
6856
7027
|
console.log("[EXULU] Hashed password", input.password);
|
|
6857
7028
|
}
|
|
7029
|
+
if (table.name.singular === "agent") {
|
|
7030
|
+
input = await applyAgentGuestFieldTransforms(input);
|
|
7031
|
+
}
|
|
6858
7032
|
Object.keys(input).forEach((key) => {
|
|
6859
7033
|
if (table.fields.find((field) => field.name === key)?.type === "json") {
|
|
6860
7034
|
if (typeof input[key] === "object" || Array.isArray(input[key])) {
|
|
@@ -7346,6 +7520,9 @@ var getEnabledTools = async (agent, allExuluTools, allContexts, disabledTools =
|
|
|
7346
7520
|
model: void 0
|
|
7347
7521
|
});
|
|
7348
7522
|
}
|
|
7523
|
+
if (id === KB_EDITOR_TOOL_ID) {
|
|
7524
|
+
return null;
|
|
7525
|
+
}
|
|
7349
7526
|
if (type === "agent") {
|
|
7350
7527
|
if (id === agent.id) {
|
|
7351
7528
|
return null;
|
|
@@ -7654,6 +7831,35 @@ var isRunSessionMetadata = (metadata) => {
|
|
|
7654
7831
|
return typeof parsed === "object" && parsed !== null && !!parsed.job_result_id;
|
|
7655
7832
|
};
|
|
7656
7833
|
|
|
7834
|
+
// src/exulu/auth/sanitize-ui-messages.ts
|
|
7835
|
+
var sanitizeAuthPayloadsInUiMessages = (messages) => messages.map((message) => {
|
|
7836
|
+
if (message.role !== "assistant" || !Array.isArray(message.parts)) {
|
|
7837
|
+
return message;
|
|
7838
|
+
}
|
|
7839
|
+
let changed = false;
|
|
7840
|
+
const parts = message.parts.map((part) => {
|
|
7841
|
+
const output = part?.output;
|
|
7842
|
+
if (output && typeof output === "object" && output.credentialRequest) {
|
|
7843
|
+
changed = true;
|
|
7844
|
+
return { ...part, output: { result: SCRUBBED_CREDENTIAL_TEXT } };
|
|
7845
|
+
}
|
|
7846
|
+
if (output && typeof output === "object" && output.oauth?.authorizationUrl) {
|
|
7847
|
+
changed = true;
|
|
7848
|
+
return { ...part, output: { result: SCRUBBED_OAUTH_TEXT } };
|
|
7849
|
+
}
|
|
7850
|
+
return part;
|
|
7851
|
+
});
|
|
7852
|
+
return changed ? { ...message, parts } : message;
|
|
7853
|
+
});
|
|
7854
|
+
|
|
7855
|
+
// src/exulu/auth/guardrail.ts
|
|
7856
|
+
var CREDENTIAL_GUARDRAIL = `Credential safety:
|
|
7857
|
+
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.
|
|
7858
|
+
- Never ask the user to type credential values into the chat.
|
|
7859
|
+
- 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).
|
|
7860
|
+
- After the user confirms they submitted the form, call the tool again.`;
|
|
7861
|
+
var credentialGuardrailBlock = (currentTools) => currentTools?.some((t) => t.authentication?.authType === "user_credentials") ? CREDENTIAL_GUARDRAIL : null;
|
|
7862
|
+
|
|
7657
7863
|
// src/exulu/provider.ts
|
|
7658
7864
|
var ExuluProvider = class {
|
|
7659
7865
|
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
@@ -8023,6 +8229,10 @@ var ExuluProvider = class {
|
|
|
8023
8229
|
|
|
8024
8230
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
8025
8231
|
'Inform the user that the action was not performed.`;
|
|
8232
|
+
const credentialGuardrail = credentialGuardrailBlock(currentTools);
|
|
8233
|
+
if (credentialGuardrail) {
|
|
8234
|
+
system += "\n\n" + credentialGuardrail;
|
|
8235
|
+
}
|
|
8026
8236
|
if (prompt) {
|
|
8027
8237
|
let result = { object: null, text: "" };
|
|
8028
8238
|
let inputTokens = 0;
|
|
@@ -8100,8 +8310,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
8100
8310
|
model,
|
|
8101
8311
|
// Should be a LanguageModelV1
|
|
8102
8312
|
system,
|
|
8103
|
-
|
|
8104
|
-
|
|
8313
|
+
// tools: applies each tool's toModelOutput to historical tool results;
|
|
8314
|
+
// sanitize: guarantees auth payloads never reach the model regardless
|
|
8315
|
+
// of part encoding (spec 2026-07-22 §1.2).
|
|
8316
|
+
messages: await convertToModelMessages(sanitizeAuthPayloadsInUiMessages(messages), {
|
|
8317
|
+
ignoreIncompleteToolCalls: true,
|
|
8318
|
+
tools
|
|
8105
8319
|
}),
|
|
8106
8320
|
maxRetries: 2,
|
|
8107
8321
|
tools,
|
|
@@ -8421,6 +8635,10 @@ ${skillsList}
|
|
|
8421
8635
|
|
|
8422
8636
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
8423
8637
|
'Inform the user that the action was not performed.`;
|
|
8638
|
+
const credentialGuardrail = credentialGuardrailBlock(currentTools);
|
|
8639
|
+
if (credentialGuardrail) {
|
|
8640
|
+
system += "\n\n" + credentialGuardrail;
|
|
8641
|
+
}
|
|
8424
8642
|
console.log("[EXULU] Tools", currentTools?.map((x) => x.name));
|
|
8425
8643
|
console.log("[EXULU] Skills", currentSkills?.map((x) => x.name));
|
|
8426
8644
|
const tools = await convertExuluToolsToAiSdkTools(
|
|
@@ -8504,8 +8722,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
8504
8722
|
// TODO Make this configurable
|
|
8505
8723
|
model,
|
|
8506
8724
|
// Should be a LanguageModelV1
|
|
8507
|
-
|
|
8508
|
-
|
|
8725
|
+
// tools: applies each tool's toModelOutput to historical tool results;
|
|
8726
|
+
// sanitize: guarantees auth payloads never reach the model regardless
|
|
8727
|
+
// of part encoding (spec 2026-07-22 §1.2).
|
|
8728
|
+
messages: await convertToModelMessages(sanitizeAuthPayloadsInUiMessages(messages), {
|
|
8729
|
+
ignoreIncompleteToolCalls: true,
|
|
8730
|
+
tools
|
|
8509
8731
|
}),
|
|
8510
8732
|
// PrepareStep could be used here to set the model
|
|
8511
8733
|
// for the first step or change other parameters.
|
|
@@ -10822,10 +11044,25 @@ var RecallApiError = class extends Error {
|
|
|
10822
11044
|
}
|
|
10823
11045
|
};
|
|
10824
11046
|
var jitterMs = () => 1e3 * Math.ceil(Math.random() * 5);
|
|
11047
|
+
var API_TIMEOUT_MS = 6e4;
|
|
11048
|
+
var DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
10825
11049
|
async function fetch_with_retry(args) {
|
|
10826
|
-
const { url, options, max_attempts = 6 } = args;
|
|
11050
|
+
const { url, options, max_attempts = 6, timeout_ms, retry_on_reject } = args;
|
|
10827
11051
|
for (let attempt = 1; attempt <= max_attempts; attempt++) {
|
|
10828
|
-
|
|
11052
|
+
let response;
|
|
11053
|
+
try {
|
|
11054
|
+
response = await fetch(
|
|
11055
|
+
url,
|
|
11056
|
+
timeout_ms ? { ...options, signal: AbortSignal.timeout(timeout_ms) } : options
|
|
11057
|
+
);
|
|
11058
|
+
} catch (err) {
|
|
11059
|
+
if (!retry_on_reject || attempt === max_attempts) throw err;
|
|
11060
|
+
console.log(
|
|
11061
|
+
`[EXULU-RECALL] fetch error from ${url} (${err.message}); retrying in ~5s (attempt ${attempt}/${max_attempts})`
|
|
11062
|
+
);
|
|
11063
|
+
await new Promise((resolve5) => setTimeout(resolve5, 5e3 + jitterMs()));
|
|
11064
|
+
continue;
|
|
11065
|
+
}
|
|
10829
11066
|
let wait_for = null;
|
|
10830
11067
|
switch (response.status) {
|
|
10831
11068
|
case 429:
|
|
@@ -10866,7 +11103,9 @@ var request2 = async (path2, init = {}) => {
|
|
|
10866
11103
|
...init.body ? { "content-type": "application/json" } : {},
|
|
10867
11104
|
...init.headers ?? {}
|
|
10868
11105
|
}
|
|
10869
|
-
}
|
|
11106
|
+
},
|
|
11107
|
+
timeout_ms: API_TIMEOUT_MS,
|
|
11108
|
+
retry_on_reject: (init.method ?? "GET").toUpperCase() === "GET"
|
|
10870
11109
|
});
|
|
10871
11110
|
if (!response.ok) {
|
|
10872
11111
|
const body = await response.text();
|
|
@@ -10938,7 +11177,9 @@ var recallClient = {
|
|
|
10938
11177
|
downloadTranscript: async (downloadUrl) => {
|
|
10939
11178
|
const response = await fetch_with_retry({
|
|
10940
11179
|
url: downloadUrl,
|
|
10941
|
-
options: { method: "GET" }
|
|
11180
|
+
options: { method: "GET" },
|
|
11181
|
+
timeout_ms: DOWNLOAD_TIMEOUT_MS,
|
|
11182
|
+
retry_on_reject: true
|
|
10942
11183
|
});
|
|
10943
11184
|
if (!response.ok) {
|
|
10944
11185
|
const body = await response.text();
|
|
@@ -10991,6 +11232,12 @@ var durationFromSegments = (segments) => {
|
|
|
10991
11232
|
// src/exulu/recall/service.ts
|
|
10992
11233
|
var TABLE2 = "transcription_jobs";
|
|
10993
11234
|
var DEFAULT_BOT_NAME = "Company Notetaker";
|
|
11235
|
+
var RECONCILE_STALE_MS = 10 * 60 * 1e3;
|
|
11236
|
+
var RECONCILE_PROBE_QUIET_MS = 60 * 60 * 1e3;
|
|
11237
|
+
var RECONCILE_GIVE_UP_MS = 24 * 60 * 60 * 1e3;
|
|
11238
|
+
var POST_PROCESSING_REDO_STALE_MS = 30 * 60 * 1e3;
|
|
11239
|
+
var POST_PROCESSING_PROMPT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
11240
|
+
var BOT_ENDED_STATUSES = ["done", "call_ended"];
|
|
10994
11241
|
var log3 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
|
|
10995
11242
|
var parseJson = (v) => {
|
|
10996
11243
|
if (v == null) return null;
|
|
@@ -11067,7 +11314,9 @@ var recallService = {
|
|
|
11067
11314
|
target_rights_mode: input.target_rights_mode ?? "private",
|
|
11068
11315
|
target_rbac_users: input.target_rbac_users ? JSON.stringify(input.target_rbac_users) : null,
|
|
11069
11316
|
target_rbac_roles: input.target_rbac_roles ? JSON.stringify(input.target_rbac_roles) : null,
|
|
11070
|
-
|
|
11317
|
+
// Normalized to NULL when empty so the reconcile sweep's redo select
|
|
11318
|
+
// ("prompts configured but never ran") can never match a no-prompt row.
|
|
11319
|
+
post_processing_prompts: input.post_processing_prompts?.length ? JSON.stringify(input.post_processing_prompts) : null,
|
|
11071
11320
|
rights_mode: "private",
|
|
11072
11321
|
created_by: input.userId,
|
|
11073
11322
|
createdAt: now,
|
|
@@ -11136,7 +11385,13 @@ var recallService = {
|
|
|
11136
11385
|
return;
|
|
11137
11386
|
}
|
|
11138
11387
|
const { db } = await postgresClient();
|
|
11139
|
-
const claimed = await db(TABLE2).where({ id: jobId }).whereNotIn("status", [
|
|
11388
|
+
const claimed = await db(TABLE2).where({ id: jobId }).whereNotIn("status", [
|
|
11389
|
+
"transcribing",
|
|
11390
|
+
"awaiting_review",
|
|
11391
|
+
"saved",
|
|
11392
|
+
"failed",
|
|
11393
|
+
"cancelled"
|
|
11394
|
+
]).update({
|
|
11140
11395
|
recall_recording_id: recordingId,
|
|
11141
11396
|
status: "transcribing",
|
|
11142
11397
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -11228,14 +11483,55 @@ var recallService = {
|
|
|
11228
11483
|
log3(`post-processing for job ${jobId} skipped: no transcript.`);
|
|
11229
11484
|
return [];
|
|
11230
11485
|
}
|
|
11486
|
+
const claimed = await db(TABLE2).where({ id: jobId }).whereRaw(
|
|
11487
|
+
`(post_processing_outputs IS NULL OR (post_processing_outputs::text = '[]' AND "updatedAt" < ?))`,
|
|
11488
|
+
[new Date(Date.now() - POST_PROCESSING_REDO_STALE_MS)]
|
|
11489
|
+
).update({ post_processing_outputs: "[]", updatedAt: /* @__PURE__ */ new Date() });
|
|
11490
|
+
if (!claimed) {
|
|
11491
|
+
log3(`post-processing for job ${jobId} already in flight; skipping.`);
|
|
11492
|
+
return [];
|
|
11493
|
+
}
|
|
11231
11494
|
const outputs = [];
|
|
11232
11495
|
for (const p of prompts) {
|
|
11233
11496
|
outputs.push(await this._runOnePrompt(job, p.prompt_id, p.agent_id));
|
|
11497
|
+
await this._update(jobId, {});
|
|
11234
11498
|
}
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11499
|
+
return this._mergeOutputs(jobId, outputs);
|
|
11500
|
+
},
|
|
11501
|
+
/**
|
|
11502
|
+
* Upsert entries into post_processing_outputs without clobbering concurrent
|
|
11503
|
+
* writers: optimistic-concurrency loop — read, merge by {prompt_id,
|
|
11504
|
+
* agent_id}, write only while the column still matches the snapshot.
|
|
11505
|
+
*/
|
|
11506
|
+
async _mergeOutputs(jobId, entries) {
|
|
11507
|
+
const { db } = await postgresClient();
|
|
11508
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
11509
|
+
const row = await db(TABLE2).where({ id: jobId }).first();
|
|
11510
|
+
if (!row) return entries;
|
|
11511
|
+
const raw = row.post_processing_outputs ?? null;
|
|
11512
|
+
const snapshot = raw == null ? null : typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
11513
|
+
const current = parseJson(raw) ?? [];
|
|
11514
|
+
const merged = [
|
|
11515
|
+
...current.filter(
|
|
11516
|
+
(o) => !entries.some(
|
|
11517
|
+
(e) => e.prompt_id === o.prompt_id && e.agent_id === o.agent_id
|
|
11518
|
+
)
|
|
11519
|
+
),
|
|
11520
|
+
...entries
|
|
11521
|
+
];
|
|
11522
|
+
const updated = await db(TABLE2).where({ id: jobId }).whereRaw(
|
|
11523
|
+
"post_processing_outputs::jsonb IS NOT DISTINCT FROM ?::jsonb",
|
|
11524
|
+
[snapshot]
|
|
11525
|
+
).update({
|
|
11526
|
+
post_processing_outputs: JSON.stringify(merged),
|
|
11527
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
11528
|
+
});
|
|
11529
|
+
if (updated) return merged;
|
|
11530
|
+
}
|
|
11531
|
+
log3(
|
|
11532
|
+
`post-processing outputs for job ${jobId} kept changing under the merge; leaving the concurrent writer's data in place.`
|
|
11533
|
+
);
|
|
11534
|
+
return entries;
|
|
11239
11535
|
},
|
|
11240
11536
|
/**
|
|
11241
11537
|
* Manual single-prompt run (re-run from the review sheet). Upserts the matching
|
|
@@ -11246,13 +11542,14 @@ var recallService = {
|
|
|
11246
11542
|
const dbRow = await db(TABLE2).where({ id: jobId }).first();
|
|
11247
11543
|
if (!dbRow) throw new Error(`transcription_job ${jobId} not found`);
|
|
11248
11544
|
const job = this._row(dbRow);
|
|
11545
|
+
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;
|
|
11546
|
+
if (claimInFlight) {
|
|
11547
|
+
throw new Error(
|
|
11548
|
+
"POST_PROCESSING_IN_FLIGHT: the automatic post-processing run is still in progress; its results will appear shortly."
|
|
11549
|
+
);
|
|
11550
|
+
}
|
|
11249
11551
|
const result = await this._runOnePrompt(job, promptId, agentId);
|
|
11250
|
-
|
|
11251
|
-
const next = existing.filter(
|
|
11252
|
-
(o) => !(o.prompt_id === promptId && o.agent_id === agentId)
|
|
11253
|
-
);
|
|
11254
|
-
next.push(result);
|
|
11255
|
-
await this._update(jobId, { post_processing_outputs: JSON.stringify(next) });
|
|
11552
|
+
await this._mergeOutputs(jobId, [result]);
|
|
11256
11553
|
return result;
|
|
11257
11554
|
},
|
|
11258
11555
|
async _runOnePrompt(job, promptId, agentId) {
|
|
@@ -11286,7 +11583,8 @@ var recallService = {
|
|
|
11286
11583
|
Meeting transcript:
|
|
11287
11584
|
|
|
11288
11585
|
${transcriptText}`,
|
|
11289
|
-
maxRetries: 3
|
|
11586
|
+
maxRetries: 3,
|
|
11587
|
+
abortSignal: AbortSignal.timeout(POST_PROCESSING_PROMPT_TIMEOUT_MS)
|
|
11290
11588
|
});
|
|
11291
11589
|
return {
|
|
11292
11590
|
prompt_id: promptId,
|
|
@@ -11310,6 +11608,206 @@ ${transcriptText}`,
|
|
|
11310
11608
|
};
|
|
11311
11609
|
}
|
|
11312
11610
|
},
|
|
11611
|
+
/**
|
|
11612
|
+
* Reconciliation sweep: re-drive recall jobs whose webhook event was lost
|
|
11613
|
+
* (ACK-first delivery + crash/restart = no redelivery). Called periodically
|
|
11614
|
+
* from the reconcile loop. Returns the number of jobs it acted on.
|
|
11615
|
+
*
|
|
11616
|
+
* Idempotent by construction: every recovery path funnels into the same
|
|
11617
|
+
* guarded transitions the webhook handlers use (_onRecordingDone's atomic
|
|
11618
|
+
* claim, _onTranscriptDone's already-processed check, runPostProcessing's
|
|
11619
|
+
* skip-if-outputs-exist), so a webhook racing the sweep is harmless.
|
|
11620
|
+
*/
|
|
11621
|
+
async reconcileOnce(limit = 10) {
|
|
11622
|
+
if (!recallEnabled()) return 0;
|
|
11623
|
+
const { db } = await postgresClient();
|
|
11624
|
+
const now = Date.now();
|
|
11625
|
+
const stuck = await db(TABLE2).where({ source: "recall" }).whereIn("status", ["queued", "transcribing"]).whereRaw(`("join_at" IS NULL OR "join_at" < ?)`, [
|
|
11626
|
+
new Date(now - RECONCILE_STALE_MS)
|
|
11627
|
+
]).where("updatedAt", "<", new Date(now - RECONCILE_STALE_MS)).orderBy("updatedAt", "asc").limit(limit);
|
|
11628
|
+
const redo = await db(TABLE2).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(
|
|
11629
|
+
"(post_processing_outputs IS NULL OR post_processing_outputs::text = '[]')"
|
|
11630
|
+
).where("updatedAt", "<", new Date(now - POST_PROCESSING_REDO_STALE_MS)).orderBy("updatedAt", "asc").limit(limit);
|
|
11631
|
+
let acted = 0;
|
|
11632
|
+
for (const dbRow of stuck) {
|
|
11633
|
+
const job = this._row(dbRow);
|
|
11634
|
+
try {
|
|
11635
|
+
if (await this._reconcileStuckJob(job)) acted++;
|
|
11636
|
+
} catch (err) {
|
|
11637
|
+
if (await this._reconcileError(job, err)) acted++;
|
|
11638
|
+
}
|
|
11639
|
+
}
|
|
11640
|
+
for (const dbRow of redo) {
|
|
11641
|
+
try {
|
|
11642
|
+
log3(`reconcile: re-running lost post-processing for job ${dbRow.id}`);
|
|
11643
|
+
const outputs = await this.runPostProcessing(dbRow.id);
|
|
11644
|
+
if (outputs.length > 0) acted++;
|
|
11645
|
+
} catch (err) {
|
|
11646
|
+
log3(
|
|
11647
|
+
`post-processing redo failed for job ${dbRow.id}: ${err.message}`
|
|
11648
|
+
);
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11651
|
+
return acted;
|
|
11652
|
+
},
|
|
11653
|
+
async _reconcileStuckJob(job) {
|
|
11654
|
+
if (!job.recall_bot_id) {
|
|
11655
|
+
await this._fail(job.id, "bot was never launched (lost during creation)");
|
|
11656
|
+
return true;
|
|
11657
|
+
}
|
|
11658
|
+
if (job.status === "queued") return this._reconcileQueued(job);
|
|
11659
|
+
if (job.status === "transcribing") return this._reconcileTranscribing(job);
|
|
11660
|
+
return false;
|
|
11661
|
+
},
|
|
11662
|
+
/**
|
|
11663
|
+
* A recovery step threw. 404 means the Recall object is gone — terminal.
|
|
11664
|
+
* Otherwise apply the 24h give-up (API errors must not defer it forever),
|
|
11665
|
+
* and touch the row so it rotates to the back of the sweep window.
|
|
11666
|
+
*/
|
|
11667
|
+
async _reconcileError(job, err) {
|
|
11668
|
+
const message = err.message ?? String(err);
|
|
11669
|
+
if (err.status === 404) {
|
|
11670
|
+
await this._fail(
|
|
11671
|
+
job.id,
|
|
11672
|
+
`Recall no longer knows this recording: ${message}`
|
|
11673
|
+
);
|
|
11674
|
+
return true;
|
|
11675
|
+
}
|
|
11676
|
+
if (this._pastGiveUp(job)) {
|
|
11677
|
+
await this._fail(
|
|
11678
|
+
job.id,
|
|
11679
|
+
`still stuck 24 hours after the meeting start (last error: ${message})`
|
|
11680
|
+
);
|
|
11681
|
+
return true;
|
|
11682
|
+
}
|
|
11683
|
+
log3(`reconcile failed for job ${job.id}: ${message}`);
|
|
11684
|
+
await this._update(job.id, {});
|
|
11685
|
+
return false;
|
|
11686
|
+
},
|
|
11687
|
+
/** A queued row: the recording.done event (or the whole bot lifecycle) was lost. */
|
|
11688
|
+
async _reconcileQueued(job) {
|
|
11689
|
+
const ended = !!job.bot_status && BOT_ENDED_STATUSES.includes(job.bot_status);
|
|
11690
|
+
const quietMs = Date.now() - new Date(job.updatedAt).getTime();
|
|
11691
|
+
if (!ended && quietMs < RECONCILE_PROBE_QUIET_MS) return false;
|
|
11692
|
+
const bot = await recallClient.retrieveBot(job.recall_bot_id);
|
|
11693
|
+
const code = bot?.status_changes?.at(-1)?.code ?? null;
|
|
11694
|
+
if (code === "fatal") {
|
|
11695
|
+
await this._fail(job.id, "bot fatal (recovered by reconciliation)");
|
|
11696
|
+
return true;
|
|
11697
|
+
}
|
|
11698
|
+
const recording = bot?.recordings?.[0] ?? null;
|
|
11699
|
+
if (recording?.id) {
|
|
11700
|
+
const readiness = await this._recordingReadiness(recording);
|
|
11701
|
+
if (readiness === "failed") {
|
|
11702
|
+
await this._fail(job.id, "recording failed (found by reconciliation)");
|
|
11703
|
+
return true;
|
|
11704
|
+
}
|
|
11705
|
+
if (readiness === "done") {
|
|
11706
|
+
log3(`reconcile: driving lost recording.done for job ${job.id}`);
|
|
11707
|
+
await this._onRecordingDone(job.id, recording.id);
|
|
11708
|
+
return true;
|
|
11709
|
+
}
|
|
11710
|
+
} else if (code === "done") {
|
|
11711
|
+
await this._fail(job.id, "bot finished without a recording");
|
|
11712
|
+
return true;
|
|
11713
|
+
}
|
|
11714
|
+
if (this._pastGiveUp(job)) {
|
|
11715
|
+
await this._fail(
|
|
11716
|
+
job.id,
|
|
11717
|
+
"no recording within 24 hours of the meeting start"
|
|
11718
|
+
);
|
|
11719
|
+
return true;
|
|
11720
|
+
}
|
|
11721
|
+
await this._update(
|
|
11722
|
+
job.id,
|
|
11723
|
+
code && code !== job.bot_status ? { bot_status: code } : {}
|
|
11724
|
+
);
|
|
11725
|
+
return false;
|
|
11726
|
+
},
|
|
11727
|
+
/** Whether a recording is safe to transcribe, checking the full recording
|
|
11728
|
+
* object when the bot payload carries no status. */
|
|
11729
|
+
async _recordingReadiness(recording) {
|
|
11730
|
+
const codeOf = (c) => c === "done" ? "done" : c === "failed" ? "failed" : c ? "processing" : null;
|
|
11731
|
+
const fromBot = codeOf(recording.status?.code);
|
|
11732
|
+
if (fromBot) return fromBot;
|
|
11733
|
+
if (recording.completed_at) return "done";
|
|
11734
|
+
const full = await recallClient.retrieveRecording(recording.id);
|
|
11735
|
+
const fromFull = codeOf(full?.status?.code);
|
|
11736
|
+
if (fromFull) return fromFull;
|
|
11737
|
+
return full?.completed_at || recordingDurationSeconds(full) != null ? "done" : "processing";
|
|
11738
|
+
},
|
|
11739
|
+
/**
|
|
11740
|
+
* A transcribing row: transcript.done was lost, or the original
|
|
11741
|
+
* recording.done handling crashed between the status claim and
|
|
11742
|
+
* createAsyncTranscript (in which case Recall was never asked to
|
|
11743
|
+
* transcribe and no transcript.* event will ever arrive).
|
|
11744
|
+
*/
|
|
11745
|
+
async _reconcileTranscribing(job) {
|
|
11746
|
+
let recordingId = job.recall_recording_id;
|
|
11747
|
+
if (!recordingId && !job.recall_transcript_id) {
|
|
11748
|
+
const bot = await recallClient.retrieveBot(job.recall_bot_id);
|
|
11749
|
+
recordingId = bot?.recordings?.[0]?.id ?? null;
|
|
11750
|
+
}
|
|
11751
|
+
let transcriptId = job.recall_transcript_id;
|
|
11752
|
+
if (!transcriptId) {
|
|
11753
|
+
if (!recordingId) return this._transcribingNotReady(job);
|
|
11754
|
+
const rec = await recallClient.retrieveRecording(recordingId);
|
|
11755
|
+
const shortcut = rec?.media_shortcuts?.transcript;
|
|
11756
|
+
if (shortcut?.id) {
|
|
11757
|
+
transcriptId = shortcut.id;
|
|
11758
|
+
await this._update(job.id, {
|
|
11759
|
+
recall_recording_id: recordingId,
|
|
11760
|
+
recall_transcript_id: transcriptId
|
|
11761
|
+
});
|
|
11762
|
+
} else {
|
|
11763
|
+
const { db } = await postgresClient();
|
|
11764
|
+
const claimed = await db(TABLE2).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() });
|
|
11765
|
+
if (!claimed) return false;
|
|
11766
|
+
log3(
|
|
11767
|
+
`reconcile: re-requesting transcript for job ${job.id} (lost before createAsyncTranscript)`
|
|
11768
|
+
);
|
|
11769
|
+
const transcript2 = await recallClient.createAsyncTranscript(
|
|
11770
|
+
recordingId,
|
|
11771
|
+
job.language || "auto"
|
|
11772
|
+
);
|
|
11773
|
+
await this._update(job.id, {
|
|
11774
|
+
recall_transcript_id: transcript2.id ?? null
|
|
11775
|
+
});
|
|
11776
|
+
return true;
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11779
|
+
const transcript = await recallClient.retrieveTranscript(transcriptId);
|
|
11780
|
+
const transcriptCode = transcript?.status?.code ?? null;
|
|
11781
|
+
if (transcriptCode === "error" || transcriptCode === "failed") {
|
|
11782
|
+
await this._fail(
|
|
11783
|
+
job.id,
|
|
11784
|
+
`transcript failed at Recall: ${transcript?.status?.sub_code || transcriptCode}`
|
|
11785
|
+
);
|
|
11786
|
+
return true;
|
|
11787
|
+
}
|
|
11788
|
+
if (transcript?.data?.download_url) {
|
|
11789
|
+
log3(`reconcile: driving lost transcript.done for job ${job.id}`);
|
|
11790
|
+
await this._onTranscriptDone(job.id, transcriptId, recordingId);
|
|
11791
|
+
return true;
|
|
11792
|
+
}
|
|
11793
|
+
return this._transcribingNotReady(job);
|
|
11794
|
+
},
|
|
11795
|
+
/** Transcript not ready yet: wait (touch) or give up after the hard cap. */
|
|
11796
|
+
async _transcribingNotReady(job) {
|
|
11797
|
+
if (this._pastGiveUp(job)) {
|
|
11798
|
+
await this._fail(
|
|
11799
|
+
job.id,
|
|
11800
|
+
"transcript was not ready within 24 hours of the meeting start"
|
|
11801
|
+
);
|
|
11802
|
+
return true;
|
|
11803
|
+
}
|
|
11804
|
+
await this._update(job.id, {});
|
|
11805
|
+
return false;
|
|
11806
|
+
},
|
|
11807
|
+
_pastGiveUp(job) {
|
|
11808
|
+
const startedAt = new Date(job.join_at ?? job.createdAt).getTime();
|
|
11809
|
+
return Number.isFinite(startedAt) && Date.now() - startedAt > RECONCILE_GIVE_UP_MS;
|
|
11810
|
+
},
|
|
11313
11811
|
async _findJob(botId, recordingId, transcriptId) {
|
|
11314
11812
|
const { db } = await postgresClient();
|
|
11315
11813
|
let dbRow;
|
|
@@ -11539,7 +12037,8 @@ function createExuluContextsTypeDefs(table) {
|
|
|
11539
12037
|
${enumValues}
|
|
11540
12038
|
}`;
|
|
11541
12039
|
}).filter((enumDef) => enumDef !== null).join("\n");
|
|
11542
|
-
|
|
12040
|
+
const graphqlFields = table.fields.filter((field) => field.hidden !== true);
|
|
12041
|
+
let fields = graphqlFields.map((field) => {
|
|
11543
12042
|
let type;
|
|
11544
12043
|
type = mapExuluFieldTypesToGraphqlTypes(field);
|
|
11545
12044
|
const required = field.required ? "!" : "";
|
|
@@ -11560,6 +12059,7 @@ function createExuluContextsTypeDefs(table) {
|
|
|
11560
12059
|
fields.push(" systemInstructions: String");
|
|
11561
12060
|
fields.push(" workflows: AgentWorkflows");
|
|
11562
12061
|
fields.push(" slug: String");
|
|
12062
|
+
fields.push(" guest_has_password: Boolean");
|
|
11563
12063
|
}
|
|
11564
12064
|
if (table.name.singular === "workflow_template") {
|
|
11565
12065
|
fields.push(" variables: [String]");
|
|
@@ -11576,16 +12076,20 @@ function createExuluContextsTypeDefs(table) {
|
|
|
11576
12076
|
}
|
|
11577
12077
|
`;
|
|
11578
12078
|
const rbacInputField = table.RBAC ? " RBAC: RBACInput" : "";
|
|
12079
|
+
const inputFields = table.fields.filter((f) => f.name !== "guest_password_hash");
|
|
12080
|
+
const inputExtra = table.name.singular === "agent" ? " guest_password: String" : "";
|
|
11579
12081
|
const inputDef = `
|
|
11580
12082
|
input ${table.name.singular}Input {
|
|
11581
|
-
${
|
|
12083
|
+
${inputFields.map((f) => ` ${f.name}: ${mapExuluFieldTypesToGraphqlTypes(f)}`).join("\n")}
|
|
12084
|
+
${inputExtra}
|
|
11582
12085
|
${rbacInputField}
|
|
11583
12086
|
}
|
|
11584
12087
|
`;
|
|
11585
12088
|
return enumDefs + typeDef + inputDef;
|
|
11586
12089
|
}
|
|
11587
12090
|
function createExuluContextsFilterTypeDefs(table) {
|
|
11588
|
-
const
|
|
12091
|
+
const filterFields = table.fields.filter((field) => field.hidden !== true);
|
|
12092
|
+
const fieldFilters = filterFields.map((field) => {
|
|
11589
12093
|
let type;
|
|
11590
12094
|
if (field.type === "enum" && field.enumValues) {
|
|
11591
12095
|
type = `${field.name}Enum`;
|
|
@@ -11598,7 +12102,7 @@ function createExuluContextsFilterTypeDefs(table) {
|
|
|
11598
12102
|
let operatorTypes = "";
|
|
11599
12103
|
let enumFilterOperators = [];
|
|
11600
12104
|
const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
|
|
11601
|
-
enumFilterOperators =
|
|
12105
|
+
enumFilterOperators = filterFields.filter((field) => field.type === "enum" && field.enumValues).map((field) => {
|
|
11602
12106
|
const enumTypeName = `${field.name}Enum`;
|
|
11603
12107
|
return `
|
|
11604
12108
|
input FilterOperator${enumTypeName} {
|
|
@@ -13157,12 +13661,12 @@ type LiteLLMModel {
|
|
|
13157
13661
|
nonArchived().andWhere((b) => b.whereNull("chunks_count").orWhere("chunks_count", "<=", 0)).count("* as c").first(),
|
|
13158
13662
|
nonArchived().where("embeddings_updated_at", "<=", staleCutoff).count("* as c").first()
|
|
13159
13663
|
]);
|
|
13160
|
-
const
|
|
13664
|
+
const num2 = (v) => v == null ? 0 : Number(v);
|
|
13161
13665
|
return {
|
|
13162
|
-
item_count:
|
|
13163
|
-
chunk_total:
|
|
13164
|
-
stuck_count:
|
|
13165
|
-
stale_count:
|
|
13666
|
+
item_count: num2(itemRow?.c),
|
|
13667
|
+
chunk_total: num2(chunkRow?.s),
|
|
13668
|
+
stuck_count: num2(stuckRow?.c),
|
|
13669
|
+
stale_count: num2(staleRow?.c)
|
|
13166
13670
|
};
|
|
13167
13671
|
} catch (err) {
|
|
13168
13672
|
console.error("[EXULU] computeContextAggregates failed for", contextId, err);
|
|
@@ -13368,6 +13872,7 @@ type LiteLLMModel {
|
|
|
13368
13872
|
if (agenticRetrievalTool) {
|
|
13369
13873
|
allTools.push(agenticRetrievalTool);
|
|
13370
13874
|
}
|
|
13875
|
+
allTools.push(createKbEditorPickerTool());
|
|
13371
13876
|
}
|
|
13372
13877
|
if (search && search.trim()) {
|
|
13373
13878
|
const searchTerm = search.toLowerCase().trim();
|
|
@@ -14479,6 +14984,125 @@ See docs/superpowers/specs/2026-05-31-in-chat-image-generation-design.md for the
|
|
|
14479
14984
|
|
|
14480
14985
|
// src/exulu/routes.ts
|
|
14481
14986
|
import { resolve as resolvePath } from "path";
|
|
14987
|
+
|
|
14988
|
+
// src/exulu/litellm/usage-view.ts
|
|
14989
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
14990
|
+
var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
14991
|
+
var MAX_WINDOW_DAYS = 92;
|
|
14992
|
+
var DEFAULT_WINDOW_DAYS = 30;
|
|
14993
|
+
var parseDateParam = (raw) => {
|
|
14994
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
14995
|
+
if (typeof raw !== "string") return null;
|
|
14996
|
+
if (DATE_ONLY_RE.test(raw)) return raw;
|
|
14997
|
+
const dt = new Date(raw);
|
|
14998
|
+
if (Number.isNaN(dt.getTime())) return null;
|
|
14999
|
+
return dt.toISOString().slice(0, 10);
|
|
15000
|
+
};
|
|
15001
|
+
var ymdShift = (ymd, days) => new Date(Date.parse(ymd) + days * DAY_MS).toISOString().slice(0, 10);
|
|
15002
|
+
function resolveUsageWindow(startRaw, endRaw, now = /* @__PURE__ */ new Date()) {
|
|
15003
|
+
const start = parseDateParam(startRaw);
|
|
15004
|
+
const end = parseDateParam(endRaw);
|
|
15005
|
+
if (start === null || end === null) return null;
|
|
15006
|
+
const end_date = end ?? now.toISOString().slice(0, 10);
|
|
15007
|
+
let start_date = start ?? ymdShift(end_date, -(DEFAULT_WINDOW_DAYS - 1));
|
|
15008
|
+
if (start_date > end_date) return null;
|
|
15009
|
+
const days = Math.round((Date.parse(end_date) - Date.parse(start_date)) / DAY_MS) + 1;
|
|
15010
|
+
if (days > MAX_WINDOW_DAYS) {
|
|
15011
|
+
start_date = ymdShift(end_date, -(MAX_WINDOW_DAYS - 1));
|
|
15012
|
+
}
|
|
15013
|
+
return { start_date, end_date };
|
|
15014
|
+
}
|
|
15015
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
15016
|
+
var zeroMetrics = () => ({
|
|
15017
|
+
spend: 0,
|
|
15018
|
+
prompt_tokens: 0,
|
|
15019
|
+
completion_tokens: 0,
|
|
15020
|
+
total_tokens: 0,
|
|
15021
|
+
successful_requests: 0,
|
|
15022
|
+
failed_requests: 0,
|
|
15023
|
+
api_requests: 0
|
|
15024
|
+
});
|
|
15025
|
+
var readMetrics = (source) => {
|
|
15026
|
+
const m = source?.metrics ?? {};
|
|
15027
|
+
return {
|
|
15028
|
+
spend: num(m.spend ?? source?.spend),
|
|
15029
|
+
prompt_tokens: num(m.prompt_tokens ?? source?.prompt_tokens),
|
|
15030
|
+
completion_tokens: num(m.completion_tokens ?? source?.completion_tokens),
|
|
15031
|
+
total_tokens: num(m.total_tokens ?? source?.total_tokens),
|
|
15032
|
+
successful_requests: num(
|
|
15033
|
+
m.successful_requests ?? source?.successful_requests
|
|
15034
|
+
),
|
|
15035
|
+
failed_requests: num(m.failed_requests ?? source?.failed_requests),
|
|
15036
|
+
api_requests: num(m.api_requests ?? source?.api_requests)
|
|
15037
|
+
};
|
|
15038
|
+
};
|
|
15039
|
+
var addMetrics = (into, add) => {
|
|
15040
|
+
into.spend += add.spend;
|
|
15041
|
+
into.prompt_tokens += add.prompt_tokens;
|
|
15042
|
+
into.completion_tokens += add.completion_tokens;
|
|
15043
|
+
into.total_tokens += add.total_tokens;
|
|
15044
|
+
into.successful_requests += add.successful_requests;
|
|
15045
|
+
into.failed_requests += add.failed_requests;
|
|
15046
|
+
into.api_requests += add.api_requests;
|
|
15047
|
+
};
|
|
15048
|
+
function projectMyUsage(raw) {
|
|
15049
|
+
const results = Array.isArray(raw?.results) ? raw.results : Array.isArray(raw) ? raw : [];
|
|
15050
|
+
const totals = zeroMetrics();
|
|
15051
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
15052
|
+
const byModelMap = /* @__PURE__ */ new Map();
|
|
15053
|
+
for (const result of results) {
|
|
15054
|
+
const date = typeof result?.date === "string" ? result.date : null;
|
|
15055
|
+
if (!date) continue;
|
|
15056
|
+
const metrics = readMetrics(result);
|
|
15057
|
+
addMetrics(totals, metrics);
|
|
15058
|
+
const day = byDate.get(date) ?? zeroMetrics();
|
|
15059
|
+
addMetrics(day, metrics);
|
|
15060
|
+
byDate.set(date, day);
|
|
15061
|
+
const models2 = result?.breakdown && typeof result.breakdown.models === "object" ? result.breakdown.models : {};
|
|
15062
|
+
for (const [model, entry] of Object.entries(models2 ?? {})) {
|
|
15063
|
+
const acc = byModelMap.get(model) ?? zeroMetrics();
|
|
15064
|
+
addMetrics(acc, readMetrics(entry));
|
|
15065
|
+
byModelMap.set(model, acc);
|
|
15066
|
+
}
|
|
15067
|
+
}
|
|
15068
|
+
const daily = [...byDate.entries()].map(([date, m]) => ({ date, ...m })).sort((a, b) => a.date.localeCompare(b.date));
|
|
15069
|
+
const byModel = [...byModelMap.entries()].map(([model, m]) => ({
|
|
15070
|
+
model,
|
|
15071
|
+
spend: m.spend,
|
|
15072
|
+
prompt_tokens: m.prompt_tokens,
|
|
15073
|
+
completion_tokens: m.completion_tokens,
|
|
15074
|
+
total_tokens: m.total_tokens,
|
|
15075
|
+
successful_requests: m.successful_requests,
|
|
15076
|
+
failed_requests: m.failed_requests
|
|
15077
|
+
})).sort((a, b) => b.spend - a.spend);
|
|
15078
|
+
return { totals, daily, byModel };
|
|
15079
|
+
}
|
|
15080
|
+
async function getMyUsageView(userId, window) {
|
|
15081
|
+
const settings = await getBudgetSettings();
|
|
15082
|
+
if (!settings.show_user_budget_in_chat) return null;
|
|
15083
|
+
const tag = budgetTagFor("user", userId);
|
|
15084
|
+
if (!tag) return null;
|
|
15085
|
+
const daysInRange = Math.round(
|
|
15086
|
+
(Date.parse(window.end_date) - Date.parse(window.start_date)) / DAY_MS
|
|
15087
|
+
) + 1;
|
|
15088
|
+
const raw = await getTagDailyActivity({
|
|
15089
|
+
startDate: window.start_date,
|
|
15090
|
+
endDate: window.end_date,
|
|
15091
|
+
tags: [tag],
|
|
15092
|
+
page: 1,
|
|
15093
|
+
pageSize: Math.min(daysInRange + 100, 1e4)
|
|
15094
|
+
});
|
|
15095
|
+
const { totals, daily, byModel } = projectMyUsage(raw);
|
|
15096
|
+
return {
|
|
15097
|
+
window,
|
|
15098
|
+
display: settings.user_budget_display,
|
|
15099
|
+
totals,
|
|
15100
|
+
daily,
|
|
15101
|
+
byModel
|
|
15102
|
+
};
|
|
15103
|
+
}
|
|
15104
|
+
|
|
15105
|
+
// src/exulu/routes.ts
|
|
14482
15106
|
import multer from "multer";
|
|
14483
15107
|
import Busboy from "busboy";
|
|
14484
15108
|
|
|
@@ -15188,7 +15812,7 @@ var getEnabledSkills = async (agent, disabledSkills = []) => {
|
|
|
15188
15812
|
return enabledSkills;
|
|
15189
15813
|
};
|
|
15190
15814
|
|
|
15191
|
-
// src/exulu/
|
|
15815
|
+
// src/exulu/auth/callback-handler.ts
|
|
15192
15816
|
var escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15193
15817
|
var renderResultPage = ({ success, message }) => `<!doctype html>
|
|
15194
15818
|
<html lang="en">
|
|
@@ -15240,7 +15864,7 @@ var handleOauthCallback = async (req, res) => {
|
|
|
15240
15864
|
"This authorization link is invalid or has expired. Return to your chat and run the tool again to get a fresh link."
|
|
15241
15865
|
);
|
|
15242
15866
|
}
|
|
15243
|
-
const config =
|
|
15867
|
+
const config = authRegistry.getByProvider(parsed.provider);
|
|
15244
15868
|
if (!config) {
|
|
15245
15869
|
return send(
|
|
15246
15870
|
404,
|
|
@@ -15248,13 +15872,31 @@ var handleOauthCallback = async (req, res) => {
|
|
|
15248
15872
|
`No OAuth configuration is registered for provider "${parsed.provider}".`
|
|
15249
15873
|
);
|
|
15250
15874
|
}
|
|
15875
|
+
if (config.authType !== "oauth") {
|
|
15876
|
+
return send(
|
|
15877
|
+
500,
|
|
15878
|
+
false,
|
|
15879
|
+
`Provider "${parsed.provider}" is not configured for OAuth. This callback only handles OAuth flows.`
|
|
15880
|
+
);
|
|
15881
|
+
}
|
|
15251
15882
|
try {
|
|
15252
15883
|
const record = await exchangeCodeForTokens({
|
|
15253
15884
|
config,
|
|
15254
15885
|
code,
|
|
15255
15886
|
codeVerifier: parsed.codeVerifier
|
|
15256
15887
|
});
|
|
15257
|
-
await
|
|
15888
|
+
await credentialStore.upsert({
|
|
15889
|
+
provider: parsed.provider,
|
|
15890
|
+
userId: parsed.userId,
|
|
15891
|
+
authType: "oauth",
|
|
15892
|
+
data: {
|
|
15893
|
+
accessToken: record.accessToken,
|
|
15894
|
+
refreshToken: record.refreshToken ?? null,
|
|
15895
|
+
tokenType: record.tokenType ?? null,
|
|
15896
|
+
scopes: record.scopes ?? null,
|
|
15897
|
+
expiresAt: record.expiresAt ? record.expiresAt.toISOString() : null
|
|
15898
|
+
}
|
|
15899
|
+
});
|
|
15258
15900
|
} catch (caught) {
|
|
15259
15901
|
console.error("[EXULU] OAuth code exchange failed:", caught);
|
|
15260
15902
|
return send(
|
|
@@ -15266,6 +15908,107 @@ var handleOauthCallback = async (req, res) => {
|
|
|
15266
15908
|
return send(200, true, "You can close this tab and return to your chat.");
|
|
15267
15909
|
};
|
|
15268
15910
|
|
|
15911
|
+
// src/exulu/auth/submit-handler.ts
|
|
15912
|
+
import { z as z4 } from "zod";
|
|
15913
|
+
var bodySchema = z4.object({
|
|
15914
|
+
nonce: z4.string().min(1),
|
|
15915
|
+
values: z4.record(z4.string(), z4.string())
|
|
15916
|
+
});
|
|
15917
|
+
async function handleCredentialSubmit(req, res) {
|
|
15918
|
+
const authResult = await requestValidators.authenticate(req);
|
|
15919
|
+
if (!authResult.user?.id) {
|
|
15920
|
+
res.status(401).json({ ok: false, error: "authentication required" });
|
|
15921
|
+
return;
|
|
15922
|
+
}
|
|
15923
|
+
const sessionUserId = authResult.user.id;
|
|
15924
|
+
let parsed;
|
|
15925
|
+
try {
|
|
15926
|
+
parsed = bodySchema.parse(req.body);
|
|
15927
|
+
} catch (caught) {
|
|
15928
|
+
res.status(400).json({ ok: false, error: "invalid body" });
|
|
15929
|
+
return;
|
|
15930
|
+
}
|
|
15931
|
+
const { nonce, values } = parsed;
|
|
15932
|
+
let nonceData;
|
|
15933
|
+
try {
|
|
15934
|
+
nonceData = verifyCredentialNonce(nonce);
|
|
15935
|
+
} catch (e) {
|
|
15936
|
+
res.status(401).json({
|
|
15937
|
+
ok: false,
|
|
15938
|
+
error: /expired/i.test(e.message) ? "nonce expired" : "nonce invalid"
|
|
15939
|
+
});
|
|
15940
|
+
return;
|
|
15941
|
+
}
|
|
15942
|
+
if (sessionUserId !== Number(nonceData.userId)) {
|
|
15943
|
+
res.status(403).json({ ok: false, error: "userId mismatch" });
|
|
15944
|
+
return;
|
|
15945
|
+
}
|
|
15946
|
+
const config = authRegistry.getByProvider(nonceData.provider);
|
|
15947
|
+
if (!config || config.authType !== "user_credentials") {
|
|
15948
|
+
res.status(400).json({
|
|
15949
|
+
ok: false,
|
|
15950
|
+
error: "provider is not a user_credentials provider"
|
|
15951
|
+
});
|
|
15952
|
+
return;
|
|
15953
|
+
}
|
|
15954
|
+
const expectedFields = new Set(config.fields.map((f) => f.name));
|
|
15955
|
+
const submittedFields = new Set(Object.keys(values));
|
|
15956
|
+
if (expectedFields.size !== submittedFields.size || [...expectedFields].some((f) => !submittedFields.has(f))) {
|
|
15957
|
+
res.status(400).json({
|
|
15958
|
+
ok: false,
|
|
15959
|
+
error: "field set mismatch"
|
|
15960
|
+
});
|
|
15961
|
+
return;
|
|
15962
|
+
}
|
|
15963
|
+
if (config.validate) {
|
|
15964
|
+
try {
|
|
15965
|
+
await config.validate(values);
|
|
15966
|
+
} catch (e) {
|
|
15967
|
+
res.status(400).json({
|
|
15968
|
+
ok: false,
|
|
15969
|
+
error: `validation failed: ${e.message}`
|
|
15970
|
+
});
|
|
15971
|
+
return;
|
|
15972
|
+
}
|
|
15973
|
+
}
|
|
15974
|
+
if (!Number.isInteger(sessionUserId) || sessionUserId <= 0) {
|
|
15975
|
+
res.status(401).json({ ok: false, error: "session invalid" });
|
|
15976
|
+
return;
|
|
15977
|
+
}
|
|
15978
|
+
await credentialStore.upsert({
|
|
15979
|
+
provider: nonceData.provider,
|
|
15980
|
+
userId: sessionUserId,
|
|
15981
|
+
authType: "user_credentials",
|
|
15982
|
+
data: values
|
|
15983
|
+
});
|
|
15984
|
+
res.status(200).json({ ok: true });
|
|
15985
|
+
}
|
|
15986
|
+
|
|
15987
|
+
// src/exulu/auth/manage-handlers.ts
|
|
15988
|
+
var handleCredentialList = async (req, res) => {
|
|
15989
|
+
const authResult = await requestValidators.authenticate(req);
|
|
15990
|
+
if (!authResult.user?.id) {
|
|
15991
|
+
res.status(authResult.code ?? 401).json({ ok: false, error: "authentication required" });
|
|
15992
|
+
return;
|
|
15993
|
+
}
|
|
15994
|
+
const credentials = await credentialStore.listByUser(authResult.user.id);
|
|
15995
|
+
res.status(200).json({ ok: true, credentials });
|
|
15996
|
+
};
|
|
15997
|
+
var handleCredentialDelete = async (req, res) => {
|
|
15998
|
+
const authResult = await requestValidators.authenticate(req);
|
|
15999
|
+
if (!authResult.user?.id) {
|
|
16000
|
+
res.status(authResult.code ?? 401).json({ ok: false, error: "authentication required" });
|
|
16001
|
+
return;
|
|
16002
|
+
}
|
|
16003
|
+
const provider = req.params.provider;
|
|
16004
|
+
if (!provider) {
|
|
16005
|
+
res.status(400).json({ ok: false, error: "provider is required" });
|
|
16006
|
+
return;
|
|
16007
|
+
}
|
|
16008
|
+
await credentialStore.delete(provider, authResult.user.id);
|
|
16009
|
+
res.status(200).json({ ok: true });
|
|
16010
|
+
};
|
|
16011
|
+
|
|
15269
16012
|
// src/exulu/recall/verify.ts
|
|
15270
16013
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
15271
16014
|
var TOLERANCE_SECONDS = 5 * 60;
|
|
@@ -15315,51 +16058,123 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
|
|
|
15315
16058
|
return passed ? { ok: true } : { ok: false, reason: "signature mismatch" };
|
|
15316
16059
|
};
|
|
15317
16060
|
|
|
15318
|
-
// src/exulu/
|
|
15319
|
-
|
|
15320
|
-
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15328
|
-
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
|
|
15335
|
-
|
|
15336
|
-
|
|
15337
|
-
|
|
15338
|
-
|
|
15339
|
-
|
|
15340
|
-
|
|
15341
|
-
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
}
|
|
15348
|
-
|
|
15349
|
-
|
|
16061
|
+
// src/exulu/public-agents.ts
|
|
16062
|
+
var publicAgentView = (row, slug) => ({
|
|
16063
|
+
id: row.id,
|
|
16064
|
+
name: row.name ?? "",
|
|
16065
|
+
description: row.description ?? "",
|
|
16066
|
+
image: row.image ?? null,
|
|
16067
|
+
welcomemessage: row.welcomemessage ?? "",
|
|
16068
|
+
slug,
|
|
16069
|
+
guest_auth_mode: row.guest_auth_mode || "regular",
|
|
16070
|
+
guest_has_cover: !!row.guest_cover_image
|
|
16071
|
+
});
|
|
16072
|
+
var evaluateGuestChatAccess = async (agent, userId, guestPassword) => {
|
|
16073
|
+
if (agent.guest_access) {
|
|
16074
|
+
if (userId != null) return { allowed: true, via: "guest" };
|
|
16075
|
+
const mode = agent.guest_auth_mode || "regular";
|
|
16076
|
+
if (mode === "public") return { allowed: true, via: "guest" };
|
|
16077
|
+
if (mode === "password") {
|
|
16078
|
+
if (agent.guest_password_hash && guestPassword) {
|
|
16079
|
+
const ok = await verifySharePassword(guestPassword, agent.guest_password_hash);
|
|
16080
|
+
if (ok) return { allowed: true, via: "guest" };
|
|
16081
|
+
return { allowed: false, status: 401, message: "Incorrect password." };
|
|
16082
|
+
}
|
|
16083
|
+
return { allowed: false, status: 401, message: "Password required." };
|
|
16084
|
+
}
|
|
16085
|
+
return { allowed: false, status: 401, message: "Authentication required." };
|
|
16086
|
+
}
|
|
16087
|
+
if (userId == null && agent.rights_mode === "public") {
|
|
16088
|
+
return { allowed: true, via: "rbac-public" };
|
|
16089
|
+
}
|
|
16090
|
+
return { allowed: false, status: 401, message: "Authentication required." };
|
|
16091
|
+
};
|
|
16092
|
+
|
|
16093
|
+
// src/exulu/guest-rate-limit.ts
|
|
16094
|
+
var MINUTE_MS = 6e4;
|
|
16095
|
+
var HOUR_MS2 = 36e5;
|
|
16096
|
+
var perMinuteLimit = () => parseInt(process.env.EXULU_GUEST_RATE_PER_MINUTE || "10", 10);
|
|
16097
|
+
var perHourLimit = () => parseInt(process.env.EXULU_GUEST_RATE_PER_HOUR || "60", 10);
|
|
16098
|
+
var maxMessageChars = () => parseInt(process.env.EXULU_GUEST_MAX_MESSAGE_CHARS || "8000", 10);
|
|
16099
|
+
var maxTotalChars = () => parseInt(process.env.EXULU_GUEST_MAX_TOTAL_CHARS || "32000", 10);
|
|
16100
|
+
var MAX_PART_COUNT = 100;
|
|
16101
|
+
var windows = /* @__PURE__ */ new Map();
|
|
16102
|
+
var guestRateLimitExceeded = (ip, now = Date.now()) => {
|
|
16103
|
+
const state = windows.get(ip) ?? {
|
|
16104
|
+
minuteStart: now,
|
|
16105
|
+
minuteCount: 0,
|
|
16106
|
+
hourStart: now,
|
|
16107
|
+
hourCount: 0,
|
|
16108
|
+
lastSeen: now
|
|
16109
|
+
};
|
|
16110
|
+
if (now - state.minuteStart >= MINUTE_MS) {
|
|
16111
|
+
state.minuteStart = now;
|
|
16112
|
+
state.minuteCount = 0;
|
|
16113
|
+
}
|
|
16114
|
+
if (now - state.hourStart >= HOUR_MS2) {
|
|
16115
|
+
state.hourStart = now;
|
|
16116
|
+
state.hourCount = 0;
|
|
16117
|
+
}
|
|
16118
|
+
state.minuteCount += 1;
|
|
16119
|
+
state.hourCount += 1;
|
|
16120
|
+
state.lastSeen = now;
|
|
16121
|
+
windows.set(ip, state);
|
|
16122
|
+
if (windows.size > 1e4) {
|
|
16123
|
+
for (const [key, value] of windows) {
|
|
16124
|
+
if (now - value.lastSeen >= HOUR_MS2) windows.delete(key);
|
|
16125
|
+
}
|
|
16126
|
+
if (windows.size > 1e4) {
|
|
16127
|
+
const sorted = [...windows.entries()].sort(
|
|
16128
|
+
(a, b) => a[1].lastSeen - b[1].lastSeen
|
|
16129
|
+
);
|
|
16130
|
+
for (const [key] of sorted) {
|
|
16131
|
+
if (windows.size <= 1e4) break;
|
|
16132
|
+
windows.delete(key);
|
|
16133
|
+
}
|
|
16134
|
+
}
|
|
15350
16135
|
}
|
|
15351
|
-
return
|
|
16136
|
+
return state.minuteCount > perMinuteLimit() || state.hourCount > perHourLimit();
|
|
15352
16137
|
};
|
|
15353
|
-
var
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
16138
|
+
var partsTooLong = (parts) => Array.isArray(parts) && parts.some(
|
|
16139
|
+
(p) => typeof p?.text === "string" && p.text.length > maxMessageChars()
|
|
16140
|
+
);
|
|
16141
|
+
var collectTextParts = (b) => {
|
|
16142
|
+
const parts = [];
|
|
16143
|
+
const addFromParts = (ps) => {
|
|
16144
|
+
if (!Array.isArray(ps)) return;
|
|
16145
|
+
for (const p of ps) {
|
|
16146
|
+
if (typeof p?.text === "string") parts.push(p.text);
|
|
16147
|
+
}
|
|
15360
16148
|
};
|
|
16149
|
+
if (b.message) addFromParts(b.message.parts);
|
|
16150
|
+
if (Array.isArray(b.messages)) {
|
|
16151
|
+
for (const m of b.messages) addFromParts(m?.parts);
|
|
16152
|
+
}
|
|
16153
|
+
return parts;
|
|
16154
|
+
};
|
|
16155
|
+
var guestMessageTooLong = (body) => {
|
|
16156
|
+
const b = body;
|
|
16157
|
+
if (!b) return false;
|
|
16158
|
+
if (b.message && partsTooLong(b.message.parts)) return true;
|
|
16159
|
+
if (Array.isArray(b.messages)) {
|
|
16160
|
+
if (b.messages.some((m) => partsTooLong(m?.parts))) return true;
|
|
16161
|
+
}
|
|
16162
|
+
const allParts = collectTextParts(b);
|
|
16163
|
+
if (allParts.length > MAX_PART_COUNT) return true;
|
|
16164
|
+
const totalChars = allParts.reduce((sum, t) => sum + t.length, 0);
|
|
16165
|
+
if (totalChars > maxTotalChars()) return true;
|
|
16166
|
+
return false;
|
|
16167
|
+
};
|
|
16168
|
+
var extractClientIp = (req) => {
|
|
16169
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
16170
|
+
if (process.env.EXULU_TRUST_PROXY === "true") {
|
|
16171
|
+
if (typeof forwarded === "string" && forwarded.length > 0) {
|
|
16172
|
+
const parts = forwarded.split(",");
|
|
16173
|
+
return parts[parts.length - 1].trim();
|
|
16174
|
+
}
|
|
16175
|
+
}
|
|
16176
|
+
return req.ip || req.socket?.remoteAddress || "unknown";
|
|
15361
16177
|
};
|
|
15362
|
-
var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
|
|
15363
16178
|
|
|
15364
16179
|
// src/skills/skill-access.ts
|
|
15365
16180
|
async function resolveSkillByName(db, name) {
|
|
@@ -15717,6 +16532,9 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
|
|
|
15717
16532
|
})
|
|
15718
16533
|
);
|
|
15719
16534
|
app.get(OAUTH_CALLBACK_PATH, handleOauthCallback);
|
|
16535
|
+
app.post("/credentials/submit", handleCredentialSubmit);
|
|
16536
|
+
app.get("/credentials", handleCredentialList);
|
|
16537
|
+
app.delete("/credentials/:provider", handleCredentialDelete);
|
|
15720
16538
|
app.post("/test", async (req, res) => {
|
|
15721
16539
|
const { item_name, context_id } = req.body;
|
|
15722
16540
|
let itemFilters = [];
|
|
@@ -16060,17 +16878,33 @@ Mood: friendly and intelligent.
|
|
|
16060
16878
|
}
|
|
16061
16879
|
console.log("[EXULU] agent.rights_mode", agent.rights_mode);
|
|
16062
16880
|
const authenticationResult = await requestValidators.authenticate(req);
|
|
16063
|
-
|
|
16064
|
-
|
|
16881
|
+
const user = authenticationResult.user;
|
|
16882
|
+
if (!user?.id) {
|
|
16883
|
+
const ip = extractClientIp(req);
|
|
16884
|
+
if (guestRateLimitExceeded(ip)) {
|
|
16885
|
+
res.status(429).json({ detail: "Too many requests. Try again later." });
|
|
16886
|
+
return;
|
|
16887
|
+
}
|
|
16888
|
+
if (guestMessageTooLong(req.body)) {
|
|
16889
|
+
res.status(413).json({ detail: "Message too long." });
|
|
16890
|
+
return;
|
|
16891
|
+
}
|
|
16892
|
+
}
|
|
16893
|
+
const guestGate = await evaluateGuestChatAccess(
|
|
16894
|
+
agent,
|
|
16895
|
+
user?.id,
|
|
16896
|
+
req.headers["x-guest-password"]
|
|
16897
|
+
);
|
|
16898
|
+
if (!user?.id && !guestGate.allowed) {
|
|
16899
|
+
res.status(guestGate.status).json({ detail: guestGate.message });
|
|
16065
16900
|
return;
|
|
16066
16901
|
}
|
|
16067
|
-
const user = authenticationResult.user;
|
|
16068
16902
|
const scopeCheck = checkApiKeyScope(user, instance);
|
|
16069
16903
|
if (!scopeCheck.allowed) {
|
|
16070
16904
|
res.status(scopeCheck.code).json({ detail: scopeCheck.reason });
|
|
16071
16905
|
return;
|
|
16072
16906
|
}
|
|
16073
|
-
const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
|
|
16907
|
+
const hasAccessToAgent = guestGate.allowed || await checkRecordAccess(agent, "read", user);
|
|
16074
16908
|
if (!hasAccessToAgent) {
|
|
16075
16909
|
res.status(401).json({
|
|
16076
16910
|
message: "You don't have access to this agent."
|
|
@@ -17428,6 +18262,32 @@ ${style.markdown}` : params.prompt;
|
|
|
17428
18262
|
}
|
|
17429
18263
|
res.status(200).json({ budget: await getUserBudgetView(authResult.user.id) });
|
|
17430
18264
|
});
|
|
18265
|
+
app.get("/me/usage", async (req, res) => {
|
|
18266
|
+
const authResult = await requestValidators.authenticate(req);
|
|
18267
|
+
if (!authResult.user?.id) {
|
|
18268
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
18269
|
+
return;
|
|
18270
|
+
}
|
|
18271
|
+
const window = resolveUsageWindow(req.query.start_date, req.query.end_date);
|
|
18272
|
+
if (!window) {
|
|
18273
|
+
res.status(400).json({
|
|
18274
|
+
detail: "start_date and end_date must be YYYY-MM-DD or ISO datetimes, with start_date <= end_date."
|
|
18275
|
+
});
|
|
18276
|
+
return;
|
|
18277
|
+
}
|
|
18278
|
+
try {
|
|
18279
|
+
res.status(200).json({ usage: await getMyUsageView(authResult.user.id, window) });
|
|
18280
|
+
} catch (err) {
|
|
18281
|
+
if (err instanceof LiteLLMAdminError) {
|
|
18282
|
+
res.status(502).json({ detail: err.message });
|
|
18283
|
+
return;
|
|
18284
|
+
}
|
|
18285
|
+
console.error("[EXULU] /me/usage failed", err);
|
|
18286
|
+
res.status(500).json({
|
|
18287
|
+
detail: err instanceof Error ? err.message : "Usage query failed."
|
|
18288
|
+
});
|
|
18289
|
+
}
|
|
18290
|
+
});
|
|
17431
18291
|
app.put(
|
|
17432
18292
|
"/admin/budgets/:entityType/bulk",
|
|
17433
18293
|
async (req, res) => {
|
|
@@ -17541,10 +18401,10 @@ ${style.markdown}` : params.prompt;
|
|
|
17541
18401
|
}
|
|
17542
18402
|
return { user: authResult.user };
|
|
17543
18403
|
};
|
|
17544
|
-
const
|
|
18404
|
+
const DATE_ONLY_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
17545
18405
|
const normaliseDateParam = (raw) => {
|
|
17546
18406
|
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
17547
|
-
if (
|
|
18407
|
+
if (DATE_ONLY_RE2.test(raw)) return raw;
|
|
17548
18408
|
const dt = new Date(raw);
|
|
17549
18409
|
if (Number.isNaN(dt.getTime())) return null;
|
|
17550
18410
|
return dt.toISOString().slice(0, 10);
|
|
@@ -19141,6 +20001,93 @@ ${style.markdown}` : params.prompt;
|
|
|
19141
20001
|
if (headers.disposition) res.setHeader("Content-Disposition", headers.disposition);
|
|
19142
20002
|
res.send(bytes);
|
|
19143
20003
|
});
|
|
20004
|
+
const resolvePublicAgentSlug = async (agentModel) => {
|
|
20005
|
+
if (isLiteLLMEnabled()) return "/agents/litellm/run";
|
|
20006
|
+
if (!agentModel) return "";
|
|
20007
|
+
const { db } = await postgresClient();
|
|
20008
|
+
const modelRow = await db.from("models").where({ id: agentModel }).first();
|
|
20009
|
+
const provider = modelRow?.provider ? providers.find((a) => a.id === modelRow.provider) : void 0;
|
|
20010
|
+
return provider?.slug || "";
|
|
20011
|
+
};
|
|
20012
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20013
|
+
const getGuestAgentById = async (id) => {
|
|
20014
|
+
if (!UUID_RE.test(id)) return void 0;
|
|
20015
|
+
const { db } = await postgresClient();
|
|
20016
|
+
return db.from("agents").where({ id, guest_access: true, active: true }).first();
|
|
20017
|
+
};
|
|
20018
|
+
app.get("/public-agents", async (_req, res) => {
|
|
20019
|
+
const { db } = await postgresClient();
|
|
20020
|
+
const rows = await db.from("agents").where({ guest_access: true, active: true }).select(
|
|
20021
|
+
"id",
|
|
20022
|
+
"name",
|
|
20023
|
+
"description",
|
|
20024
|
+
"image",
|
|
20025
|
+
"welcomemessage",
|
|
20026
|
+
"model",
|
|
20027
|
+
"guest_auth_mode",
|
|
20028
|
+
"guest_cover_image"
|
|
20029
|
+
);
|
|
20030
|
+
const views = await Promise.all(
|
|
20031
|
+
rows.map(
|
|
20032
|
+
async (row) => publicAgentView(row, await resolvePublicAgentSlug(row.model))
|
|
20033
|
+
)
|
|
20034
|
+
);
|
|
20035
|
+
res.json(views);
|
|
20036
|
+
});
|
|
20037
|
+
app.get("/public-agents/:id/meta", async (req, res) => {
|
|
20038
|
+
const row = await getGuestAgentById(req.params.id ?? "");
|
|
20039
|
+
if (!row) {
|
|
20040
|
+
res.status(404).json({ detail: "Not found." });
|
|
20041
|
+
return;
|
|
20042
|
+
}
|
|
20043
|
+
res.json(publicAgentView(row, await resolvePublicAgentSlug(row.model)));
|
|
20044
|
+
});
|
|
20045
|
+
app.get("/public-agents/:id/cover", async (req, res) => {
|
|
20046
|
+
const row = await getGuestAgentById(req.params.id ?? "");
|
|
20047
|
+
if (!row?.guest_cover_image) {
|
|
20048
|
+
res.status(404).json({ detail: "Not found." });
|
|
20049
|
+
return;
|
|
20050
|
+
}
|
|
20051
|
+
let bytes;
|
|
20052
|
+
try {
|
|
20053
|
+
bytes = await getS3ObjectBytes(row.guest_cover_image, config);
|
|
20054
|
+
} catch (e) {
|
|
20055
|
+
if (e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.$metadata?.httpStatusCode === 404) {
|
|
20056
|
+
res.status(404).json({ detail: "Cover not found." });
|
|
20057
|
+
return;
|
|
20058
|
+
}
|
|
20059
|
+
console.error("[EXULU] public-agent cover read failed", e);
|
|
20060
|
+
res.status(500).json({ detail: "Failed to read cover." });
|
|
20061
|
+
return;
|
|
20062
|
+
}
|
|
20063
|
+
const ext = row.guest_cover_image.split(".").pop()?.toLowerCase();
|
|
20064
|
+
const contentType = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
20065
|
+
res.setHeader("Content-Type", contentType);
|
|
20066
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
20067
|
+
res.setHeader("Cache-Control", "public, max-age=300");
|
|
20068
|
+
res.send(bytes);
|
|
20069
|
+
});
|
|
20070
|
+
app.post(
|
|
20071
|
+
"/public-agents/:id/verify-password",
|
|
20072
|
+
async (req, res) => {
|
|
20073
|
+
const ip = extractClientIp(req);
|
|
20074
|
+
if (guestRateLimitExceeded(ip)) {
|
|
20075
|
+
res.status(429).json({ detail: "Too many requests. Try again later." });
|
|
20076
|
+
return;
|
|
20077
|
+
}
|
|
20078
|
+
const row = await getGuestAgentById(req.params.id ?? "");
|
|
20079
|
+
if (!row || row.guest_auth_mode !== "password") {
|
|
20080
|
+
res.status(404).json({ detail: "Not found." });
|
|
20081
|
+
return;
|
|
20082
|
+
}
|
|
20083
|
+
const password = typeof req.body?.password === "string" ? req.body.password : "";
|
|
20084
|
+
if (!row.guest_password_hash || !await verifySharePassword(password, row.guest_password_hash)) {
|
|
20085
|
+
res.status(401).json({ detail: "Incorrect password." });
|
|
20086
|
+
return;
|
|
20087
|
+
}
|
|
20088
|
+
res.status(204).end();
|
|
20089
|
+
}
|
|
20090
|
+
);
|
|
19144
20091
|
app.use(express2.static("public"));
|
|
19145
20092
|
await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config);
|
|
19146
20093
|
return app;
|
|
@@ -19213,7 +20160,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
19213
20160
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
19214
20161
|
import "express";
|
|
19215
20162
|
import "@opentelemetry/api";
|
|
19216
|
-
import { z as
|
|
20163
|
+
import { z as z5 } from "zod";
|
|
19217
20164
|
var SESSION_ID_HEADER = "mcp-session-id";
|
|
19218
20165
|
var ExuluMCP = class {
|
|
19219
20166
|
server = {};
|
|
@@ -19285,7 +20232,7 @@ var ExuluMCP = class {
|
|
|
19285
20232
|
title: tool2.name + " agent",
|
|
19286
20233
|
description: tool2.description,
|
|
19287
20234
|
inputSchema: {
|
|
19288
|
-
inputs: tool2.inputSchema ||
|
|
20235
|
+
inputs: tool2.inputSchema || z5.object({})
|
|
19289
20236
|
}
|
|
19290
20237
|
},
|
|
19291
20238
|
async ({ inputs }, args) => {
|
|
@@ -19337,7 +20284,7 @@ var ExuluMCP = class {
|
|
|
19337
20284
|
title: "Get List of Prompt Templates",
|
|
19338
20285
|
description: "Retrieves a list of prompt templates available for this agent. Returns the name, description, and ID of each template.",
|
|
19339
20286
|
inputSchema: {
|
|
19340
|
-
inputs:
|
|
20287
|
+
inputs: z5.object({})
|
|
19341
20288
|
}
|
|
19342
20289
|
},
|
|
19343
20290
|
async ({ inputs }, args) => {
|
|
@@ -19383,8 +20330,8 @@ var ExuluMCP = class {
|
|
|
19383
20330
|
title: "Get Prompt Template Details",
|
|
19384
20331
|
description: "Retrieves the full details of a specific prompt template by ID, including the actual template content with variables.",
|
|
19385
20332
|
inputSchema: {
|
|
19386
|
-
inputs:
|
|
19387
|
-
id:
|
|
20333
|
+
inputs: z5.object({
|
|
20334
|
+
id: z5.string().describe("The ID of the prompt template to retrieve")
|
|
19388
20335
|
})
|
|
19389
20336
|
}
|
|
19390
20337
|
},
|
|
@@ -20485,7 +21432,7 @@ var ExuluEval = class {
|
|
|
20485
21432
|
};
|
|
20486
21433
|
|
|
20487
21434
|
// src/templates/evals/index.ts
|
|
20488
|
-
import { z as
|
|
21435
|
+
import { z as z6 } from "zod";
|
|
20489
21436
|
import { generateText as generateText8, Output as Output2 } from "ai";
|
|
20490
21437
|
var llmAsJudgeEval = () => {
|
|
20491
21438
|
if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
|
|
@@ -20538,8 +21485,8 @@ var llmAsJudgeEval = () => {
|
|
|
20538
21485
|
prompt,
|
|
20539
21486
|
maxRetries: 2,
|
|
20540
21487
|
output: Output2.object({
|
|
20541
|
-
schema:
|
|
20542
|
-
score:
|
|
21488
|
+
schema: z6.object({
|
|
21489
|
+
score: z6.number().min(0).max(100).describe("The score between 0 and 100.")
|
|
20543
21490
|
})
|
|
20544
21491
|
})
|
|
20545
21492
|
});
|
|
@@ -20767,12 +21714,12 @@ Usage:
|
|
|
20767
21714
|
- If no todos exist yet, an empty list will be returned`;
|
|
20768
21715
|
|
|
20769
21716
|
// src/templates/tools/todo/todo.ts
|
|
20770
|
-
import
|
|
20771
|
-
var TodoSchema =
|
|
20772
|
-
content:
|
|
20773
|
-
status:
|
|
20774
|
-
priority:
|
|
20775
|
-
id:
|
|
21717
|
+
import z7 from "zod";
|
|
21718
|
+
var TodoSchema = z7.object({
|
|
21719
|
+
content: z7.string().describe("Brief description of the task"),
|
|
21720
|
+
status: z7.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
|
|
21721
|
+
priority: z7.string().describe("Priority level of the task: high, medium, low"),
|
|
21722
|
+
id: z7.string().describe("Unique identifier for the todo item")
|
|
20776
21723
|
});
|
|
20777
21724
|
var TodoWriteTool = new ExuluTool({
|
|
20778
21725
|
id: "todo_write",
|
|
@@ -20788,8 +21735,8 @@ var TodoWriteTool = new ExuluTool({
|
|
|
20788
21735
|
default: todowrite_default
|
|
20789
21736
|
}
|
|
20790
21737
|
],
|
|
20791
|
-
inputSchema:
|
|
20792
|
-
todos:
|
|
21738
|
+
inputSchema: z7.object({
|
|
21739
|
+
todos: z7.array(TodoSchema).describe("The updated todo list")
|
|
20793
21740
|
}),
|
|
20794
21741
|
execute: async (inputs) => {
|
|
20795
21742
|
const { sessionID, todos, user } = inputs;
|
|
@@ -20824,7 +21771,7 @@ var TodoReadTool = new ExuluTool({
|
|
|
20824
21771
|
id: "todo_read",
|
|
20825
21772
|
name: "Todo Read",
|
|
20826
21773
|
description: "Use this tool to read your todo list",
|
|
20827
|
-
inputSchema:
|
|
21774
|
+
inputSchema: z7.object({}),
|
|
20828
21775
|
type: "function",
|
|
20829
21776
|
category: "todo",
|
|
20830
21777
|
config: [
|
|
@@ -20866,7 +21813,7 @@ var todoTools = [TodoWriteTool, TodoReadTool];
|
|
|
20866
21813
|
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';
|
|
20867
21814
|
|
|
20868
21815
|
// src/templates/tools/question/question.ts
|
|
20869
|
-
import
|
|
21816
|
+
import z9 from "zod";
|
|
20870
21817
|
|
|
20871
21818
|
// src/templates/tools/question/questionask.txt
|
|
20872
21819
|
var questionask_default = `Use this tool to ask the user a question with multiple choice answer options during your session. This helps you gather user input, clarify requirements, and make informed decisions based on user preferences.
|
|
@@ -20953,18 +21900,18 @@ After asking a question, use the Question Read tool to check if the user has ans
|
|
|
20953
21900
|
`;
|
|
20954
21901
|
|
|
20955
21902
|
// src/templates/tools/question/question-ask.ts
|
|
20956
|
-
import
|
|
21903
|
+
import z8 from "zod";
|
|
20957
21904
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
20958
|
-
var AnswerOptionSchema =
|
|
20959
|
-
id:
|
|
20960
|
-
text:
|
|
21905
|
+
var AnswerOptionSchema = z8.object({
|
|
21906
|
+
id: z8.string().describe("Unique identifier for the answer option"),
|
|
21907
|
+
text: z8.string().describe("The text of the answer option")
|
|
20961
21908
|
});
|
|
20962
|
-
var _QuestionSchema =
|
|
20963
|
-
id:
|
|
20964
|
-
question:
|
|
20965
|
-
answerOptions:
|
|
20966
|
-
selectedAnswerId:
|
|
20967
|
-
status:
|
|
21909
|
+
var _QuestionSchema = z8.object({
|
|
21910
|
+
id: z8.string().describe("Unique identifier for the question"),
|
|
21911
|
+
question: z8.string().describe("The question to ask the user"),
|
|
21912
|
+
answerOptions: z8.array(AnswerOptionSchema).describe("Array of possible answer options"),
|
|
21913
|
+
selectedAnswerId: z8.string().optional().describe("The ID of the answer option selected by the user"),
|
|
21914
|
+
status: z8.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
|
|
20968
21915
|
});
|
|
20969
21916
|
var QuestionAskTool = new ExuluTool({
|
|
20970
21917
|
id: "question_ask",
|
|
@@ -20981,9 +21928,9 @@ var QuestionAskTool = new ExuluTool({
|
|
|
20981
21928
|
default: questionask_default
|
|
20982
21929
|
}
|
|
20983
21930
|
],
|
|
20984
|
-
inputSchema:
|
|
20985
|
-
question:
|
|
20986
|
-
answerOptions:
|
|
21931
|
+
inputSchema: z8.object({
|
|
21932
|
+
question: z8.string().describe("The question to ask the user"),
|
|
21933
|
+
answerOptions: z8.array(z8.string()).describe("Array of possible answer options (strings)")
|
|
20987
21934
|
}),
|
|
20988
21935
|
execute: async (inputs) => {
|
|
20989
21936
|
const { sessionID, question, answerOptions, user } = inputs;
|
|
@@ -21056,7 +22003,7 @@ var QuestionReadTool = new ExuluTool({
|
|
|
21056
22003
|
name: "Question Read",
|
|
21057
22004
|
needsApproval: false,
|
|
21058
22005
|
description: "Use this tool to read questions and their answers",
|
|
21059
|
-
inputSchema:
|
|
22006
|
+
inputSchema: z9.object({}),
|
|
21060
22007
|
type: "function",
|
|
21061
22008
|
category: "question",
|
|
21062
22009
|
config: [
|
|
@@ -21086,15 +22033,15 @@ async function getQuestions(sessionID) {
|
|
|
21086
22033
|
var questionTools = [QuestionAskTool, QuestionReadTool];
|
|
21087
22034
|
|
|
21088
22035
|
// src/templates/tools/perplexity.ts
|
|
21089
|
-
import
|
|
22036
|
+
import z10 from "zod";
|
|
21090
22037
|
import Perplexity from "@perplexity-ai/perplexity_ai";
|
|
21091
22038
|
var internetSearchTool = new ExuluTool({
|
|
21092
22039
|
id: "internet_search",
|
|
21093
22040
|
name: "Internet Search",
|
|
21094
22041
|
description: "Search the internet for information.",
|
|
21095
|
-
inputSchema:
|
|
21096
|
-
query:
|
|
21097
|
-
search_recency_filter:
|
|
22042
|
+
inputSchema: z10.object({
|
|
22043
|
+
query: z10.string().describe("The query to the tool."),
|
|
22044
|
+
search_recency_filter: z10.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
|
|
21098
22045
|
}),
|
|
21099
22046
|
category: "internet_search",
|
|
21100
22047
|
type: "web_search",
|
|
@@ -21187,7 +22134,7 @@ var perplexityTools = [internetSearchTool];
|
|
|
21187
22134
|
|
|
21188
22135
|
// src/templates/tools/email.ts
|
|
21189
22136
|
import * as nodemailer from "nodemailer";
|
|
21190
|
-
import { z as
|
|
22137
|
+
import { z as z11 } from "zod";
|
|
21191
22138
|
var transporter = null;
|
|
21192
22139
|
function getTransporter(config) {
|
|
21193
22140
|
if (!transporter) {
|
|
@@ -21211,11 +22158,11 @@ var emailTool = new ExuluTool({
|
|
|
21211
22158
|
id: "email",
|
|
21212
22159
|
name: "Email",
|
|
21213
22160
|
description: "Send an email.",
|
|
21214
|
-
inputSchema:
|
|
21215
|
-
recipient:
|
|
21216
|
-
subject:
|
|
21217
|
-
html:
|
|
21218
|
-
text:
|
|
22161
|
+
inputSchema: z11.object({
|
|
22162
|
+
recipient: z11.string().describe("The recipient of the email."),
|
|
22163
|
+
subject: z11.string().describe("The subject of the email."),
|
|
22164
|
+
html: z11.string().describe("The HTML body of the email."),
|
|
22165
|
+
text: z11.string().describe("The text body of the email.")
|
|
21219
22166
|
}),
|
|
21220
22167
|
type: "function",
|
|
21221
22168
|
config: [{
|
|
@@ -21281,7 +22228,7 @@ var emailTool = new ExuluTool({
|
|
|
21281
22228
|
});
|
|
21282
22229
|
|
|
21283
22230
|
// src/templates/tools/image-generation.ts
|
|
21284
|
-
import { z as
|
|
22231
|
+
import { z as z12 } from "zod";
|
|
21285
22232
|
var _cachedImageModels;
|
|
21286
22233
|
var setCachedImageModels = (models2) => {
|
|
21287
22234
|
_cachedImageModels = models2;
|
|
@@ -21342,8 +22289,8 @@ var createImageGenerationWidgetTool = (models2) => {
|
|
|
21342
22289
|
needsApproval: false,
|
|
21343
22290
|
type: "function",
|
|
21344
22291
|
config: [],
|
|
21345
|
-
inputSchema:
|
|
21346
|
-
prompt:
|
|
22292
|
+
inputSchema: z12.object({
|
|
22293
|
+
prompt: z12.string().describe(
|
|
21347
22294
|
"Initial image prompt. The user can edit it before generating."
|
|
21348
22295
|
)
|
|
21349
22296
|
}),
|
|
@@ -21454,6 +22401,42 @@ var startTranscriptionPollingLoop = () => {
|
|
|
21454
22401
|
process.on("SIGTERM", stop);
|
|
21455
22402
|
};
|
|
21456
22403
|
|
|
22404
|
+
// src/exulu/recall/reconcile-loop.ts
|
|
22405
|
+
var RECONCILE_INTERVAL_MS = 6e4;
|
|
22406
|
+
var timer2 = null;
|
|
22407
|
+
var stopped2 = false;
|
|
22408
|
+
var tick2 = async () => {
|
|
22409
|
+
if (stopped2) return;
|
|
22410
|
+
try {
|
|
22411
|
+
const acted = await recallService.reconcileOnce();
|
|
22412
|
+
if (acted > 0) {
|
|
22413
|
+
console.log(`[EXULU-RECALL] reconcile tick recovered ${acted} job(s)`);
|
|
22414
|
+
}
|
|
22415
|
+
} catch (err) {
|
|
22416
|
+
console.error(
|
|
22417
|
+
`[EXULU-RECALL] reconcile tick failed: ${err.message}`
|
|
22418
|
+
);
|
|
22419
|
+
} finally {
|
|
22420
|
+
if (!stopped2) {
|
|
22421
|
+
timer2 = setTimeout(tick2, RECONCILE_INTERVAL_MS);
|
|
22422
|
+
}
|
|
22423
|
+
}
|
|
22424
|
+
};
|
|
22425
|
+
var startRecallReconcileLoop = () => {
|
|
22426
|
+
if (timer2) return;
|
|
22427
|
+
stopped2 = false;
|
|
22428
|
+
timer2 = setTimeout(tick2, RECONCILE_INTERVAL_MS);
|
|
22429
|
+
const stop = () => {
|
|
22430
|
+
stopped2 = true;
|
|
22431
|
+
if (timer2) {
|
|
22432
|
+
clearTimeout(timer2);
|
|
22433
|
+
timer2 = null;
|
|
22434
|
+
}
|
|
22435
|
+
};
|
|
22436
|
+
process.on("SIGINT", stop);
|
|
22437
|
+
process.on("SIGTERM", stop);
|
|
22438
|
+
};
|
|
22439
|
+
|
|
21457
22440
|
// src/exulu/app/index.ts
|
|
21458
22441
|
var isDev = process.env.NODE_ENV !== "production";
|
|
21459
22442
|
var lineLimitFormat = winston2.format((info) => {
|
|
@@ -21688,6 +22671,9 @@ var ExuluApp = class {
|
|
|
21688
22671
|
);
|
|
21689
22672
|
}
|
|
21690
22673
|
logRecallStartup();
|
|
22674
|
+
if (recallEnabled()) {
|
|
22675
|
+
startRecallReconcileLoop();
|
|
22676
|
+
}
|
|
21691
22677
|
return this._expressApp;
|
|
21692
22678
|
}
|
|
21693
22679
|
};
|
|
@@ -22728,7 +23714,6 @@ var {
|
|
|
22728
23714
|
promptFavoritesSchema: promptFavoritesSchema3,
|
|
22729
23715
|
transcriptionJobsSchema: transcriptionJobsSchema3,
|
|
22730
23716
|
imageGenerationsSchema: imageGenerationsSchema2,
|
|
22731
|
-
oauthTokensSchema: oauthTokensSchema2,
|
|
22732
23717
|
sharedArtifactsSchema: sharedArtifactsSchema2
|
|
22733
23718
|
} = coreSchemas.get();
|
|
22734
23719
|
var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
|
|
@@ -22751,6 +23736,18 @@ var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
|
|
|
22751
23736
|
console.log(`[EXULU] Field '${sanitizedName}' already exists in ${tableName} table.`);
|
|
22752
23737
|
}
|
|
22753
23738
|
};
|
|
23739
|
+
var migrateUserCredentialsDataColumn = async (knex) => {
|
|
23740
|
+
const dataType = await knex.raw(
|
|
23741
|
+
`SELECT data_type FROM information_schema.columns
|
|
23742
|
+
WHERE table_name = 'user_credentials' AND column_name = 'data'`
|
|
23743
|
+
);
|
|
23744
|
+
if (dataType.rows?.[0]?.data_type === "jsonb") {
|
|
23745
|
+
console.log("[EXULU] Migrating user_credentials.data jsonb -> text.");
|
|
23746
|
+
await knex.raw(
|
|
23747
|
+
"ALTER TABLE user_credentials ALTER COLUMN data TYPE text USING data::text"
|
|
23748
|
+
);
|
|
23749
|
+
}
|
|
23750
|
+
};
|
|
22754
23751
|
var up = async function(knex) {
|
|
22755
23752
|
console.log("[EXULU] Database up.");
|
|
22756
23753
|
const schemas = [
|
|
@@ -22772,7 +23769,6 @@ var up = async function(knex) {
|
|
|
22772
23769
|
promptFavoritesSchema3(),
|
|
22773
23770
|
transcriptionJobsSchema3(),
|
|
22774
23771
|
imageGenerationsSchema2(),
|
|
22775
|
-
oauthTokensSchema2(),
|
|
22776
23772
|
sharedArtifactsSchema2(),
|
|
22777
23773
|
rbacSchema3(),
|
|
22778
23774
|
agentsSchema3(),
|
|
@@ -22806,10 +23802,9 @@ var up = async function(knex) {
|
|
|
22806
23802
|
console.log(`[EXULU] Creating ${schema.name.plural} table.`, schema.fields);
|
|
22807
23803
|
await createTable(schema);
|
|
22808
23804
|
}
|
|
22809
|
-
await knex.raw("DROP
|
|
22810
|
-
await knex.raw(
|
|
22811
|
-
|
|
22812
|
-
);
|
|
23805
|
+
await knex.raw("DROP TABLE IF EXISTS oauth_tokens CASCADE;");
|
|
23806
|
+
await knex.raw(userCredentialsSchema());
|
|
23807
|
+
await migrateUserCredentialsDataColumn(knex);
|
|
22813
23808
|
if (await knex.schema.hasColumn("job_results", "workflow") && await knex.schema.hasColumn("job_results", "trigger_metadata")) {
|
|
22814
23809
|
await knex.raw(
|
|
22815
23810
|
"CREATE INDEX IF NOT EXISTS job_results_email_dedup_idx ON job_results (workflow, (trigger_metadata->>'message_id'))"
|
|
@@ -23003,6 +23998,11 @@ var execute = async ({ contexts }) => {
|
|
|
23003
23998
|
evals: "read"
|
|
23004
23999
|
}).returning("id");
|
|
23005
24000
|
}
|
|
24001
|
+
const existingExternalRole = await db.from("roles").where({ name: "external" }).first();
|
|
24002
|
+
if (!existingExternalRole) {
|
|
24003
|
+
console.log("[EXULU] Creating external role.");
|
|
24004
|
+
await db.from("roles").insert({ name: "external" }).returning("id");
|
|
24005
|
+
}
|
|
23006
24006
|
const existingUser = await db.from("users").where({ email: "admin@exulu.com" }).first();
|
|
23007
24007
|
if (!existingUser) {
|
|
23008
24008
|
const password = await encryptString("admin");
|
|
@@ -23836,7 +24836,7 @@ var MarkdownChunker = class {
|
|
|
23836
24836
|
import * as fs3 from "fs";
|
|
23837
24837
|
import * as path from "path";
|
|
23838
24838
|
import { generateText as generateText9, Output as Output3 } from "ai";
|
|
23839
|
-
import { z as
|
|
24839
|
+
import { z as z13 } from "zod";
|
|
23840
24840
|
import pLimit from "p-limit";
|
|
23841
24841
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
23842
24842
|
import * as mammoth from "mammoth";
|
|
@@ -24295,15 +25295,15 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
24295
25295
|
const result = await generateText9({
|
|
24296
25296
|
model,
|
|
24297
25297
|
output: Output3.object({
|
|
24298
|
-
schema:
|
|
24299
|
-
needs_correction:
|
|
24300
|
-
corrected_text:
|
|
24301
|
-
current_page_table:
|
|
24302
|
-
headers:
|
|
24303
|
-
is_continuation:
|
|
25298
|
+
schema: z13.object({
|
|
25299
|
+
needs_correction: z13.boolean(),
|
|
25300
|
+
corrected_text: z13.string().nullable(),
|
|
25301
|
+
current_page_table: z13.object({
|
|
25302
|
+
headers: z13.array(z13.string()),
|
|
25303
|
+
is_continuation: z13.boolean()
|
|
24304
25304
|
}).nullable(),
|
|
24305
|
-
confidence:
|
|
24306
|
-
reasoning:
|
|
25305
|
+
confidence: z13.enum(["high", "medium", "low"]),
|
|
25306
|
+
reasoning: z13.string()
|
|
24307
25307
|
})
|
|
24308
25308
|
}),
|
|
24309
25309
|
messages: [
|
|
@@ -24923,6 +25923,7 @@ var ExuluPython = {
|
|
|
24923
25923
|
instructions: getPythonSetupInstructions
|
|
24924
25924
|
};
|
|
24925
25925
|
export {
|
|
25926
|
+
CredentialInvalidError,
|
|
24926
25927
|
JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM,
|
|
24927
25928
|
STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM,
|
|
24928
25929
|
ExuluApp,
|