@exulu/backend 2.0.1 → 2.2.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-IJ4HNHOT.js → chunk-Y7JPNBFM.js} +603 -279
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-2PEDFZ2X.js → convert-exulu-tools-to-ai-sdk-tools-M7I2TZQQ.js} +1 -1
- package/dist/index.cjs +1689 -525
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +1108 -307
- package/ee/agentic-retrieval/pipeline/config.test.ts +15 -0
- package/ee/agentic-retrieval/pipeline/config.ts +6 -0
- package/ee/agentic-retrieval/pipeline/global-ids.ts +30 -0
- package/ee/agentic-retrieval/pipeline/index.test.ts +86 -1
- package/ee/agentic-retrieval/pipeline/index.ts +96 -46
- package/ee/agentic-retrieval/pipeline/project-scope.test.ts +73 -0
- package/ee/agentic-retrieval/pipeline/project-scope.ts +77 -0
- package/ee/agentic-retrieval/pipeline/search.test.ts +27 -0
- package/ee/agentic-retrieval/pipeline/search.ts +7 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
validatePythonEnvironment
|
|
8
8
|
} from "./chunk-T6JVFT7L.js";
|
|
9
9
|
import {
|
|
10
|
+
COMPACTION_INSUFFICIENT,
|
|
11
|
+
ContextCompactionRequiredError,
|
|
12
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
10
13
|
ExuluTool,
|
|
11
14
|
LITELLM_UI_PATH,
|
|
12
15
|
LiteLLMAdminError,
|
|
@@ -18,16 +21,19 @@ import {
|
|
|
18
21
|
buildTags,
|
|
19
22
|
checkLicense,
|
|
20
23
|
checkRecordAccess,
|
|
24
|
+
contextOccupancy,
|
|
21
25
|
convertExuluToolsToAiSdkTools,
|
|
22
26
|
copyS3Object,
|
|
23
27
|
createAgenticRetrievalTool,
|
|
24
|
-
createProjectItemsRetrievalTool,
|
|
25
28
|
createTaggedFetch,
|
|
26
29
|
createUppyRoutes,
|
|
27
30
|
decryptOauthState,
|
|
28
31
|
deleteS3Object,
|
|
32
|
+
deriveContextBudget,
|
|
29
33
|
downloadKeyIntoSandbox,
|
|
30
34
|
enableLiteLLMClientMode,
|
|
35
|
+
estimateMessageTokens,
|
|
36
|
+
estimateTokens,
|
|
31
37
|
exchangeCodeForTokens,
|
|
32
38
|
exuluApp,
|
|
33
39
|
getBudgetSettings,
|
|
@@ -40,12 +46,15 @@ import {
|
|
|
40
46
|
getTagDailyActivity,
|
|
41
47
|
getToken,
|
|
42
48
|
getUserBudgetView,
|
|
49
|
+
guardExtractedFileText,
|
|
43
50
|
invalidateBudgetCaches,
|
|
44
51
|
isLiteLLMEnabled,
|
|
45
52
|
listS3ObjectsByPrefix,
|
|
46
53
|
listTagsByPrefix,
|
|
54
|
+
mapStreamErrorMessage,
|
|
47
55
|
oauthRegistry,
|
|
48
56
|
oauthTokenStore,
|
|
57
|
+
parseResetAt,
|
|
49
58
|
postgresClient,
|
|
50
59
|
provisionDefaultUserBudget,
|
|
51
60
|
reportSystemDependencies,
|
|
@@ -55,15 +64,17 @@ import {
|
|
|
55
64
|
sanitizeToolName,
|
|
56
65
|
setBudgetSettings,
|
|
57
66
|
setLiteLLMPackageRoot,
|
|
67
|
+
sliceHistoryAtCheckpoint,
|
|
58
68
|
startLiteLLMSupervisor,
|
|
59
69
|
tagDelete,
|
|
60
70
|
tagInfo,
|
|
71
|
+
truncateToolOutput,
|
|
61
72
|
updateStatistic,
|
|
62
73
|
uploadFile,
|
|
63
74
|
upsertBudget,
|
|
64
75
|
waitForLiteLLMReady,
|
|
65
76
|
withRetry
|
|
66
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-Y7JPNBFM.js";
|
|
67
78
|
import {
|
|
68
79
|
findLiteLLMModel
|
|
69
80
|
} from "./chunk-7CCMW3IW.js";
|
|
@@ -2213,6 +2224,13 @@ var agentsSchema = {
|
|
|
2213
2224
|
name: "sandbox_enabled",
|
|
2214
2225
|
type: "boolean",
|
|
2215
2226
|
default: false
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
// Per-turn budget for ALL tool steps on one chat message (bash, files,
|
|
2230
|
+
// knowledge search, integrations). 0/null = platform default
|
|
2231
|
+
// (DEFAULT_MAX_STEPS in resolve-max-steps.ts). Auto-ALTERed on boot.
|
|
2232
|
+
name: "max_tool_steps",
|
|
2233
|
+
type: "number"
|
|
2216
2234
|
}
|
|
2217
2235
|
]
|
|
2218
2236
|
};
|
|
@@ -4252,7 +4270,7 @@ var ExuluContext2 = class {
|
|
|
4252
4270
|
embedder,
|
|
4253
4271
|
chunker,
|
|
4254
4272
|
processor,
|
|
4255
|
-
active,
|
|
4273
|
+
active: active2,
|
|
4256
4274
|
fields,
|
|
4257
4275
|
queryRewriter,
|
|
4258
4276
|
resultReranker,
|
|
@@ -4283,7 +4301,7 @@ var ExuluContext2 = class {
|
|
|
4283
4301
|
this.description = description;
|
|
4284
4302
|
this.embedder = embedder;
|
|
4285
4303
|
this.chunker = chunker;
|
|
4286
|
-
this.active =
|
|
4304
|
+
this.active = active2;
|
|
4287
4305
|
this.queryRewriter = queryRewriter;
|
|
4288
4306
|
this.resultReranker = resultReranker;
|
|
4289
4307
|
this.entities = entities;
|
|
@@ -5398,14 +5416,26 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
|
|
|
5398
5416
|
)
|
|
5399
5417
|
);
|
|
5400
5418
|
if (args.project) {
|
|
5401
|
-
const
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5419
|
+
const hasAgentic = result.tools.some(
|
|
5420
|
+
(tool2) => tool2?.id === "agentic_context_search"
|
|
5421
|
+
);
|
|
5422
|
+
if (!hasAgentic) {
|
|
5423
|
+
const instance = createAgenticRetrievalTool({
|
|
5424
|
+
contexts: [],
|
|
5425
|
+
user,
|
|
5426
|
+
role: user.role?.id,
|
|
5427
|
+
model: void 0
|
|
5428
|
+
});
|
|
5429
|
+
if (instance) {
|
|
5430
|
+
result.tools.unshift({
|
|
5431
|
+
id: instance.id,
|
|
5432
|
+
name: instance.name,
|
|
5433
|
+
description: instance.description,
|
|
5434
|
+
category: instance.category,
|
|
5435
|
+
type: instance.type,
|
|
5436
|
+
config: []
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5409
5439
|
}
|
|
5410
5440
|
}
|
|
5411
5441
|
result.tools = result.tools.filter((tool2) => tool2 !== null);
|
|
@@ -11105,21 +11135,7 @@ function isOsJunkPath(path2) {
|
|
|
11105
11135
|
const basename = path2.split("/").pop() ?? "";
|
|
11106
11136
|
return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
|
|
11107
11137
|
}
|
|
11108
|
-
async function
|
|
11109
|
-
const { bytes, skillId, isZip, config } = opts;
|
|
11110
|
-
if (!isZip) {
|
|
11111
|
-
await uploadFile(
|
|
11112
|
-
bytes,
|
|
11113
|
-
`skills/${skillId}/v1/SKILL.md`,
|
|
11114
|
-
config,
|
|
11115
|
-
{ contentType: "text/markdown" },
|
|
11116
|
-
void 0,
|
|
11117
|
-
void 0,
|
|
11118
|
-
true
|
|
11119
|
-
// global=true so the key isn't user-prefixed (skill files are shared)
|
|
11120
|
-
);
|
|
11121
|
-
return { filesCount: 1 };
|
|
11122
|
-
}
|
|
11138
|
+
async function extractZipToPrefix(bytes, prefix, config) {
|
|
11123
11139
|
let zip;
|
|
11124
11140
|
try {
|
|
11125
11141
|
zip = await JSZip.loadAsync(bytes);
|
|
@@ -11183,7 +11199,7 @@ async function extractBundleToS3(opts) {
|
|
|
11183
11199
|
}
|
|
11184
11200
|
let filesCount = 0;
|
|
11185
11201
|
for (const { relPath, content } of prepared) {
|
|
11186
|
-
const s3Key =
|
|
11202
|
+
const s3Key = `${prefix}${relPath}`;
|
|
11187
11203
|
await uploadFile(
|
|
11188
11204
|
content,
|
|
11189
11205
|
s3Key,
|
|
@@ -11192,12 +11208,76 @@ async function extractBundleToS3(opts) {
|
|
|
11192
11208
|
void 0,
|
|
11193
11209
|
void 0,
|
|
11194
11210
|
true
|
|
11195
|
-
// global=true —
|
|
11211
|
+
// global=true — skill files are shared across users
|
|
11196
11212
|
);
|
|
11197
11213
|
filesCount += 1;
|
|
11198
11214
|
}
|
|
11199
11215
|
return { filesCount };
|
|
11200
11216
|
}
|
|
11217
|
+
async function extractBundleToS3(opts) {
|
|
11218
|
+
const { bytes, skillId, isZip, config } = opts;
|
|
11219
|
+
if (!isZip) {
|
|
11220
|
+
await uploadFile(
|
|
11221
|
+
bytes,
|
|
11222
|
+
`skills/${skillId}/v1/SKILL.md`,
|
|
11223
|
+
config,
|
|
11224
|
+
{ contentType: "text/markdown" },
|
|
11225
|
+
void 0,
|
|
11226
|
+
void 0,
|
|
11227
|
+
true
|
|
11228
|
+
// global=true so the key isn't user-prefixed (skill files are shared)
|
|
11229
|
+
);
|
|
11230
|
+
return { filesCount: 1 };
|
|
11231
|
+
}
|
|
11232
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v1/`, config);
|
|
11233
|
+
}
|
|
11234
|
+
async function extractBundleToVersion(opts) {
|
|
11235
|
+
const { bytes, skillId, version, config } = opts;
|
|
11236
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v${version}/`, config);
|
|
11237
|
+
}
|
|
11238
|
+
|
|
11239
|
+
// src/skills/frontmatter.ts
|
|
11240
|
+
import JSZip2 from "jszip";
|
|
11241
|
+
function parseFrontmatter(md) {
|
|
11242
|
+
const match = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
11243
|
+
if (!match) return {};
|
|
11244
|
+
const block = match[1];
|
|
11245
|
+
const out = {};
|
|
11246
|
+
for (const line of block.split(/\r?\n/)) {
|
|
11247
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
11248
|
+
if (!m) continue;
|
|
11249
|
+
const key = m[1];
|
|
11250
|
+
let v = m[2].trim();
|
|
11251
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
11252
|
+
v = v.slice(1, -1);
|
|
11253
|
+
}
|
|
11254
|
+
out[key] = v;
|
|
11255
|
+
}
|
|
11256
|
+
return out;
|
|
11257
|
+
}
|
|
11258
|
+
async function parseSkillFrontmatter(zipBytes) {
|
|
11259
|
+
let zip;
|
|
11260
|
+
try {
|
|
11261
|
+
zip = await JSZip2.loadAsync(zipBytes);
|
|
11262
|
+
} catch {
|
|
11263
|
+
return {};
|
|
11264
|
+
}
|
|
11265
|
+
const paths = [];
|
|
11266
|
+
zip.forEach((p, entry) => {
|
|
11267
|
+
if (!entry.dir) paths.push(p);
|
|
11268
|
+
});
|
|
11269
|
+
const heads = new Set(paths.map((p) => p.split("/")[0]).filter(Boolean));
|
|
11270
|
+
let strip = (p) => p;
|
|
11271
|
+
if (heads.size === 1) {
|
|
11272
|
+
const head = [...heads][0] + "/";
|
|
11273
|
+
if (paths.every((p) => p.startsWith(head))) strip = (p) => p.slice(head.length);
|
|
11274
|
+
}
|
|
11275
|
+
const skillPath = paths.find((p) => strip(p) === "SKILL.md");
|
|
11276
|
+
if (!skillPath) return {};
|
|
11277
|
+
const md = await zip.file(skillPath).async("string");
|
|
11278
|
+
const fm = parseFrontmatter(md);
|
|
11279
|
+
return { name: fm.name, description: fm.description };
|
|
11280
|
+
}
|
|
11201
11281
|
|
|
11202
11282
|
// src/sessions/pdf-preview-cache.ts
|
|
11203
11283
|
import { exec } from "child_process";
|
|
@@ -11275,14 +11355,15 @@ import bodyParser from "body-parser";
|
|
|
11275
11355
|
import CryptoJS4 from "crypto-js";
|
|
11276
11356
|
import OpenAI from "openai";
|
|
11277
11357
|
import fs2 from "fs";
|
|
11278
|
-
import { randomUUID as
|
|
11358
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
11279
11359
|
import "@opentelemetry/api";
|
|
11280
|
-
import
|
|
11360
|
+
import JSZip3 from "jszip";
|
|
11281
11361
|
import { createIdGenerator } from "ai";
|
|
11282
11362
|
import cookieParser from "cookie-parser";
|
|
11283
11363
|
|
|
11284
11364
|
// src/exulu/resolve-max-steps.ts
|
|
11285
|
-
|
|
11365
|
+
var DEFAULT_MAX_STEPS = 10;
|
|
11366
|
+
function resolveRetrievalCallBudget(toolConfigs) {
|
|
11286
11367
|
const agentic = toolConfigs?.find((t) => t.id === "agentic_context_search");
|
|
11287
11368
|
if (!agentic?.config) return void 0;
|
|
11288
11369
|
const entry = agentic.config.find((c) => c.name === "max_steps");
|
|
@@ -11291,15 +11372,26 @@ function resolveMaxStepsFromToolConfigs(toolConfigs) {
|
|
|
11291
11372
|
const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
11292
11373
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
|
|
11293
11374
|
}
|
|
11375
|
+
function resolveTurnStepBudget(maxStepCount, agent) {
|
|
11376
|
+
if (typeof maxStepCount === "number" && Number.isFinite(maxStepCount) && maxStepCount > 0) {
|
|
11377
|
+
return Math.floor(maxStepCount);
|
|
11378
|
+
}
|
|
11379
|
+
const raw = agent?.max_tool_steps;
|
|
11380
|
+
const n = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
|
|
11381
|
+
if (Number.isFinite(n) && n > 0) {
|
|
11382
|
+
return Math.floor(n);
|
|
11383
|
+
}
|
|
11384
|
+
return DEFAULT_MAX_STEPS;
|
|
11385
|
+
}
|
|
11294
11386
|
function flattenPart(part) {
|
|
11295
11387
|
const p = part;
|
|
11296
11388
|
if (p?.type === "text") return p.text ?? "";
|
|
11297
11389
|
if (p?.type === "tool-call") {
|
|
11298
|
-
return `
|
|
11390
|
+
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
|
|
11299
11391
|
}
|
|
11300
11392
|
if (p?.type === "tool-result") {
|
|
11301
11393
|
const out = p.output?.value ?? p.output;
|
|
11302
|
-
return `
|
|
11394
|
+
return `The "${p.toolName}" tool returned: ${typeof out === "string" ? out : JSON.stringify(out ?? "")}`;
|
|
11303
11395
|
}
|
|
11304
11396
|
return "";
|
|
11305
11397
|
}
|
|
@@ -11317,7 +11409,7 @@ function flattenToolHistory(messages) {
|
|
|
11317
11409
|
return m;
|
|
11318
11410
|
});
|
|
11319
11411
|
}
|
|
11320
|
-
var FINAL_ANSWER_INSTRUCTION =
|
|
11412
|
+
var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
|
|
11321
11413
|
function finalAnswerGuard(maxSteps) {
|
|
11322
11414
|
return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
|
|
11323
11415
|
toolChoice: "none",
|
|
@@ -11330,6 +11422,68 @@ function finalAnswerGuard(maxSteps) {
|
|
|
11330
11422
|
} : {}
|
|
11331
11423
|
} : void 0;
|
|
11332
11424
|
}
|
|
11425
|
+
function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
|
|
11426
|
+
if (limit == null || limit <= 0 || !agenticToolKey || !allToolKeys.includes(agenticToolKey)) {
|
|
11427
|
+
return () => void 0;
|
|
11428
|
+
}
|
|
11429
|
+
const remainingTools = allToolKeys.filter((k) => k !== agenticToolKey);
|
|
11430
|
+
return ({ steps }) => {
|
|
11431
|
+
const calls = (steps ?? []).flatMap((s) => s?.toolCalls ?? []).filter((c) => c?.toolName === agenticToolKey).length;
|
|
11432
|
+
if (calls < limit) return void 0;
|
|
11433
|
+
return { activeTools: remainingTools };
|
|
11434
|
+
};
|
|
11435
|
+
}
|
|
11436
|
+
|
|
11437
|
+
// src/exulu/context-guard.ts
|
|
11438
|
+
var KEEP_RECENT_TOOL_MESSAGES = 2;
|
|
11439
|
+
var COLLAPSE_KEEP_CHARS = 400;
|
|
11440
|
+
var COLLAPSE_MARKER = " \u2026[older tool output collapsed mid-response to fit the context window \u2014 the full output is in the session file named in the notice above, if one was saved]";
|
|
11441
|
+
function contextGuard(contextWindow) {
|
|
11442
|
+
const budget = deriveContextBudget(contextWindow);
|
|
11443
|
+
return async ({ messages }) => {
|
|
11444
|
+
if (!Array.isArray(messages) || messages.length === 0) return void 0;
|
|
11445
|
+
const tokens = estimateTokens(JSON.stringify(messages));
|
|
11446
|
+
if (tokens < budget.usableWindow) return void 0;
|
|
11447
|
+
const toolIndices = messages.map((m, i) => m?.role === "tool" ? i : -1).filter((i) => i !== -1);
|
|
11448
|
+
const collapsible = new Set(toolIndices.slice(0, Math.max(0, toolIndices.length - KEEP_RECENT_TOOL_MESSAGES)));
|
|
11449
|
+
if (collapsible.size === 0) return void 0;
|
|
11450
|
+
let changed = false;
|
|
11451
|
+
const next = messages.map((m, i) => {
|
|
11452
|
+
if (!collapsible.has(i)) return m;
|
|
11453
|
+
const msg = m;
|
|
11454
|
+
if (!Array.isArray(msg.content)) return m;
|
|
11455
|
+
const content = msg.content.map((part) => {
|
|
11456
|
+
const p = part;
|
|
11457
|
+
if (p?.type !== "tool-result") return part;
|
|
11458
|
+
const out = p.output?.value ?? p.output;
|
|
11459
|
+
const asText = typeof out === "string" ? out : JSON.stringify(out ?? "");
|
|
11460
|
+
if (asText.length <= COLLAPSE_KEEP_CHARS + COLLAPSE_MARKER.length) return part;
|
|
11461
|
+
changed = true;
|
|
11462
|
+
return { ...part, output: { type: "text", value: asText.slice(0, COLLAPSE_KEEP_CHARS) + COLLAPSE_MARKER } };
|
|
11463
|
+
});
|
|
11464
|
+
return { ...m, content };
|
|
11465
|
+
});
|
|
11466
|
+
return changed ? { messages: next } : void 0;
|
|
11467
|
+
};
|
|
11468
|
+
}
|
|
11469
|
+
function composePrepareSteps(...guards) {
|
|
11470
|
+
return async (opts) => {
|
|
11471
|
+
let merged;
|
|
11472
|
+
let messages = opts.messages;
|
|
11473
|
+
for (const guard of guards) {
|
|
11474
|
+
const result = await guard({ ...opts, messages });
|
|
11475
|
+
if (!result) continue;
|
|
11476
|
+
merged = { ...merged ?? {}, ...result };
|
|
11477
|
+
if (Array.isArray(result.messages)) {
|
|
11478
|
+
messages = result.messages;
|
|
11479
|
+
}
|
|
11480
|
+
}
|
|
11481
|
+
if (merged && messages && !("messages" in merged)) {
|
|
11482
|
+
merged.messages = messages;
|
|
11483
|
+
}
|
|
11484
|
+
return merged;
|
|
11485
|
+
};
|
|
11486
|
+
}
|
|
11333
11487
|
|
|
11334
11488
|
// src/exulu/auto-decline-stale-approvals.ts
|
|
11335
11489
|
var AUTO_DECLINE_REASON = "Automatically declined because the user sent a new message instead of responding to the approval request.";
|
|
@@ -11593,7 +11747,9 @@ var ExuluProvider = class {
|
|
|
11593
11747
|
agent,
|
|
11594
11748
|
instructions,
|
|
11595
11749
|
maxStepCount,
|
|
11596
|
-
onTokenUsage
|
|
11750
|
+
onTokenUsage,
|
|
11751
|
+
contextWindow,
|
|
11752
|
+
disabledTools
|
|
11597
11753
|
}) => {
|
|
11598
11754
|
console.log(
|
|
11599
11755
|
"[EXULU] Called generate sync for agent: " + this.name,
|
|
@@ -11621,9 +11777,7 @@ var ExuluProvider = class {
|
|
|
11621
11777
|
if (messages && session && user) {
|
|
11622
11778
|
const previousMessages = await getAgentMessages({
|
|
11623
11779
|
session,
|
|
11624
|
-
user: user.id
|
|
11625
|
-
limit: 50,
|
|
11626
|
-
page: 1
|
|
11780
|
+
user: user.id
|
|
11627
11781
|
});
|
|
11628
11782
|
const previousMessagesContent = previousMessages.map(
|
|
11629
11783
|
(message) => JSON.parse(message.content)
|
|
@@ -11632,6 +11786,12 @@ var ExuluProvider = class {
|
|
|
11632
11786
|
// append the new message to the previous messages:
|
|
11633
11787
|
messages: [...previousMessagesContent, ...messages]
|
|
11634
11788
|
});
|
|
11789
|
+
const contextBudget = deriveContextBudget(contextWindow);
|
|
11790
|
+
const occupancy = contextOccupancy(messages);
|
|
11791
|
+
if (occupancy >= contextBudget.blockThreshold) {
|
|
11792
|
+
throw new ContextCompactionRequiredError(occupancy, contextBudget);
|
|
11793
|
+
}
|
|
11794
|
+
messages = sliceHistoryAtCheckpoint(messages);
|
|
11635
11795
|
}
|
|
11636
11796
|
console.log(
|
|
11637
11797
|
"[EXULU] Message count for agent: " + this.name,
|
|
@@ -11696,6 +11856,34 @@ var ExuluProvider = class {
|
|
|
11696
11856
|
if (memoryContext) {
|
|
11697
11857
|
system += "\n\n" + memoryContext;
|
|
11698
11858
|
}
|
|
11859
|
+
const tools = await convertExuluToolsToAiSdkTools(
|
|
11860
|
+
currentTools,
|
|
11861
|
+
currentSkills,
|
|
11862
|
+
approvedTools,
|
|
11863
|
+
allExuluTools,
|
|
11864
|
+
toolConfigs,
|
|
11865
|
+
providerapikey,
|
|
11866
|
+
contexts,
|
|
11867
|
+
user,
|
|
11868
|
+
exuluConfig,
|
|
11869
|
+
session,
|
|
11870
|
+
req,
|
|
11871
|
+
project,
|
|
11872
|
+
sessionItems,
|
|
11873
|
+
model,
|
|
11874
|
+
agent,
|
|
11875
|
+
memoryItems,
|
|
11876
|
+
contextWindow,
|
|
11877
|
+
disabledTools
|
|
11878
|
+
);
|
|
11879
|
+
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
11880
|
+
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
11881
|
+
const retrievalGuard = retrievalBudgetGuard(
|
|
11882
|
+
resolveRetrievalCallBudget(toolConfigs),
|
|
11883
|
+
agenticToolKey,
|
|
11884
|
+
Object.keys(tools)
|
|
11885
|
+
);
|
|
11886
|
+
const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
|
|
11699
11887
|
const includesContextSearchTool = currentTools?.some(
|
|
11700
11888
|
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
11701
11889
|
);
|
|
@@ -11708,12 +11896,12 @@ var ExuluProvider = class {
|
|
|
11708
11896
|
system += `
|
|
11709
11897
|
|
|
11710
11898
|
|
|
11711
|
-
|
|
11899
|
+
|
|
11712
11900
|
When you use a context search tool, you will include references to the items
|
|
11713
11901
|
retrieved from the tool call result inline in the response using this exact JSON format
|
|
11714
11902
|
(all on one line, no line breaks):
|
|
11715
11903
|
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
11716
|
-
|
|
11904
|
+
|
|
11717
11905
|
IMPORTANT formatting rules:
|
|
11718
11906
|
- Do NOT reference just chunks like "Looking at chunk_index 5 and chunk_index 0 from the search result", always use the JSON format above.
|
|
11719
11907
|
- Use the exact format shown above, all on ONE line
|
|
@@ -11721,9 +11909,9 @@ var ExuluProvider = class {
|
|
|
11721
11909
|
- Use the context ID from the tool result
|
|
11722
11910
|
- Include the file/item name, not the full path
|
|
11723
11911
|
- Separate multiple citations with spaces
|
|
11724
|
-
|
|
11912
|
+
|
|
11725
11913
|
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
11726
|
-
|
|
11914
|
+
|
|
11727
11915
|
The citations will be rendered as interactive badges in the UI.
|
|
11728
11916
|
`;
|
|
11729
11917
|
}
|
|
@@ -11734,12 +11922,12 @@ var ExuluProvider = class {
|
|
|
11734
11922
|
When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
|
|
11735
11923
|
(all on one line, no line breaks):
|
|
11736
11924
|
{url: <url>, title: <title>, snippet: <snippet>}
|
|
11737
|
-
|
|
11925
|
+
|
|
11738
11926
|
IMPORTANT formatting rules:
|
|
11739
11927
|
- Use the exact format shown above, all on ONE line
|
|
11740
11928
|
- Do NOT use quotes around field names or values
|
|
11741
11929
|
- Separate multiple results with spaces
|
|
11742
|
-
|
|
11930
|
+
|
|
11743
11931
|
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
11744
11932
|
`;
|
|
11745
11933
|
}
|
|
@@ -11772,29 +11960,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11772
11960
|
system,
|
|
11773
11961
|
prompt,
|
|
11774
11962
|
maxRetries: 2,
|
|
11775
|
-
tools
|
|
11776
|
-
currentTools,
|
|
11777
|
-
currentSkills,
|
|
11778
|
-
approvedTools,
|
|
11779
|
-
allExuluTools,
|
|
11780
|
-
toolConfigs,
|
|
11781
|
-
providerapikey,
|
|
11782
|
-
contexts,
|
|
11783
|
-
user,
|
|
11784
|
-
exuluConfig,
|
|
11785
|
-
session,
|
|
11786
|
-
req,
|
|
11787
|
-
project,
|
|
11788
|
-
sessionItems,
|
|
11789
|
-
model,
|
|
11790
|
-
agent,
|
|
11791
|
-
memoryItems
|
|
11792
|
-
),
|
|
11963
|
+
tools,
|
|
11793
11964
|
// Stop after the image_generation tool fires — the widget IS the
|
|
11794
11965
|
// assistant's response, no follow-up text turn is wanted (same
|
|
11795
11966
|
// reasoning as question_ask: the UI artifact is the message).
|
|
11796
|
-
prepareStep:
|
|
11797
|
-
stopWhen: [stepCountIs(
|
|
11967
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
11968
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
11798
11969
|
});
|
|
11799
11970
|
console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
|
|
11800
11971
|
const {
|
|
@@ -11855,26 +12026,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11855
12026
|
ignoreIncompleteToolCalls: true
|
|
11856
12027
|
}),
|
|
11857
12028
|
maxRetries: 2,
|
|
11858
|
-
tools
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
approvedTools,
|
|
11862
|
-
allExuluTools,
|
|
11863
|
-
toolConfigs,
|
|
11864
|
-
providerapikey,
|
|
11865
|
-
contexts,
|
|
11866
|
-
user,
|
|
11867
|
-
exuluConfig,
|
|
11868
|
-
session,
|
|
11869
|
-
req,
|
|
11870
|
-
project,
|
|
11871
|
-
sessionItems,
|
|
11872
|
-
model,
|
|
11873
|
-
agent,
|
|
11874
|
-
memoryItems
|
|
11875
|
-
),
|
|
11876
|
-
prepareStep: finalAnswerGuard(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5),
|
|
11877
|
-
stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? 5), hasToolCall("image_generation")]
|
|
12029
|
+
tools,
|
|
12030
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
12031
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
11878
12032
|
});
|
|
11879
12033
|
if (statistics) {
|
|
11880
12034
|
await Promise.all([
|
|
@@ -11926,7 +12080,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11926
12080
|
* - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
|
|
11927
12081
|
* - Image files -> image parts (which ARE supported by Responses API)
|
|
11928
12082
|
*/
|
|
11929
|
-
async processFilePartsInMessages(messages) {
|
|
12083
|
+
async processFilePartsInMessages(messages, offloadCtx) {
|
|
11930
12084
|
const processedMessages = await Promise.all(
|
|
11931
12085
|
messages.map(async (message) => {
|
|
11932
12086
|
if (message.role !== "user" || !Array.isArray(message.parts)) {
|
|
@@ -11971,10 +12125,11 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
11971
12125
|
outputErrorToConsole: false,
|
|
11972
12126
|
newlineDelimiter: "\n"
|
|
11973
12127
|
});
|
|
12128
|
+
const guardedText = await guardExtractedFileText(filename, String(extractedText), offloadCtx);
|
|
11974
12129
|
return {
|
|
11975
12130
|
type: "text",
|
|
11976
12131
|
text: `<file file name = "${filename}" >
|
|
11977
|
-
${
|
|
12132
|
+
${guardedText}
|
|
11978
12133
|
</file>`
|
|
11979
12134
|
};
|
|
11980
12135
|
} catch (error) {
|
|
@@ -11990,7 +12145,6 @@ ${extractedText}
|
|
|
11990
12145
|
...message,
|
|
11991
12146
|
parts: processedParts
|
|
11992
12147
|
};
|
|
11993
|
-
console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
|
|
11994
12148
|
return result;
|
|
11995
12149
|
})
|
|
11996
12150
|
);
|
|
@@ -12013,7 +12167,9 @@ ${extractedText}
|
|
|
12013
12167
|
exuluConfig,
|
|
12014
12168
|
instructions,
|
|
12015
12169
|
req,
|
|
12016
|
-
maxStepCount
|
|
12170
|
+
maxStepCount,
|
|
12171
|
+
contextWindow,
|
|
12172
|
+
disabledTools
|
|
12017
12173
|
}) => {
|
|
12018
12174
|
if (!this.config) {
|
|
12019
12175
|
console.error("[EXULU] Config is required for streaming.");
|
|
@@ -12034,9 +12190,7 @@ ${extractedText}
|
|
|
12034
12190
|
console.log("[EXULU] loading previous messages from session: " + session);
|
|
12035
12191
|
const previousMessages2 = await getAgentMessages({
|
|
12036
12192
|
session,
|
|
12037
|
-
user: user?.id
|
|
12038
|
-
limit: 50,
|
|
12039
|
-
page: 1
|
|
12193
|
+
user: user?.id
|
|
12040
12194
|
});
|
|
12041
12195
|
previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
|
|
12042
12196
|
}
|
|
@@ -12099,60 +12253,28 @@ ${extractedText}
|
|
|
12099
12253
|
if (declined.length && session && user) {
|
|
12100
12254
|
await saveChat({ session, user: user.id, messages: declined });
|
|
12101
12255
|
}
|
|
12102
|
-
messages = await this.processFilePartsInMessages(messages
|
|
12256
|
+
messages = await this.processFilePartsInMessages(messages, {
|
|
12257
|
+
contextWindow,
|
|
12258
|
+
sessionID: session,
|
|
12259
|
+
user,
|
|
12260
|
+
exuluConfig
|
|
12261
|
+
});
|
|
12262
|
+
const chronologicalMessages = messages;
|
|
12263
|
+
const contextBudget = deriveContextBudget(contextWindow);
|
|
12264
|
+
const occupancy = contextOccupancy(chronologicalMessages);
|
|
12265
|
+
if (occupancy >= contextBudget.blockThreshold) {
|
|
12266
|
+
console.warn(
|
|
12267
|
+
`[EXULU] Blocking request: occupancy ${occupancy} >= blockThreshold ${contextBudget.blockThreshold} (window ${contextBudget.contextWindow}).`
|
|
12268
|
+
);
|
|
12269
|
+
throw new ContextCompactionRequiredError(occupancy, contextBudget);
|
|
12270
|
+
}
|
|
12271
|
+
messages = sliceHistoryAtCheckpoint(chronologicalMessages);
|
|
12103
12272
|
const genericContext = "IMPORTANT: \n\n The current date is " + (/* @__PURE__ */ new Date()).toLocaleDateString() + " and the current time is " + (/* @__PURE__ */ new Date()).toLocaleTimeString() + ". If the user does not explicitly provide the current date, for examle when saying ' this weekend', you should assume they are talking with the current date in mind as a reference.";
|
|
12104
12273
|
let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
|
|
12105
12274
|
if (user?.personal_system_prompt?.trim()) {
|
|
12106
12275
|
system += "\n\nUser preferences:\n" + user.personal_system_prompt.trim();
|
|
12107
12276
|
}
|
|
12108
12277
|
system += "\n\n" + genericContext;
|
|
12109
|
-
const includesContextSearchTool = currentTools?.some(
|
|
12110
|
-
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
12111
|
-
);
|
|
12112
|
-
const includesWebSearchTool = currentTools?.some(
|
|
12113
|
-
(tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
|
|
12114
|
-
);
|
|
12115
|
-
console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
|
|
12116
|
-
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
12117
|
-
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
12118
|
-
if (includesContextSearchTool) {
|
|
12119
|
-
system += `
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
When you use a context search tool, you will include references to the items
|
|
12124
|
-
retrieved from the tool call result inline in the response using this exact JSON format
|
|
12125
|
-
(all on one line, no line breaks):
|
|
12126
|
-
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
12127
|
-
|
|
12128
|
-
IMPORTANT formatting rules:
|
|
12129
|
-
- Use the exact format shown above, all on ONE line
|
|
12130
|
-
- Do NOT use quotes around field names or values
|
|
12131
|
-
- Use the context ID from the tool result
|
|
12132
|
-
- Include the file/item name, not the full path
|
|
12133
|
-
- Separate multiple citations with spaces
|
|
12134
|
-
|
|
12135
|
-
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
12136
|
-
|
|
12137
|
-
The citations will be rendered as interactive badges in the UI.
|
|
12138
|
-
`;
|
|
12139
|
-
}
|
|
12140
|
-
if (includesWebSearchTool) {
|
|
12141
|
-
system += `
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
|
|
12145
|
-
(all on one line, no line breaks):
|
|
12146
|
-
{url: <url>, title: <title>, snippet: <snippet>}
|
|
12147
|
-
|
|
12148
|
-
IMPORTANT formatting rules:
|
|
12149
|
-
- Use the exact format shown above, all on ONE line
|
|
12150
|
-
- Do NOT use quotes around field names or values
|
|
12151
|
-
- Separate multiple results with spaces
|
|
12152
|
-
|
|
12153
|
-
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
12154
|
-
`;
|
|
12155
|
-
}
|
|
12156
12278
|
if (currentSkills?.length) {
|
|
12157
12279
|
const skillsList = currentSkills.map((skill) => {
|
|
12158
12280
|
const description = (skill.description ?? "").trim();
|
|
@@ -12207,6 +12329,11 @@ ${skillsList}
|
|
|
12207
12329
|
read them with the readFile tool. Files you produce yourself (via writeFile or via shell
|
|
12208
12330
|
commands like \`node create_doc.js\`) live in the same place. These files are scoped to
|
|
12209
12331
|
this single session; they are NOT visible in other sessions, projects, or knowledge bases.
|
|
12332
|
+
|
|
12333
|
+
Note on large outputs: oversized tool outputs and large uploaded documents are automatically
|
|
12334
|
+
truncated in the conversation; the FULL content is saved as a session file (named in the
|
|
12335
|
+
truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
|
|
12336
|
+
to page through it \u2014 do not ask the user to re-upload.
|
|
12210
12337
|
`;
|
|
12211
12338
|
system += `
|
|
12212
12339
|
|
|
@@ -12230,9 +12357,66 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12230
12357
|
sessionItems,
|
|
12231
12358
|
model,
|
|
12232
12359
|
agent,
|
|
12233
|
-
memoryItems
|
|
12360
|
+
memoryItems,
|
|
12361
|
+
contextWindow,
|
|
12362
|
+
disabledTools
|
|
12234
12363
|
);
|
|
12235
12364
|
console.log("[EXULU] Converted tools", Object.keys(tools));
|
|
12365
|
+
const includesContextSearchTool = currentTools?.some(
|
|
12366
|
+
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
12367
|
+
);
|
|
12368
|
+
const includesWebSearchTool = currentTools?.some(
|
|
12369
|
+
(tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
|
|
12370
|
+
);
|
|
12371
|
+
console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
|
|
12372
|
+
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
12373
|
+
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
12374
|
+
if (includesContextSearchTool) {
|
|
12375
|
+
system += `
|
|
12376
|
+
|
|
12377
|
+
|
|
12378
|
+
|
|
12379
|
+
When you use a context search tool, you will include references to the items
|
|
12380
|
+
retrieved from the tool call result inline in the response using this exact JSON format
|
|
12381
|
+
(all on one line, no line breaks):
|
|
12382
|
+
{item_name: <item_name>, item_id: <item_id>, context: <context_id>, chunk_id: <chunk_id>, chunk_index: <chunk_index>}
|
|
12383
|
+
|
|
12384
|
+
IMPORTANT formatting rules:
|
|
12385
|
+
- Use the exact format shown above, all on ONE line
|
|
12386
|
+
- Do NOT use quotes around field names or values
|
|
12387
|
+
- Use the context ID from the tool result
|
|
12388
|
+
- Include the file/item name, not the full path
|
|
12389
|
+
- Separate multiple citations with spaces
|
|
12390
|
+
|
|
12391
|
+
Example: {item_name: document.pdf, item_id: abc123, context: my-context-id, chunk_id: chunk_456, chunk_index: 0}
|
|
12392
|
+
|
|
12393
|
+
The citations will be rendered as interactive badges in the UI.
|
|
12394
|
+
`;
|
|
12395
|
+
}
|
|
12396
|
+
if (includesWebSearchTool) {
|
|
12397
|
+
system += `
|
|
12398
|
+
|
|
12399
|
+
|
|
12400
|
+
When you use a web search tool, you will include references to the results of the tool call result inline in the response using this exact JSON format
|
|
12401
|
+
(all on one line, no line breaks):
|
|
12402
|
+
{url: <url>, title: <title>, snippet: <snippet>}
|
|
12403
|
+
|
|
12404
|
+
IMPORTANT formatting rules:
|
|
12405
|
+
- Use the exact format shown above, all on ONE line
|
|
12406
|
+
- Do NOT use quotes around field names or values
|
|
12407
|
+
- Separate multiple results with spaces
|
|
12408
|
+
|
|
12409
|
+
Example: {url: https://www.google.com, title: Google, snippet: The result of the web search.}
|
|
12410
|
+
`;
|
|
12411
|
+
}
|
|
12412
|
+
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
12413
|
+
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
12414
|
+
const retrievalGuard = retrievalBudgetGuard(
|
|
12415
|
+
resolveRetrievalCallBudget(toolConfigs),
|
|
12416
|
+
agenticToolKey,
|
|
12417
|
+
Object.keys(tools)
|
|
12418
|
+
);
|
|
12419
|
+
const turnBudget = resolveTurnStepBudget(maxStepCount, agent);
|
|
12236
12420
|
const result = streamText({
|
|
12237
12421
|
temperature: 0,
|
|
12238
12422
|
// TODO Make this configurable
|
|
@@ -12257,10 +12441,9 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12257
12441
|
`Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
|
|
12258
12442
|
);
|
|
12259
12443
|
},
|
|
12260
|
-
//
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
stopWhen: [stepCountIs(maxStepCount ?? resolveMaxStepsFromToolConfigs(toolConfigs) ?? (currentSkills?.length ? 10 : 5)), hasToolCall("image_generation")]
|
|
12444
|
+
// todo allow configuring the step budget per skill
|
|
12445
|
+
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
|
|
12446
|
+
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
12264
12447
|
});
|
|
12265
12448
|
return {
|
|
12266
12449
|
stream: result,
|
|
@@ -12271,19 +12454,14 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
12271
12454
|
};
|
|
12272
12455
|
var getAgentMessages = async ({
|
|
12273
12456
|
session,
|
|
12274
|
-
user
|
|
12275
|
-
limit,
|
|
12276
|
-
page
|
|
12457
|
+
user
|
|
12277
12458
|
}) => {
|
|
12278
12459
|
const { db } = await postgresClient();
|
|
12279
|
-
console.log(
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
query.offset((page - 1) * limit);
|
|
12285
|
-
}
|
|
12286
|
-
const messages = await query;
|
|
12460
|
+
console.log("[EXULU] getting agent messages for session: " + session + " and user: " + user);
|
|
12461
|
+
const messages = await db.from("agent_messages").where({ session, user: user || null }).orderBy([
|
|
12462
|
+
{ column: "createdAt", order: "asc" },
|
|
12463
|
+
{ column: "id", order: "asc" }
|
|
12464
|
+
]);
|
|
12287
12465
|
return messages;
|
|
12288
12466
|
};
|
|
12289
12467
|
var getSession = async ({ sessionID }) => {
|
|
@@ -12386,18 +12564,163 @@ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
|
|
|
12386
12564
|
};
|
|
12387
12565
|
};
|
|
12388
12566
|
|
|
12389
|
-
// src/exulu/
|
|
12390
|
-
var
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12567
|
+
// src/exulu/resolve-context-window.ts
|
|
12568
|
+
var resolveContextWindow = async ({
|
|
12569
|
+
modelId,
|
|
12570
|
+
exuluProvider
|
|
12571
|
+
}) => {
|
|
12572
|
+
if (process.env.EXULU_USE_LITELLM === "true") {
|
|
12573
|
+
const entry = await findLiteLLMModel(modelId);
|
|
12574
|
+
const fromCatalog = entry?.max_input_tokens ?? entry?.max_tokens;
|
|
12575
|
+
if (fromCatalog != null && fromCatalog > 0) return fromCatalog;
|
|
12576
|
+
} else if (exuluProvider) {
|
|
12577
|
+
try {
|
|
12578
|
+
const fromProvider = exuluProvider.maxContextLength;
|
|
12579
|
+
if (fromProvider != null && fromProvider > 0) return fromProvider;
|
|
12580
|
+
} catch {
|
|
12581
|
+
}
|
|
12395
12582
|
}
|
|
12583
|
+
console.warn(
|
|
12584
|
+
`[EXULU] Unknown context window for model "${modelId}" \u2014 assuming ${DEFAULT_CONTEXT_WINDOW}. Check the LiteLLM catalog / provider template metadata.`
|
|
12585
|
+
);
|
|
12586
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
12396
12587
|
};
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12588
|
+
|
|
12589
|
+
// src/exulu/active-streams.ts
|
|
12590
|
+
var active = /* @__PURE__ */ new Set();
|
|
12591
|
+
var markStreamActive = (sessionID) => {
|
|
12592
|
+
active.add(sessionID);
|
|
12593
|
+
};
|
|
12594
|
+
var clearStreamActive = (sessionID) => {
|
|
12595
|
+
active.delete(sessionID);
|
|
12596
|
+
};
|
|
12597
|
+
var isStreamActive = (sessionID) => active.has(sessionID);
|
|
12598
|
+
|
|
12599
|
+
// src/exulu/compact-session.ts
|
|
12600
|
+
import { randomUUID } from "crypto";
|
|
12601
|
+
import { generateText as generateText6, validateUIMessages as validateUIMessages2 } from "ai";
|
|
12602
|
+
var CompactionInsufficientError = class extends Error {
|
|
12603
|
+
constructor(reason) {
|
|
12604
|
+
super(JSON.stringify({ code: COMPACTION_INSUFFICIENT, message: reason }));
|
|
12605
|
+
this.name = "CompactionInsufficientError";
|
|
12606
|
+
}
|
|
12607
|
+
};
|
|
12608
|
+
var MIN_TAIL_MESSAGES = 2;
|
|
12609
|
+
var SUMMARY_TOOL_OUTPUT_SLICE = 1500;
|
|
12610
|
+
var SUMMARY_TOOL_INPUT_SLICE = 200;
|
|
12611
|
+
var SUMMARY_SYSTEM = `You compress chat histories for an AI assistant. Produce a dense, factual summary of the conversation below. Preserve:
|
|
12612
|
+
- the user's intent and any outstanding requests
|
|
12613
|
+
- key facts, decisions, and constraints
|
|
12614
|
+
- files, artifacts, and session files touched \u2014 ALWAYS keep exact file and item names so they stay retrievable
|
|
12615
|
+
- errors encountered and how they were resolved
|
|
12616
|
+
- pending tasks and the current state of the work
|
|
12617
|
+
Do not invent information. Do not include pleasantries. Write compact prose or bullet points.`;
|
|
12618
|
+
var splitTail = (messages, tailTokenBudget) => {
|
|
12619
|
+
const minTail = Math.min(MIN_TAIL_MESSAGES, messages.length);
|
|
12620
|
+
let cut = messages.length;
|
|
12621
|
+
let tokens = 0;
|
|
12622
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
12623
|
+
const t = estimateMessageTokens(messages[i]);
|
|
12624
|
+
const tailCount = messages.length - i;
|
|
12625
|
+
if (tailCount > minTail && tokens + t > tailTokenBudget) break;
|
|
12626
|
+
tokens += t;
|
|
12627
|
+
cut = i;
|
|
12628
|
+
}
|
|
12629
|
+
return { head: messages.slice(0, cut), tail: messages.slice(cut) };
|
|
12630
|
+
};
|
|
12631
|
+
var serializeForSummary = (messages) => messages.map((m) => {
|
|
12632
|
+
const parts = (m.parts ?? []).map((part) => {
|
|
12633
|
+
const p = part;
|
|
12634
|
+
if (p.type === "text") return p.text ?? "";
|
|
12635
|
+
if (p.type === "file") return `[file: ${p.filename ?? p.url ?? "attachment"}]`;
|
|
12636
|
+
if (p.type === "reasoning" || p.type === "step-start") return "";
|
|
12637
|
+
if (p.type?.startsWith("tool-") || p.type === "dynamic-tool") {
|
|
12638
|
+
const out = p.output?.value ?? p.output;
|
|
12639
|
+
const outText = typeof out === "string" ? out : JSON.stringify(out ?? "");
|
|
12640
|
+
return `[tool ${p.type}: ${JSON.stringify(p.input ?? {}).slice(0, SUMMARY_TOOL_INPUT_SLICE)}] \u2192 ${outText.slice(0, SUMMARY_TOOL_OUTPUT_SLICE)}`;
|
|
12641
|
+
}
|
|
12642
|
+
return "";
|
|
12643
|
+
}).filter(Boolean).join("\n");
|
|
12644
|
+
return `${m.role.toUpperCase()}:
|
|
12645
|
+
${parts}`;
|
|
12646
|
+
}).join("\n\n");
|
|
12647
|
+
var compactSession = async ({
|
|
12648
|
+
sessionID,
|
|
12649
|
+
user,
|
|
12650
|
+
languageModel,
|
|
12651
|
+
contextWindow,
|
|
12652
|
+
steer,
|
|
12653
|
+
modelId,
|
|
12654
|
+
summarize
|
|
12655
|
+
}) => {
|
|
12656
|
+
const budget = deriveContextBudget(contextWindow);
|
|
12657
|
+
const rows = await getAgentMessages({ session: sessionID, user: user.id });
|
|
12658
|
+
const all = await validateUIMessages2({ messages: rows.map((r) => JSON.parse(r.content)) });
|
|
12659
|
+
const history = sliceHistoryAtCheckpoint(all);
|
|
12660
|
+
const { head, tail } = splitTail(history, budget.compactionTailTokens);
|
|
12661
|
+
if (head.length === 0) {
|
|
12662
|
+
throw new CompactionInsufficientError(
|
|
12663
|
+
"There is nothing left to compact \u2014 the recent messages already form the whole context. Start a new chat instead."
|
|
12664
|
+
);
|
|
12665
|
+
}
|
|
12666
|
+
let corpus = serializeForSummary(head);
|
|
12667
|
+
const originalTokens = estimateTokens(corpus);
|
|
12668
|
+
corpus = truncateToolOutput(corpus, contextWindow, "history", 0.3, Math.floor(budget.usableWindow * 0.8) * 4);
|
|
12669
|
+
const system = steer?.trim() ? `${SUMMARY_SYSTEM}
|
|
12670
|
+
|
|
12671
|
+
Focus especially on: ${steer.trim()}` : SUMMARY_SYSTEM;
|
|
12672
|
+
const doSummarize = summarize ?? (async ({ system: sys, prompt, maxOutputTokens }) => {
|
|
12673
|
+
const { text } = await generateText6({
|
|
12674
|
+
model: languageModel,
|
|
12675
|
+
system: sys,
|
|
12676
|
+
prompt,
|
|
12677
|
+
temperature: 0,
|
|
12678
|
+
maxRetries: 2,
|
|
12679
|
+
maxOutputTokens
|
|
12680
|
+
});
|
|
12681
|
+
return text;
|
|
12682
|
+
});
|
|
12683
|
+
const summary = await doSummarize({ system, prompt: corpus, maxOutputTokens: budget.summaryBudgetTokens });
|
|
12684
|
+
const summaryTokens = estimateTokens(summary);
|
|
12685
|
+
let tailTokens = 0;
|
|
12686
|
+
for (const m of tail) tailTokens += estimateMessageTokens(m);
|
|
12687
|
+
const occupancyEstimate = summaryTokens + tailTokens;
|
|
12688
|
+
if (occupancyEstimate >= budget.blockThreshold) {
|
|
12689
|
+
throw new CompactionInsufficientError(
|
|
12690
|
+
"Compacting cannot shrink this conversation below the context limit \u2014 a recent message or output is too large by itself. Start a new chat."
|
|
12691
|
+
);
|
|
12692
|
+
}
|
|
12693
|
+
const compaction = {
|
|
12694
|
+
coversUpTo: head[head.length - 1].id,
|
|
12695
|
+
originalTokens,
|
|
12696
|
+
summaryTokens,
|
|
12697
|
+
occupancyEstimate,
|
|
12698
|
+
...steer?.trim() ? { steer: steer.trim() } : {}
|
|
12699
|
+
};
|
|
12700
|
+
const checkpoint = {
|
|
12701
|
+
id: `compaction_${randomUUID()}`,
|
|
12702
|
+
role: "user",
|
|
12703
|
+
parts: [{ type: "text", text: `[Conversation summary \u2014 earlier messages were compacted]
|
|
12704
|
+
|
|
12705
|
+
${summary}` }],
|
|
12706
|
+
metadata: { compaction }
|
|
12707
|
+
};
|
|
12708
|
+
await saveChat({ session: sessionID, user: user.id, messages: [checkpoint], ...modelId ? { model: modelId } : {} });
|
|
12709
|
+
return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
|
|
12710
|
+
};
|
|
12711
|
+
|
|
12712
|
+
// src/exulu/transcribe.ts
|
|
12713
|
+
var TranscriptionError = class extends Error {
|
|
12714
|
+
constructor(upstreamStatus, message) {
|
|
12715
|
+
super(message);
|
|
12716
|
+
this.upstreamStatus = upstreamStatus;
|
|
12717
|
+
this.name = "TranscriptionError";
|
|
12718
|
+
}
|
|
12719
|
+
};
|
|
12720
|
+
async function transcribeAudio(args) {
|
|
12721
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
12722
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
12723
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
12401
12724
|
const model = process.env.TRANSCRIPTION_MODEL;
|
|
12402
12725
|
if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
|
|
12403
12726
|
if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
|
|
@@ -12751,7 +13074,7 @@ function checkApiKeyScope(user, agentId) {
|
|
|
12751
13074
|
import "express";
|
|
12752
13075
|
import {
|
|
12753
13076
|
streamText as streamText2,
|
|
12754
|
-
generateText as
|
|
13077
|
+
generateText as generateText7,
|
|
12755
13078
|
stepCountIs as stepCountIs2,
|
|
12756
13079
|
jsonSchema
|
|
12757
13080
|
} from "ai";
|
|
@@ -12841,7 +13164,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
|
|
|
12841
13164
|
}
|
|
12842
13165
|
|
|
12843
13166
|
// src/exulu/openai-gateway.ts
|
|
12844
|
-
import { randomUUID } from "crypto";
|
|
13167
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
12845
13168
|
import "crypto-js";
|
|
12846
13169
|
import express from "express";
|
|
12847
13170
|
function convertOpenAIToolsToAiSdkTools(tools) {
|
|
@@ -13155,6 +13478,10 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
13155
13478
|
}
|
|
13156
13479
|
const providerapikey = resolved.apiKey;
|
|
13157
13480
|
const languageModel = resolved.languageModel;
|
|
13481
|
+
const contextWindow = await resolveContextWindow({
|
|
13482
|
+
modelId: resolved.model.id,
|
|
13483
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
13484
|
+
});
|
|
13158
13485
|
const disabledTools = req.body.disabledTools ?? [];
|
|
13159
13486
|
const enabledTools = await getEnabledTools(
|
|
13160
13487
|
agent,
|
|
@@ -13179,12 +13506,50 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
13179
13506
|
project?.id,
|
|
13180
13507
|
void 0,
|
|
13181
13508
|
languageModel,
|
|
13182
|
-
agent
|
|
13509
|
+
agent,
|
|
13510
|
+
void 0,
|
|
13511
|
+
contextWindow,
|
|
13512
|
+
disabledTools
|
|
13183
13513
|
);
|
|
13514
|
+
const gatewayAgenticEntry = enabledTools?.find((t) => t.id === "agentic_context_search");
|
|
13515
|
+
const gatewayAgenticKey = gatewayAgenticEntry ? sanitizeToolName(gatewayAgenticEntry.name) : void 0;
|
|
13516
|
+
const gatewayRetrievalGuard = retrievalBudgetGuard(
|
|
13517
|
+
resolveRetrievalCallBudget(agent.tools),
|
|
13518
|
+
gatewayAgenticKey,
|
|
13519
|
+
Object.keys(convertedTools)
|
|
13520
|
+
);
|
|
13521
|
+
const turnBudget = resolveTurnStepBudget(void 0, agent);
|
|
13184
13522
|
const clientTools = Array.isArray(req.body.tools) ? req.body.tools : [];
|
|
13185
13523
|
const activeTools = clientTools.length > 0 ? convertOpenAIToolsToAiSdkTools(clientTools) : convertedTools;
|
|
13186
13524
|
const openaiMessages = req.body.messages ?? [];
|
|
13187
13525
|
const { systemPrompt: requestSystemPrompt, coreMessages } = convertOpenAIMessagesToModelMessages(openaiMessages);
|
|
13526
|
+
const gatewayBudget = deriveContextBudget(contextWindow);
|
|
13527
|
+
const IMAGE_TOKEN_ALLOWANCE = 1e3;
|
|
13528
|
+
let imageCount = 0;
|
|
13529
|
+
const textOnlyMessages = openaiMessages.map((m) => {
|
|
13530
|
+
if (!Array.isArray(m.content)) return m;
|
|
13531
|
+
return {
|
|
13532
|
+
...m,
|
|
13533
|
+
content: m.content.map((p) => {
|
|
13534
|
+
if (p && typeof p === "object" && "image_url" in p && p.image_url) {
|
|
13535
|
+
imageCount += 1;
|
|
13536
|
+
return { ...p, image_url: { url: "[image]" } };
|
|
13537
|
+
}
|
|
13538
|
+
return p;
|
|
13539
|
+
})
|
|
13540
|
+
};
|
|
13541
|
+
});
|
|
13542
|
+
const promptTokens = estimateTokens(JSON.stringify(textOnlyMessages)) + imageCount * IMAGE_TOKEN_ALLOWANCE;
|
|
13543
|
+
if (promptTokens >= gatewayBudget.blockThreshold) {
|
|
13544
|
+
res.status(400).json({
|
|
13545
|
+
error: {
|
|
13546
|
+
message: `This request is ~${promptTokens.toLocaleString("en-US")} tokens, which exceeds the model's usable context window (${gatewayBudget.usableWindow.toLocaleString("en-US")} tokens). Reduce the conversation history.`,
|
|
13547
|
+
type: "invalid_request_error",
|
|
13548
|
+
code: "context_length_exceeded"
|
|
13549
|
+
}
|
|
13550
|
+
});
|
|
13551
|
+
return;
|
|
13552
|
+
}
|
|
13188
13553
|
const agentInstructions = agent.instructions ?? "";
|
|
13189
13554
|
const systemParts = [
|
|
13190
13555
|
agentInstructions ? `You are an agent named: ${agent.name}
|
|
@@ -13194,7 +13559,7 @@ ${project.description}` : ""}` : "",
|
|
|
13194
13559
|
requestSystemPrompt
|
|
13195
13560
|
].filter(Boolean);
|
|
13196
13561
|
const systemPrompt = systemParts.join("\n\n");
|
|
13197
|
-
const completionId = `chatcmpl-${
|
|
13562
|
+
const completionId = `chatcmpl-${randomUUID2()}`;
|
|
13198
13563
|
const created = Math.floor(Date.now() / 1e3);
|
|
13199
13564
|
const hasTools = Object.keys(activeTools).length > 0;
|
|
13200
13565
|
const ctx = { completionId, created, modelId };
|
|
@@ -13208,7 +13573,8 @@ ${project.description}` : ""}` : "",
|
|
|
13208
13573
|
messages: coreMessages,
|
|
13209
13574
|
tools: hasTools ? activeTools : void 0,
|
|
13210
13575
|
maxRetries: 2,
|
|
13211
|
-
|
|
13576
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
13577
|
+
stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)],
|
|
13212
13578
|
onError: (error) => {
|
|
13213
13579
|
console.error("[OPENAI GATEWAY] stream error:", error);
|
|
13214
13580
|
}
|
|
@@ -13241,13 +13607,14 @@ ${project.description}` : ""}` : "",
|
|
|
13241
13607
|
const usage = await result.usage;
|
|
13242
13608
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
13243
13609
|
} else {
|
|
13244
|
-
const { text, usage } = await
|
|
13610
|
+
const { text, usage } = await generateText7({
|
|
13245
13611
|
model: languageModel,
|
|
13246
13612
|
system: systemPrompt || void 0,
|
|
13247
13613
|
messages: coreMessages,
|
|
13248
13614
|
tools: hasTools ? activeTools : void 0,
|
|
13249
13615
|
maxRetries: 2,
|
|
13250
|
-
|
|
13616
|
+
prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
|
|
13617
|
+
stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)]
|
|
13251
13618
|
});
|
|
13252
13619
|
res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
|
|
13253
13620
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
@@ -13447,6 +13814,148 @@ var contentHeadersFor = (key, contentType, filename) => {
|
|
|
13447
13814
|
};
|
|
13448
13815
|
var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
|
|
13449
13816
|
|
|
13817
|
+
// src/skills/skill-access.ts
|
|
13818
|
+
async function resolveSkillByName(db, name) {
|
|
13819
|
+
const row = await db("skills").where({ name }).first();
|
|
13820
|
+
return row ?? null;
|
|
13821
|
+
}
|
|
13822
|
+
async function canAccessSkill(db, skill, action, user) {
|
|
13823
|
+
const rbac = await RBACResolver(db, "skill", skill.id, skill.rights_mode || "private");
|
|
13824
|
+
return checkRecordAccess({ ...skill, RBAC: rbac }, action, user);
|
|
13825
|
+
}
|
|
13826
|
+
async function filterReadableSkills(db, skills, user) {
|
|
13827
|
+
const out = [];
|
|
13828
|
+
for (const s of skills) {
|
|
13829
|
+
if (await canAccessSkill(db, s, "read", user)) out.push(s);
|
|
13830
|
+
}
|
|
13831
|
+
return out;
|
|
13832
|
+
}
|
|
13833
|
+
|
|
13834
|
+
// src/skills/bootstrap/clients.ts
|
|
13835
|
+
var CLIENT_MANIFEST = [
|
|
13836
|
+
{ id: "agents", dir: ".agents/skills" },
|
|
13837
|
+
// cross-agent standard (symlink canonical store)
|
|
13838
|
+
{ id: "claude", dir: ".claude/skills" },
|
|
13839
|
+
{ id: "windsurf", dir: ".windsurf/skills" },
|
|
13840
|
+
{ id: "continue", dir: ".continue/skills" },
|
|
13841
|
+
{ id: "roo", dir: ".roo/skills" },
|
|
13842
|
+
{ id: "kilocode", dir: ".kilocode/skills" },
|
|
13843
|
+
{ id: "crush", dir: ".crush/skills" },
|
|
13844
|
+
{ id: "goose", dir: ".goose/skills" },
|
|
13845
|
+
{ id: "qwen", dir: ".qwen/skills" },
|
|
13846
|
+
{ id: "iflow", dir: ".iflow/skills" },
|
|
13847
|
+
{ id: "junie", dir: ".junie/skills" },
|
|
13848
|
+
{ id: "kiro", dir: ".kiro/skills" },
|
|
13849
|
+
{ id: "trae", dir: ".trae/skills" },
|
|
13850
|
+
{ id: "augment", dir: ".augment/skills" },
|
|
13851
|
+
{ id: "factory", dir: ".factory/skills" },
|
|
13852
|
+
{ id: "devin", dir: ".devin/skills" },
|
|
13853
|
+
{ id: "openhands", dir: ".openhands/skills" },
|
|
13854
|
+
{ id: "pi", dir: ".pi/skills" },
|
|
13855
|
+
{ id: "cortex", dir: ".cortex/skills" },
|
|
13856
|
+
{ id: "zencoder", dir: ".zencoder/skills" },
|
|
13857
|
+
{ id: "codebuddy", dir: ".codebuddy/skills" },
|
|
13858
|
+
{ id: "codestudio", dir: ".codestudio/skills" },
|
|
13859
|
+
{ id: "commandcode", dir: ".commandcode/skills" },
|
|
13860
|
+
{ id: "codemaker", dir: ".codemaker/skills" },
|
|
13861
|
+
{ id: "codeartsdoer", dir: ".codeartsdoer/skills" },
|
|
13862
|
+
{ id: "lingma", dir: ".lingma/skills" },
|
|
13863
|
+
{ id: "qoder", dir: ".qoder/skills" },
|
|
13864
|
+
{ id: "rovodev", dir: ".rovodev/skills" },
|
|
13865
|
+
{ id: "moxby", dir: ".moxby/skills" },
|
|
13866
|
+
{ id: "mux", dir: ".mux/skills" },
|
|
13867
|
+
{ id: "neovate", dir: ".neovate/skills" },
|
|
13868
|
+
{ id: "ona", dir: ".ona/skills" },
|
|
13869
|
+
{ id: "pochi", dir: ".pochi/skills" },
|
|
13870
|
+
{ id: "reasonix", dir: ".reasonix/skills" },
|
|
13871
|
+
{ id: "terramind", dir: ".terramind/skills" },
|
|
13872
|
+
{ id: "tinycloud", dir: ".tinycloud/skills" },
|
|
13873
|
+
{ id: "vibe", dir: ".vibe/skills" },
|
|
13874
|
+
{ id: "adal", dir: ".adal/skills" },
|
|
13875
|
+
{ id: "aider-desk", dir: ".aider-desk/skills" },
|
|
13876
|
+
{ id: "autohand", dir: ".autohand/skills" },
|
|
13877
|
+
{ id: "bob", dir: ".bob/skills" },
|
|
13878
|
+
{ id: "hermes", dir: ".hermes/skills" },
|
|
13879
|
+
{ id: "inferencesh", dir: ".inferencesh/skills" },
|
|
13880
|
+
{ id: "jazz", dir: ".jazz/skills" },
|
|
13881
|
+
{ id: "kode", dir: ".kode/skills" },
|
|
13882
|
+
{ id: "mcpjam", dir: ".mcpjam/skills" },
|
|
13883
|
+
{ id: "forge", dir: ".forge/skills" },
|
|
13884
|
+
{ id: "tabnine", dir: ".tabnine/agent/skills" }
|
|
13885
|
+
// exception: nested under agent/
|
|
13886
|
+
];
|
|
13887
|
+
|
|
13888
|
+
// src/skills/bootstrap/exulu-sh.generated.ts
|
|
13889
|
+
var EXULU_SH_B64 = "IyEvYmluL3NoCiMgZXh1bHUg4oCUIGhlbHBlciBmb3IgdGhlIEV4dWx1IGNlbnRyYWwgc2tpbGwgbGlicmFyeS4gVGhlIGFnZW50IGludm9rZXMgdGhpcwojIChuZXZlciByYXcgY3VybCk6IHRoZSB0b2tlbiBpcyByZWFkIGZyb20gdGhlIGNvbmZpZyBmaWxlIGhlcmUgYW5kIHNlbnQgdmlhCiMgYHgtYXBpLWtleTogQmVhcmVyYCwgc28gaXQgbmV2ZXIgZW50ZXJzIHRoZSBtb2RlbCBjb250ZXh0LiBDbGllbnQgZmFuLW91dAojIChjb3B5L3N5bWxpbmsgYWNyb3NzIGFnZW50IGNsaWVudHMpIGlzIGRldGVybWluaXN0aWMuCnNldCAtZXUKCkNPTkZJR19ESVI9IiRIT01FLy5jb25maWcvZXh1bHUiCkNPTkZJR19GSUxFPSIkQ09ORklHX0RJUi9za2lsbHMuanNvbiIKCmRpZSgpIHsgcHJpbnRmICdleHVsdTogJXNcbicgIiQqIiA+JjI7IGV4aXQgMTsgfQppbmZvKCkgeyBwcmludGYgJyVzXG4nICIkKiIgPiYyOyB9Cgpqc29uX3N0cigpIHsgIyBqc29uX3N0ciA8a2V5PiA8ZmlsZT4g4oCUIGZsYXQgImtleSI6InZhbHVlIgogIHNlZCAtbiAicy8uKlwiJDFcIltbOnNwYWNlOl1dKjpbWzpzcGFjZTpdXSpcIlxcKFteXCJdKlxcKVwiLiovXFwxL3AiICIkMiIgfCBoZWFkIC1uMQp9CgpbIC1mICIkQ09ORklHX0ZJTEUiIF0gfHwgZGllICJub3QgY29uZmlndXJlZCDigJQgcnVuOiBjdXJsIC1mc1NMIDxiYXNlX3VybD4vYXBpL3NraWxscy9pbnN0YWxsLnNoIHwgc2giCgpCQVNFX1VSTD0iJChqc29uX3N0ciBiYXNlX3VybCAiJENPTkZJR19GSUxFIikiCkJBQ0tFTkQ9IiQoanNvbl9zdHIgYmFja2VuZCAiJENPTkZJR19GSUxFIikiCkFQSV9LRVk9IiQoanNvbl9zdHIgYXBpX2tleSAiJENPTkZJR19GSUxFIikiCkxJTktfTU9ERT0iJChqc29uX3N0ciBsaW5rX21vZGUgIiRDT05GSUdfRklMRSIpIgpTQ09QRT0iJChqc29uX3N0ciBzY29wZSAiJENPTkZJR19GSUxFIikiCkNMSUVOVFM9IiQoc2VkIC1uICdzLy4qImNsaWVudHMiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlxbXChbXl1dKlwpXF0uKi9cMS9wJyAiJENPTkZJR19GSUxFIiB8IHRyICcsJyAnICcgfCB0ciAtZCAnIicgfCB0ciAtcyAnICcpIgoKWyAtbiAiJENMSUVOVFMiIF0gfHwgQ0xJRU5UUz0iYWdlbnRzIgpbIC1uICIkTElOS19NT0RFIiBdIHx8IExJTktfTU9ERT0iY29weSIKWyAtbiAiJFNDT1BFIiBdIHx8IFNDT1BFPSJwcm9qZWN0IgoKaWYgWyAteiAiJEJBQ0tFTkQiIF07IHRoZW4KICBbIC1uICIkQkFTRV9VUkwiIF0gfHwgZGllICJjb25maWcgaGFzIG5vIGJhY2tlbmQgYW5kIG5vIGJhc2VfdXJsIgogIEJBQ0tFTkQ9IiQoY3VybCAtZnNTTCAiJEJBU0VfVVJML2FwaS9jb25maWciIHwgc2VkIC1uICdzLy4qImJhY2tlbmQiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKiJcKFteIl0qXCkiLiovXDEvcCcgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJEJBQ0tFTkQiIF0gfHwgZGllICJjb3VsZCBub3QgcmVzb2x2ZSBiYWNrZW5kIGZyb20gJEJBU0VfVVJML2FwaS9jb25maWciCmZpCkJBQ0tFTkQ9IiQocHJpbnRmICclcycgIiRCQUNLRU5EIiB8IHNlZCAnczovKiQ6OicpIgoKUk9PVD0iJFBXRCIKWyAiJFNDT1BFIiA9ICJob21lIiBdICYmIFJPT1Q9IiRIT01FIgoKIyBkaXJfZm9yIENMSUVOVF9JRCAtPiByZWxhdGl2ZSBza2lsbCBkaXIgKGdlbmVyYXRlZCBmcm9tIHRoZSBtYW5pZmVzdCBpbiB0aGUKIyByZWFsIGJ1aWxkOyBhIHJlcHJlc2VudGF0aXZlIHN1YnNldCBoZXJlIGZvciBsb2NhbCB0ZXN0aW5nKS4KZGlyX2ZvcigpIHsKICBjYXNlICIkMSIgaW4KX19ESVJfRk9SX0NBU0VTX18KICAgICopIHJldHVybiAxIDs7CiAgZXNhYwp9CgphcGkoKSB7ICMgYXBpIDxNRVRIT0Q+IDxwYXRoPiBbZXh0cmEgY3VybCBhcmdzLi4uXSAtPiBib2R5IG9uIHN0ZG91dAogIG09IiQxIjsgcD0iJDIiOyBzaGlmdCAyCiAgY3VybCAtZnNTIC1YICIkbSIgIiRCQUNLRU5EJHAiIC1IICJ4LWFwaS1rZXk6IEJlYXJlciAkQVBJX0tFWSIgIiRAIgp9CgptZXRhX3ZlcnNpb24oKSB7ICMgbWV0YV92ZXJzaW9uIDxuYW1lPiAtPiBjdXJyZW50X3ZlcnNpb24gZnJvbSByZWdpc3RyeQogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJDEiIDI+L2Rldi9udWxsIFwKICAgIHwgc2VkIC1uICdzLy4qImN1cnJlbnRfdmVyc2lvbiJbWzpzcGFjZTpdXSo6W1s6c3BhY2U6XV0qXChbMC05XVswLTldKlwpLiovXDEvcCcgfCBoZWFkIC1uMQp9CgpfbWFuYWdlZCgpIHsgIyBfbWFuYWdlZCA8ZGlyPiAtPiAwIGlmIHNhZmUgdG8gb3ZlcndyaXRlIChvdXJzIG9yIHN5bWxpbmsgb3IgYWJzZW50KQogIGQ9IiQxIgogIGlmIFsgLWUgIiRkIiBdICYmIFsgISAtTCAiJGQiIF0gJiYgWyAhIC1mICIkZC8uZXh1bHUtc2tpbGwuanNvbiIgXTsgdGhlbgogICAgaW5mbyAic2tpcCAkZCAoZXhpc3RzLCBub3QgbWFuYWdlZCBieSBleHVsdSkiOyByZXR1cm4gMQogIGZpCiAgcmV0dXJuIDAKfQoKX3B1dF9yZWFsKCkgeyAjIF9wdXRfcmVhbCA8ZGVzdD4gPHNyY2Rpcj4gPG1hcmtlci1qc29uPgogIF9tYW5hZ2VkICIkMSIgfHwgcmV0dXJuIDAKICBybSAtcmYgIiQxIjsgbWtkaXIgLXAgIiQxIjsgY3AgLVIgIiQyLy4iICIkMS8iCiAgcHJpbnRmICclc1xuJyAiJDMiID4gIiQxLy5leHVsdS1za2lsbC5qc29uIgp9CgpwbGFjZV9za2lsbCgpIHsgIyBwbGFjZV9za2lsbCA8bmFtZT4gPHNyY2Rpcj4gPHZlcnNpb24+CiAgcG5hbWU9IiQxIjsgcHNyYz0iJDIiOyBwdmVyPSIkezM6LTF9IgogIG1hcmtlcj0neyAibmFtZSI6ICInIiRwbmFtZSInIiwgInZlcnNpb24iOiAnIiRwdmVyIicsICJzb3VyY2UiOiAiJyIkQkFDS0VORCInIiB9JwogIGNhbm9uPSIkUk9PVC8uYWdlbnRzL3NraWxscy8kcG5hbWUiCiAgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBfcHV0X3JlYWwgIiRjYW5vbiIgIiRwc3JjIiAiJG1hcmtlciIKICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICBkPSIkKGRpcl9mb3IgIiRpZCIpIiB8fCB7IGluZm8gInVua25vd24gY2xpZW50OiAkaWQiOyBjb250aW51ZTsgfQogICAgcGFyZW50PSIkUk9PVC8kZCI7IGRlc3Q9IiRwYXJlbnQvJHBuYW1lIgogICAgaWYgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBbICIkaWQiICE9ICJhZ2VudHMiIF07IHRoZW4KICAgICAgX21hbmFnZWQgIiRkZXN0IiB8fCBjb250aW51ZQogICAgICBta2RpciAtcCAiJHBhcmVudCI7IHJtIC1yZiAiJGRlc3QiCiAgICAgIGlmIGxuIC1zICIkY2Fub24iICIkZGVzdCIgMj4vZGV2L251bGw7IHRoZW4KICAgICAgICBpbmZvICJsaW5rZWQgJGRlc3QgLT4gJGNhbm9uIgogICAgICBlbHNlCiAgICAgICAgaW5mbyAic3ltbGluayB1bnN1cHBvcnRlZCBhdCAkZGVzdDsgY29weWluZyIKICAgICAgICBfcHV0X3JlYWwgIiRkZXN0IiAiJHBzcmMiICIkbWFya2VyIgogICAgICBmaQogICAgZWxpZiBbICIkTElOS19NT0RFIiA9ICJjb3B5IiBdOyB0aGVuCiAgICAgIF9wdXRfcmVhbCAiJGRlc3QiICIkcHNyYyIgIiRtYXJrZXIiCiAgICBmaQogICAgIyBzeW1saW5rICsgYWdlbnRzOiBhbHJlYWR5IHBsYWNlZCBhcyB0aGUgY2Fub25pY2FsIHN0b3JlIGFib3ZlLgogIGRvbmUKfQoKaW5zdGFsbGVkX25hbWVzKCkgeyAjIHVuaXF1ZSBza2lsbCBuYW1lcyB0aGF0IGNhcnJ5IG91ciBtYXJrZXIgdW5kZXIgUk9PVAogIHsgZmluZCAiJFJPT1QvLmFnZW50cy9za2lsbHMiIC1tYXhkZXB0aCAyIC1uYW1lIC5leHVsdS1za2lsbC5qc29uIDI+L2Rldi9udWxsCiAgICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICAgIGQ9IiQoZGlyX2ZvciAiJGlkIikiIHx8IGNvbnRpbnVlCiAgICAgIGZpbmQgIiRST09ULyRkIiAtbWF4ZGVwdGggMiAtbmFtZSAuZXh1bHUtc2tpbGwuanNvbiAyPi9kZXYvbnVsbAogICAgZG9uZQogIH0gfCB3aGlsZSByZWFkIC1yIG07IGRvIGpzb25fc3RyIG5hbWUgIiRtIjsgZG9uZSB8IHNvcnQgLXUKfQoKbWFya2VyX3ZlcnNpb24oKSB7ICMgbWFya2VyX3ZlcnNpb24gPG5hbWU+CiAgZm9yIGJhc2UgaW4gIiRST09ULy5hZ2VudHMvc2tpbGxzIjsgZG8KICAgIFsgLWYgIiRiYXNlLyQxLy5leHVsdS1za2lsbC5qc29uIiBdICYmIHsganNvbl9zdHIgdmVyc2lvbiAiJGJhc2UvJDEvLmV4dWx1LXNraWxsLmpzb24iOyByZXR1cm47IH0KICBkb25lCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgY29udGludWUKICAgIGY9IiRST09ULyRkLyQxLy5leHVsdS1za2lsbC5qc29uIgogICAgWyAtZiAiJGYiIF0gJiYgeyBqc29uX3N0ciB2ZXJzaW9uICIkZiI7IHJldHVybjsgfQogIGRvbmUKfQoKZG9faW5zdGFsbCgpIHsgIyBkb19pbnN0YWxsIDxuYW1lPgogIG5hbWU9IiQxIgogIFRNUD0iJChta3RlbXAgLWQpIgogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJG5hbWUvZG93bmxvYWQiIC0tb3V0cHV0ICIkVE1QL3NraWxsLnppcCIgXAogICAgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImRvd25sb2FkIGZhaWxlZCBmb3IgJyRuYW1lJyAoNDAzID0gbm8gYWNjZXNzLCA0MDQgPSB1bmtub3duKSI7IH0KICBta2RpciAtcCAiJFRNUC94IgogIHVuemlwIC1xICIkVE1QL3NraWxsLnppcCIgLWQgIiRUTVAveCIgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImJhZCBhcmNoaXZlIGZvciAnJG5hbWUnIjsgfQogIHNyYz0iJChmaW5kICIkVE1QL3giIC1taW5kZXB0aCAxIC1tYXhkZXB0aCAxIC10eXBlIGQgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJHNyYyIgXSB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAidW5leHBlY3RlZCBhcmNoaXZlIGxheW91dCBmb3IgJyRuYW1lJyI7IH0KICB2ZXI9IiQobWV0YV92ZXJzaW9uICIkbmFtZSIpIgogIHBsYWNlX3NraWxsICIkbmFtZSIgIiRzcmMiICIke3ZlcjotMX0iCiAgcm0gLXJmICIkVE1QIgogIGluZm8gImluc3RhbGxlZCAkbmFtZSAodiR7dmVyOi0xfSkgWyRMSU5LX01PREVdIGludG86ICRDTElFTlRTIgp9Cgp1c2FnZSgpIHsKICBjYXQgPiYyIDw8RU9GCmV4dWx1IOKAlCBFeHVsdSBza2lsbCBsaWJyYXJ5IGhlbHBlcgogIGV4dWx1IGxpc3QgICAgICAgICAgICAgICAgIGxpc3Qgc2tpbGxzIHlvdSBjYW4gYWNjZXNzIChKU09OKQogIGV4dWx1IGdldCA8bmFtZT4gICAgICAgICAgIHNob3cgb25lIHNraWxsJ3MgbWV0YWRhdGEgKEpTT04pCiAgZXh1bHUgaW5zdGFsbCA8bmFtZT4gICAgICAgaW5zdGFsbC9yZWZyZXNoIGEgc2tpbGwgaW50byB5b3VyIGFnZW50IGNsaWVudHMKICBleHVsdSB1cGRhdGUgWzxuYW1lPl0gICAgICB1cGRhdGUgaW5zdGFsbGVkIHNraWxscyAoYWxsLCBvciBvbmUpIHRvIGxhdGVzdAogIGV4dWx1IHB1Ymxpc2ggPG5hbWU+IDxkaXI+IHB1Ymxpc2ggYSBsb2NhbCBza2lsbCBmb2xkZXIgYXMgPG5hbWU+CiAgZXh1bHUgY29uZmlnICAgICAgICAgICAgICAgc2hvdyByZXNvbHZlZCBiYWNrZW5kIC8gc2NvcGUgLyBjbGllbnRzIChubyBzZWNyZXRzKQpFT0YKfQoKY21kPSIkezE6LWhlbHB9IgpbICQjIC1ndCAwIF0gJiYgc2hpZnQgfHwgdHJ1ZQoKY2FzZSAiJGNtZCIgaW4KICBsaXN0KSBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5IiA7OwogIGdldCkgWyAkIyAtZ2UgMSBdIHx8IGRpZSAidXNhZ2U6IGV4dWx1IGdldCA8bmFtZT4iOyBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyQxIiA7OwogIGluc3RhbGwpIFsgJCMgLWdlIDEgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBpbnN0YWxsIDxuYW1lPiI7IGRvX2luc3RhbGwgIiQxIiA7OwogIHVwZGF0ZSkKICAgIGlmIFsgJCMgLWdlIDEgXTsgdGhlbiBuYW1lcz0iJDEiOyBlbHNlIG5hbWVzPSIkKGluc3RhbGxlZF9uYW1lcykiOyBmaQogICAgWyAtbiAiJG5hbWVzIiBdIHx8IHsgaW5mbyAibm8gZXh1bHUtbWFuYWdlZCBza2lsbHMgZm91bmQgdW5kZXIgJFJPT1QiOyBleGl0IDA7IH0KICAgIGZvciBuIGluICRuYW1lczsgZG8KICAgICAgY3VyPSIkKG1hcmtlcl92ZXJzaW9uICIkbiIpIgogICAgICBsYXRlc3Q9IiQobWV0YV92ZXJzaW9uICIkbiIpIgogICAgICBbIC1uICIkbGF0ZXN0IiBdIHx8IHsgaW5mbyAic2tpcCAkbiAobm90IGluIHJlZ2lzdHJ5KSI7IGNvbnRpbnVlOyB9CiAgICAgIGlmIFsgLXogIiRjdXIiIF0gfHwgWyAiJGxhdGVzdCIgLWd0ICIkY3VyIiBdIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAidXBkYXRpbmcgJG46IHYke2N1cjotP30gLT4gdiRsYXRlc3QiOyBkb19pbnN0YWxsICIkbiIKICAgICAgZWxzZQogICAgICAgIGluZm8gIiRuIHVwIHRvIGRhdGUgKHYkY3VyKSIKICAgICAgZmkKICAgIGRvbmUKICAgIDs7CiAgcHVibGlzaCkKICAgIFsgJCMgLWdlIDIgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBwdWJsaXNoIDxuYW1lPiA8ZGlyPiIKICAgIG5hbWU9IiQxIjsgZm9sZGVyPSIkMiIKICAgIFsgLWQgIiRmb2xkZXIiIF0gfHwgZGllICJubyBzdWNoIGZvbGRlcjogJGZvbGRlciIKICAgIFsgLWYgIiRmb2xkZXIvU0tJTEwubWQiIF0gfHwgZGllICIkZm9sZGVyIGhhcyBubyBTS0lMTC5tZCBhdCBpdHMgcm9vdCIKICAgIGNvbW1hbmQgLXYgemlwID4vZGV2L251bGwgMj4mMSB8fCBkaWUgInRoZSAnemlwJyBjb21tYW5kIGlzIHJlcXVpcmVkIHRvIHB1Ymxpc2giCiAgICBUTVA9IiQobWt0ZW1wIC1kKSIKICAgICggY2QgIiQoZGlybmFtZSAiJGZvbGRlciIpIiBcCiAgICAgICYmIHppcCAtcSAtciAtWCAiJFRNUC9za2lsbC56aXAiICIkKGJhc2VuYW1lICIkZm9sZGVyIikiIFwKICAgICAgICAgICAteCAnKi8uZXh1bHUtc2tpbGwuanNvbicgJyovLmdpdC8qJyAnKi5EU19TdG9yZScgJyovX19NQUNPU1gvKicgKSBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJjb3VsZCBub3QgemlwICRmb2xkZXIiOyB9CiAgICBhcGkgUE9TVCAiL3NraWxscy9yZWdpc3RyeS8kbmFtZSIgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vemlwIiBcCiAgICAgIC0tZGF0YS1iaW5hcnkgQCIkVE1QL3NraWxsLnppcCIgXAogICAgICB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAicHVibGlzaCBmYWlsZWQgKDQwMyA9IG5vIHdyaXRlIGFjY2VzcywgNDA5ID0gbmFtZSB0YWtlbikiOyB9CiAgICBybSAtcmYgIiRUTVAiCiAgICBpbmZvICJwdWJsaXNoZWQgJG5hbWUiCiAgICA7OwogIGNvbmZpZykKICAgIHByaW50ZiAnYmFja2VuZD0lc1xuc2NvcGU9JXNcbmxpbmtfbW9kZT0lc1xuY2xpZW50cz0lc1xuYXBpX2tleT0lc1xuJyBcCiAgICAgICIkQkFDS0VORCIgIiRTQ09QRSIgIiRMSU5LX01PREUiICIkQ0xJRU5UUyIgXAogICAgICAiJChbIC1uICIkQVBJX0tFWSIgXSAmJiBlY2hvIHNldCB8fCBlY2hvIE1JU1NJTkcpIgogICAgOzsKICBoZWxwfC0taGVscHwtaCkgdXNhZ2UgOzsKICAqKSBpbmZvICJ1bmtub3duIGNvbW1hbmQ6ICRjbWQiOyB1c2FnZTsgZXhpdCAyIDs7CmVzYWMK";
|
|
13890
|
+
|
|
13891
|
+
// src/skills/bootstrap/exulu-skills.ts
|
|
13892
|
+
var BOOTSTRAP_CLIENTS_JSON = JSON.stringify(CLIENT_MANIFEST, null, 2);
|
|
13893
|
+
var DIR_FOR_CASES = CLIENT_MANIFEST.map(
|
|
13894
|
+
(c) => ` ${c.id}) printf '%s' '${c.dir}' ;;`
|
|
13895
|
+
).join("\n");
|
|
13896
|
+
var BOOTSTRAP_EXULU_SH = Buffer.from(EXULU_SH_B64, "base64").toString("utf8").replace("__DIR_FOR_CASES__", DIR_FOR_CASES);
|
|
13897
|
+
var BOOTSTRAP_SKILL_MD = `---
|
|
13898
|
+
name: exulu-skills
|
|
13899
|
+
description: Install, update, and publish skills from this Exulu instance's central skill library. Use when the user asks to install a skill, get the latest version of a skill, list available skills, or publish a skill to Exulu.
|
|
13900
|
+
---
|
|
13901
|
+
|
|
13902
|
+
# Exulu Skills
|
|
13903
|
+
|
|
13904
|
+
Bridge to the Exulu central skill library. **All operations go through the
|
|
13905
|
+
bundled helper script \u2014 do not hand-write curl or copy files yourself.** The
|
|
13906
|
+
script reads the API token from config (keeping it out of this conversation) and
|
|
13907
|
+
handles the multi-client copy/symlink fan-out deterministically.
|
|
13908
|
+
|
|
13909
|
+
## The helper
|
|
13910
|
+
|
|
13911
|
+
Run the script next to this file, \`scripts/exulu\`, with \`sh\` and the absolute
|
|
13912
|
+
path of this skill's directory:
|
|
13913
|
+
|
|
13914
|
+
\`\`\`
|
|
13915
|
+
sh "<this-skill-dir>/scripts/exulu" <command>
|
|
13916
|
+
\`\`\`
|
|
13917
|
+
|
|
13918
|
+
Commands:
|
|
13919
|
+
- \`list\` \u2014 skills you can access (JSON on stdout)
|
|
13920
|
+
- \`get <name>\` \u2014 one skill's metadata (JSON)
|
|
13921
|
+
- \`install <name>\` \u2014 install/refresh a skill into the user's agent clients
|
|
13922
|
+
- \`update [<name>]\` \u2014 update every installed skill, or just \`<name>\`, to latest
|
|
13923
|
+
- \`publish <name> <folder>\` \u2014 publish a local skill folder as \`<name>\`
|
|
13924
|
+
- \`config\` \u2014 show resolved backend / scope / clients (prints no secrets)
|
|
13925
|
+
|
|
13926
|
+
The token, backend URL, target clients, copy-vs-symlink mode, and scope all come
|
|
13927
|
+
from \`~/.config/exulu/skills.json\` (written by the installer). Never print the
|
|
13928
|
+
\`api_key\` or read it into your reply \u2014 the script uses it internally.
|
|
13929
|
+
|
|
13930
|
+
## Requests \u2192 commands
|
|
13931
|
+
|
|
13932
|
+
- "list / search skills" \u2192 \`exulu list\`, then filter the JSON for the user.
|
|
13933
|
+
- "install skill X" / "add the X skill" \u2192 \`exulu install X\`.
|
|
13934
|
+
- "update / get the latest version [of X]" \u2192 \`exulu update [X]\`.
|
|
13935
|
+
- "publish / upload this skill as X" \u2192 confirm the target name with the user; for
|
|
13936
|
+
an existing skill run \`exulu get X\` first and confirm a new version is intended;
|
|
13937
|
+
then \`exulu publish X <folder>\`.
|
|
13938
|
+
|
|
13939
|
+
## Not configured yet?
|
|
13940
|
+
|
|
13941
|
+
If \`exulu config\` reports it's not configured (or \`~/.config/exulu/skills.json\`
|
|
13942
|
+
is missing), tell the user to run the installer \u2014 it sets everything up
|
|
13943
|
+
interactively (base URL, API key, target clients, copy/symlink):
|
|
13944
|
+
|
|
13945
|
+
\`\`\`
|
|
13946
|
+
curl -fsSL <base_url>/api/skills/install.sh | sh
|
|
13947
|
+
\`\`\`
|
|
13948
|
+
|
|
13949
|
+
\`<base_url>\` is their Exulu frontend URL (e.g. https://ai.open.de). They can
|
|
13950
|
+
create an API key at \`<base_url>/token\`.
|
|
13951
|
+
|
|
13952
|
+
## Errors
|
|
13953
|
+
|
|
13954
|
+
- install: \`403\` = no access to that skill; \`404\` = unknown name.
|
|
13955
|
+
- publish: \`403\` = you can see it but lack write access; \`409\` = the name is
|
|
13956
|
+
taken by a skill you can't access.
|
|
13957
|
+
`;
|
|
13958
|
+
|
|
13450
13959
|
// src/exulu/routes.ts
|
|
13451
13960
|
var REQUEST_SIZE_LIMIT = "50mb";
|
|
13452
13961
|
var getExuluVersionNumber = async () => {
|
|
@@ -13520,6 +14029,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
|
|
|
13520
14029
|
}
|
|
13521
14030
|
next();
|
|
13522
14031
|
});
|
|
14032
|
+
const rawZip = express2.raw({ type: ["application/zip", "application/octet-stream", "application/x-zip-compressed", "application/x-zip"], limit: "50mb" });
|
|
13523
14033
|
console.log(`
|
|
13524
14034
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
|
|
13525
14035
|
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
@@ -13745,7 +14255,7 @@ Mood: friendly and intelligent.
|
|
|
13745
14255
|
});
|
|
13746
14256
|
return;
|
|
13747
14257
|
}
|
|
13748
|
-
const uuid =
|
|
14258
|
+
const uuid = randomUUID3();
|
|
13749
14259
|
const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
|
|
13750
14260
|
contentType: "image/png"
|
|
13751
14261
|
}, authenticationResult.user?.id, void 0, true);
|
|
@@ -13949,6 +14459,10 @@ Mood: friendly and intelligent.
|
|
|
13949
14459
|
const providerapikey = resolved.apiKey;
|
|
13950
14460
|
const resolvedLanguageModel = resolved.languageModel;
|
|
13951
14461
|
const resolvedModelId = resolved.model.id;
|
|
14462
|
+
const contextWindow = await resolveContextWindow({
|
|
14463
|
+
modelId: resolved.model.id,
|
|
14464
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
14465
|
+
});
|
|
13952
14466
|
if (!!headers.stream) {
|
|
13953
14467
|
const statistics = {
|
|
13954
14468
|
label: agent.name,
|
|
@@ -13967,27 +14481,46 @@ Mood: friendly and intelligent.
|
|
|
13967
14481
|
const instructions = customInstructions ? `${agent.instructions}
|
|
13968
14482
|
|
|
13969
14483
|
${customInstructions}` : agent.instructions;
|
|
13970
|
-
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
14484
|
+
if (headers.session) markStreamActive(headers.session);
|
|
14485
|
+
let result;
|
|
14486
|
+
try {
|
|
14487
|
+
result = await provider.generateStream({
|
|
14488
|
+
contexts,
|
|
14489
|
+
agent,
|
|
14490
|
+
user,
|
|
14491
|
+
instructions,
|
|
14492
|
+
session: headers.session,
|
|
14493
|
+
message,
|
|
14494
|
+
previousMessages,
|
|
14495
|
+
currentTools: enabledTools,
|
|
14496
|
+
currentSkills: enabledSkills,
|
|
14497
|
+
approvedTools,
|
|
14498
|
+
allExuluTools: tools,
|
|
14499
|
+
languageModel: resolvedLanguageModel,
|
|
14500
|
+
providerapikey,
|
|
14501
|
+
toolConfigs: agent.tools,
|
|
14502
|
+
exuluConfig: config,
|
|
14503
|
+
req,
|
|
14504
|
+
contextWindow,
|
|
14505
|
+
disabledTools
|
|
14506
|
+
});
|
|
14507
|
+
} catch (err) {
|
|
14508
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
14509
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
14510
|
+
res.status(413).send(err.message);
|
|
14511
|
+
return;
|
|
14512
|
+
}
|
|
14513
|
+
throw err;
|
|
14514
|
+
}
|
|
13988
14515
|
result.stream.consumeStream();
|
|
13989
14516
|
result.stream.pipeUIMessageStreamToResponse(res, {
|
|
13990
14517
|
messageMetadata: ({ part }) => {
|
|
14518
|
+
if (part.type === "finish-step") {
|
|
14519
|
+
return {
|
|
14520
|
+
lastStepInputTokens: part.usage.inputTokens,
|
|
14521
|
+
lastStepOutputTokens: part.usage.outputTokens
|
|
14522
|
+
};
|
|
14523
|
+
}
|
|
13991
14524
|
if (part.type === "finish") {
|
|
13992
14525
|
return {
|
|
13993
14526
|
totalTokens: part.totalUsage.totalTokens,
|
|
@@ -14004,22 +14537,20 @@ ${customInstructions}` : agent.instructions;
|
|
|
14004
14537
|
sendSources: true,
|
|
14005
14538
|
onError: (error) => {
|
|
14006
14539
|
console.error("[EXULU] chat response error.", error);
|
|
14007
|
-
if (
|
|
14008
|
-
|
|
14009
|
-
|
|
14010
|
-
if (typeof error === "string")
|
|
14011
|
-
|
|
14012
|
-
|
|
14013
|
-
|
|
14014
|
-
return error.message;
|
|
14015
|
-
}
|
|
14016
|
-
return JSON.stringify(error);
|
|
14540
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
14541
|
+
let message2;
|
|
14542
|
+
if (error == null) message2 = "unknown error";
|
|
14543
|
+
else if (typeof error === "string") message2 = error;
|
|
14544
|
+
else if (error instanceof Error) message2 = error.message;
|
|
14545
|
+
else message2 = JSON.stringify(error);
|
|
14546
|
+
return mapStreamErrorMessage(message2);
|
|
14017
14547
|
},
|
|
14018
14548
|
generateMessageId: createIdGenerator({
|
|
14019
14549
|
prefix: "msg_",
|
|
14020
14550
|
size: 16
|
|
14021
14551
|
}),
|
|
14022
14552
|
onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
|
|
14553
|
+
if (headers.session) clearStreamActive(headers.session);
|
|
14023
14554
|
console.log(
|
|
14024
14555
|
"[EXULU] onFinish",
|
|
14025
14556
|
messages?.map((msg) => msg.parts?.map((part) => part.type === "text" ? part.text : null)).join("\n")
|
|
@@ -14081,33 +14612,129 @@ ${customInstructions}` : agent.instructions;
|
|
|
14081
14612
|
const instructions = customInstructions ? `${agent.instructions}
|
|
14082
14613
|
|
|
14083
14614
|
${customInstructions}` : agent.instructions;
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14615
|
+
let response;
|
|
14616
|
+
try {
|
|
14617
|
+
response = await provider.generateSync({
|
|
14618
|
+
contexts,
|
|
14619
|
+
agent,
|
|
14620
|
+
user,
|
|
14621
|
+
req,
|
|
14622
|
+
instructions,
|
|
14623
|
+
session: headers.session,
|
|
14624
|
+
inputMessages: [req.body.message],
|
|
14625
|
+
currentTools: enabledTools,
|
|
14626
|
+
currentSkills: enabledSkills,
|
|
14627
|
+
allExuluTools: tools,
|
|
14628
|
+
languageModel: resolvedLanguageModel,
|
|
14629
|
+
providerapikey,
|
|
14630
|
+
exuluConfig: config,
|
|
14631
|
+
toolConfigs: agent.tools,
|
|
14632
|
+
contextWindow,
|
|
14633
|
+
disabledTools,
|
|
14634
|
+
statistics: {
|
|
14635
|
+
label: agent.name,
|
|
14636
|
+
trigger: "agent"
|
|
14637
|
+
},
|
|
14638
|
+
onTokenUsage: async ({ inputTokens, outputTokens }) => {
|
|
14639
|
+
}
|
|
14640
|
+
});
|
|
14641
|
+
} catch (err) {
|
|
14642
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
14643
|
+
res.status(413).send(err.message);
|
|
14644
|
+
return;
|
|
14104
14645
|
}
|
|
14105
|
-
|
|
14646
|
+
throw err;
|
|
14647
|
+
}
|
|
14106
14648
|
res.status(200).json(response);
|
|
14107
14649
|
return;
|
|
14108
14650
|
}
|
|
14109
14651
|
});
|
|
14110
14652
|
};
|
|
14653
|
+
const registerAgentCompactRoute = (slug) => {
|
|
14654
|
+
app.post(slug + "/:instance", async (req, res) => {
|
|
14655
|
+
const instance = req.params.instance;
|
|
14656
|
+
if (!instance) {
|
|
14657
|
+
res.status(400).json({ message: "Missing instance in request." });
|
|
14658
|
+
return;
|
|
14659
|
+
}
|
|
14660
|
+
const sessionID = req.headers["session"] || null;
|
|
14661
|
+
if (!sessionID) {
|
|
14662
|
+
res.status(400).json({ message: "Missing session header." });
|
|
14663
|
+
return;
|
|
14664
|
+
}
|
|
14665
|
+
const agent = await exuluApp.get().agent(instance);
|
|
14666
|
+
if (!agent) {
|
|
14667
|
+
res.status(404).json({ message: "Agent with id " + instance + " not found." });
|
|
14668
|
+
return;
|
|
14669
|
+
}
|
|
14670
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
14671
|
+
if (!authenticationResult.user?.id) {
|
|
14672
|
+
res.status(authenticationResult.code || 401).json({ detail: `${authenticationResult.message}` });
|
|
14673
|
+
return;
|
|
14674
|
+
}
|
|
14675
|
+
const user = authenticationResult.user;
|
|
14676
|
+
const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
|
|
14677
|
+
if (!hasAccessToAgent) {
|
|
14678
|
+
res.status(401).json({ message: "You don't have access to this agent." });
|
|
14679
|
+
return;
|
|
14680
|
+
}
|
|
14681
|
+
const { db } = await postgresClient();
|
|
14682
|
+
const sessionRow = await db.from("agent_sessions").where({ id: sessionID }).first();
|
|
14683
|
+
if (!sessionRow) {
|
|
14684
|
+
res.status(404).json({ message: "Session not found for session ID: " + sessionID });
|
|
14685
|
+
return;
|
|
14686
|
+
}
|
|
14687
|
+
const hasAccessToSession = await checkRecordAccess(sessionRow, "write", user);
|
|
14688
|
+
if (!hasAccessToSession) {
|
|
14689
|
+
res.status(401).json({ message: "You don't have access to this session." });
|
|
14690
|
+
return;
|
|
14691
|
+
}
|
|
14692
|
+
if (isStreamActive(sessionID)) {
|
|
14693
|
+
res.status(409).json({ message: "A response is still streaming for this session \u2014 try again when it finishes." });
|
|
14694
|
+
return;
|
|
14695
|
+
}
|
|
14696
|
+
const overrideModelId = req.headers["x-exulu-model-override"];
|
|
14697
|
+
const modelId = overrideModelId ?? agent.model;
|
|
14698
|
+
if (!modelId) {
|
|
14699
|
+
res.status(400).json({ message: `Agent ${agent.name} (${agent.id}) has no model configured.` });
|
|
14700
|
+
return;
|
|
14701
|
+
}
|
|
14702
|
+
let resolved;
|
|
14703
|
+
try {
|
|
14704
|
+
resolved = await resolveModel({ modelId, user, providers, agent });
|
|
14705
|
+
} catch (err) {
|
|
14706
|
+
if (err instanceof ResolveModelError) {
|
|
14707
|
+
const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
|
|
14708
|
+
res.status(status).json({ message: err.message, code: err.code });
|
|
14709
|
+
return;
|
|
14710
|
+
}
|
|
14711
|
+
throw err;
|
|
14712
|
+
}
|
|
14713
|
+
const contextWindow = await resolveContextWindow({
|
|
14714
|
+
modelId: resolved.model.id,
|
|
14715
|
+
exuluProvider: isLiteLLMEnabled() ? void 0 : resolved.exuluProvider
|
|
14716
|
+
});
|
|
14717
|
+
const steer = typeof req.body?.steer === "string" ? req.body.steer : void 0;
|
|
14718
|
+
try {
|
|
14719
|
+
const result = await compactSession({
|
|
14720
|
+
sessionID,
|
|
14721
|
+
user,
|
|
14722
|
+
languageModel: resolved.languageModel,
|
|
14723
|
+
contextWindow,
|
|
14724
|
+
steer,
|
|
14725
|
+
modelId: resolved.model.id
|
|
14726
|
+
});
|
|
14727
|
+
res.json(result);
|
|
14728
|
+
} catch (err) {
|
|
14729
|
+
if (err instanceof CompactionInsufficientError) {
|
|
14730
|
+
res.status(422).send(err.message);
|
|
14731
|
+
return;
|
|
14732
|
+
}
|
|
14733
|
+
console.error("[EXULU] compactSession failed.", err);
|
|
14734
|
+
res.status(500).json({ message: err instanceof Error ? err.message : "Compaction failed." });
|
|
14735
|
+
}
|
|
14736
|
+
});
|
|
14737
|
+
};
|
|
14111
14738
|
providers.forEach((provider) => {
|
|
14112
14739
|
const slug = provider.slug;
|
|
14113
14740
|
if (!slug) return;
|
|
@@ -14116,6 +14743,14 @@ ${customInstructions}` : agent.instructions;
|
|
|
14116
14743
|
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
14117
14744
|
registerAgentRunRoute("/agents/litellm/run", providers[0]);
|
|
14118
14745
|
}
|
|
14746
|
+
providers.forEach((provider) => {
|
|
14747
|
+
const slug = provider.slug;
|
|
14748
|
+
if (!slug) return;
|
|
14749
|
+
registerAgentCompactRoute(slug.replace(/\/run$/, "/compact"));
|
|
14750
|
+
});
|
|
14751
|
+
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
14752
|
+
registerAgentCompactRoute("/agents/litellm/compact");
|
|
14753
|
+
}
|
|
14119
14754
|
app.post("/agents/suggestions/:agentId", async (req, res) => {
|
|
14120
14755
|
const agentId = req.params.agentId;
|
|
14121
14756
|
if (!agentId) {
|
|
@@ -14467,7 +15102,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
14467
15102
|
const keys = [];
|
|
14468
15103
|
const revisedPrompts = [];
|
|
14469
15104
|
for (const img of images) {
|
|
14470
|
-
const filename = `${
|
|
15105
|
+
const filename = `${randomUUID3()}.${img.extension}`;
|
|
14471
15106
|
const key = `sessions/${sessionId}/images/${toolCallId}/${filename}`;
|
|
14472
15107
|
const fullKey = await uploadFile(
|
|
14473
15108
|
img.buffer,
|
|
@@ -14755,7 +15390,7 @@ ${style.markdown}` : params.prompt;
|
|
|
14755
15390
|
(d) => `- ${d.presignedUrl} (prompt: "${d.prompt}", model: ${d.model}${d.styleName ? `, style: ${d.styleName}` : ""})`
|
|
14756
15391
|
);
|
|
14757
15392
|
const messageText = "The user generated and selected the following image(s) in this chat:\n" + lines.join("\n");
|
|
14758
|
-
const messageId =
|
|
15393
|
+
const messageId = randomUUID3();
|
|
14759
15394
|
const uiMessage = {
|
|
14760
15395
|
id: messageId,
|
|
14761
15396
|
role: "system",
|
|
@@ -15047,7 +15682,9 @@ ${style.markdown}` : params.prompt;
|
|
|
15047
15682
|
const budget_duration = String(body?.budget_duration ?? "");
|
|
15048
15683
|
if (!Number.isFinite(max_budget) || max_budget <= 0) return null;
|
|
15049
15684
|
if (!BUDGET_ALLOWED_DURATIONS.has(budget_duration)) return null;
|
|
15050
|
-
|
|
15685
|
+
const reset = parseResetAt(body?.budget_reset_at);
|
|
15686
|
+
if (!reset.valid) return null;
|
|
15687
|
+
return { max_budget, budget_duration, budget_reset_at: reset.value };
|
|
15051
15688
|
};
|
|
15052
15689
|
const parseBudgetSettingsBody = (body) => {
|
|
15053
15690
|
if (!body || typeof body !== "object") return null;
|
|
@@ -15117,7 +15754,7 @@ ${style.markdown}` : params.prompt;
|
|
|
15117
15754
|
}
|
|
15118
15755
|
const body = parseBudgetBody(req.body);
|
|
15119
15756
|
if (!body) {
|
|
15120
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
15757
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
15121
15758
|
return;
|
|
15122
15759
|
}
|
|
15123
15760
|
const entityIds = Array.isArray(req.body?.entityIds) ? req.body.entityIds : [];
|
|
@@ -15129,7 +15766,7 @@ ${style.markdown}` : params.prompt;
|
|
|
15129
15766
|
for (const id of entityIds) {
|
|
15130
15767
|
const tag = budgetTagFor(entityType, id);
|
|
15131
15768
|
try {
|
|
15132
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
15769
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
15133
15770
|
results.push({ entityId: String(id), ok: true });
|
|
15134
15771
|
} catch (err) {
|
|
15135
15772
|
results.push({
|
|
@@ -15154,12 +15791,12 @@ ${style.markdown}` : params.prompt;
|
|
|
15154
15791
|
}
|
|
15155
15792
|
const body = parseBudgetBody(req.body);
|
|
15156
15793
|
if (!body) {
|
|
15157
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
15794
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
15158
15795
|
return;
|
|
15159
15796
|
}
|
|
15160
15797
|
const tag = budgetTagFor(entityType, req.params.entityId ?? "");
|
|
15161
15798
|
try {
|
|
15162
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
15799
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
15163
15800
|
const info = await tagInfo([tag]);
|
|
15164
15801
|
res.status(200).json({ budget: info[tag] ?? null });
|
|
15165
15802
|
} catch (err) {
|
|
@@ -15622,6 +16259,199 @@ ${style.markdown}` : params.prompt;
|
|
|
15622
16259
|
}
|
|
15623
16260
|
return root;
|
|
15624
16261
|
}
|
|
16262
|
+
app.get("/skills/agent/bootstrap", async (_req, res) => {
|
|
16263
|
+
try {
|
|
16264
|
+
const zip = new JSZip3();
|
|
16265
|
+
zip.file("exulu-skills/SKILL.md", BOOTSTRAP_SKILL_MD);
|
|
16266
|
+
zip.file("exulu-skills/references/clients.json", BOOTSTRAP_CLIENTS_JSON);
|
|
16267
|
+
zip.file("exulu-skills/scripts/exulu", BOOTSTRAP_EXULU_SH, {
|
|
16268
|
+
unixPermissions: 493
|
|
16269
|
+
});
|
|
16270
|
+
const buffer = await zip.generateAsync({
|
|
16271
|
+
type: "nodebuffer",
|
|
16272
|
+
platform: "UNIX"
|
|
16273
|
+
});
|
|
16274
|
+
res.setHeader("Content-Type", "application/zip");
|
|
16275
|
+
res.setHeader("Content-Disposition", 'attachment; filename="exulu-skills.zip"');
|
|
16276
|
+
res.send(buffer);
|
|
16277
|
+
} catch (err) {
|
|
16278
|
+
console.error("[SKILLS] Failed to build bootstrap zip", err);
|
|
16279
|
+
res.status(500).json({ detail: "Failed to build bootstrap skill." });
|
|
16280
|
+
}
|
|
16281
|
+
});
|
|
16282
|
+
app.get("/skills/registry", async (req, res) => {
|
|
16283
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16284
|
+
if (!authResult.user?.id) {
|
|
16285
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16286
|
+
return;
|
|
16287
|
+
}
|
|
16288
|
+
const { db } = await postgresClient();
|
|
16289
|
+
const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
|
|
16290
|
+
const all = await db("skills").select("*");
|
|
16291
|
+
const readable = await filterReadableSkills(db, all, authResult.user);
|
|
16292
|
+
const skills = readable.filter((s) => {
|
|
16293
|
+
if (!tag) return true;
|
|
16294
|
+
const tags = Array.isArray(s.tags) ? s.tags : [];
|
|
16295
|
+
return tags.includes(tag);
|
|
16296
|
+
}).map((s) => ({
|
|
16297
|
+
name: s.name,
|
|
16298
|
+
description: s.description ?? "",
|
|
16299
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
16300
|
+
current_version: s.current_version ?? 1,
|
|
16301
|
+
updated_at: s.updatedAt ?? s.updated_at ?? null
|
|
16302
|
+
}));
|
|
16303
|
+
res.json({ skills });
|
|
16304
|
+
});
|
|
16305
|
+
app.get("/skills/registry/:name/download", async (req, res) => {
|
|
16306
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16307
|
+
if (!authResult.user?.id) {
|
|
16308
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16309
|
+
return;
|
|
16310
|
+
}
|
|
16311
|
+
const { db } = await postgresClient();
|
|
16312
|
+
const skill = await resolveSkillByName(db, req.params.name);
|
|
16313
|
+
if (!skill) {
|
|
16314
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
16315
|
+
return;
|
|
16316
|
+
}
|
|
16317
|
+
if (!await canAccessSkill(db, skill, "read", authResult.user)) {
|
|
16318
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
16319
|
+
return;
|
|
16320
|
+
}
|
|
16321
|
+
const vQuery = req.query.version;
|
|
16322
|
+
const version = !vQuery || vQuery === "latest" ? skill.current_version ?? 1 : Number(vQuery);
|
|
16323
|
+
if (!Number.isFinite(version) || version < 1) {
|
|
16324
|
+
res.status(400).json({ detail: "Invalid version." });
|
|
16325
|
+
return;
|
|
16326
|
+
}
|
|
16327
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
16328
|
+
const versionPrefix = `skills/${skill.id}/v${version}/`;
|
|
16329
|
+
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
16330
|
+
if (files.length === 0) {
|
|
16331
|
+
res.status(404).json({ detail: `Version v${version} has no files.` });
|
|
16332
|
+
return;
|
|
16333
|
+
}
|
|
16334
|
+
const zip = new JSZip3();
|
|
16335
|
+
for (const file of files) {
|
|
16336
|
+
const idx = file.key.indexOf(versionPrefix);
|
|
16337
|
+
const rel = idx >= 0 ? file.key.slice(idx + versionPrefix.length) : file.key;
|
|
16338
|
+
if (!rel) continue;
|
|
16339
|
+
const bytes = await getS3ObjectBytes(file.key, config);
|
|
16340
|
+
zip.file(`${safeName}/${rel}`, bytes);
|
|
16341
|
+
}
|
|
16342
|
+
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
16343
|
+
res.setHeader("Content-Type", "application/zip");
|
|
16344
|
+
res.setHeader("Content-Disposition", `attachment; filename="${safeName}.skill"`);
|
|
16345
|
+
res.send(buffer);
|
|
16346
|
+
});
|
|
16347
|
+
app.post("/skills/registry/:name", rawZip, async (req, res) => {
|
|
16348
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16349
|
+
if (!authResult.user?.id) {
|
|
16350
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16351
|
+
return;
|
|
16352
|
+
}
|
|
16353
|
+
const name = req.params.name;
|
|
16354
|
+
const bytes = req.body;
|
|
16355
|
+
if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
|
|
16356
|
+
res.status(400).json({ detail: "Empty body. Send the skill as a zip/.skill payload." });
|
|
16357
|
+
return;
|
|
16358
|
+
}
|
|
16359
|
+
const { db } = await postgresClient();
|
|
16360
|
+
const existing = await resolveSkillByName(db, name);
|
|
16361
|
+
if (existing) {
|
|
16362
|
+
const canWrite = await canAccessSkill(db, existing, "write", authResult.user);
|
|
16363
|
+
if (canWrite) {
|
|
16364
|
+
const nextVersion = (existing.current_version ?? 1) + 1;
|
|
16365
|
+
try {
|
|
16366
|
+
await extractBundleToVersion({ bytes, skillId: existing.id, version: nextVersion, config });
|
|
16367
|
+
} catch (err) {
|
|
16368
|
+
if (err instanceof BundleValidationError) {
|
|
16369
|
+
res.status(400).json({ detail: err.message });
|
|
16370
|
+
return;
|
|
16371
|
+
}
|
|
16372
|
+
console.error("[SKILLS] publish (new version) failed", err);
|
|
16373
|
+
res.status(500).json({ detail: "Failed to publish new version." });
|
|
16374
|
+
return;
|
|
16375
|
+
}
|
|
16376
|
+
const history = Array.isArray(existing.history) ? existing.history : [];
|
|
16377
|
+
await db("skills").where({ id: existing.id }).update({
|
|
16378
|
+
current_version: nextVersion,
|
|
16379
|
+
history: JSON.stringify([
|
|
16380
|
+
...history,
|
|
16381
|
+
{ version: nextVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
16382
|
+
])
|
|
16383
|
+
});
|
|
16384
|
+
res.json({ name, version: nextVersion, created: false });
|
|
16385
|
+
return;
|
|
16386
|
+
} else {
|
|
16387
|
+
const canRead = await canAccessSkill(db, existing, "read", authResult.user);
|
|
16388
|
+
if (!canRead) {
|
|
16389
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
16390
|
+
return;
|
|
16391
|
+
}
|
|
16392
|
+
res.status(403).json({ detail: "You don't have write access to this skill." });
|
|
16393
|
+
return;
|
|
16394
|
+
}
|
|
16395
|
+
}
|
|
16396
|
+
const meta = await parseSkillFrontmatter(bytes);
|
|
16397
|
+
const skillId = randomUUID3();
|
|
16398
|
+
try {
|
|
16399
|
+
await extractBundleToVersion({ bytes, skillId, version: 1, config });
|
|
16400
|
+
} catch (err) {
|
|
16401
|
+
if (err instanceof BundleValidationError) {
|
|
16402
|
+
res.status(400).json({ detail: err.message });
|
|
16403
|
+
return;
|
|
16404
|
+
}
|
|
16405
|
+
console.error("[SKILLS] publish (create) failed", err);
|
|
16406
|
+
res.status(500).json({ detail: "Failed to publish skill." });
|
|
16407
|
+
return;
|
|
16408
|
+
}
|
|
16409
|
+
try {
|
|
16410
|
+
await db("skills").insert({
|
|
16411
|
+
id: skillId,
|
|
16412
|
+
name,
|
|
16413
|
+
description: meta.description ?? "",
|
|
16414
|
+
s3folder: `skills/${skillId}`,
|
|
16415
|
+
tags: JSON.stringify([]),
|
|
16416
|
+
usage_count: 0,
|
|
16417
|
+
favorite_count: 0,
|
|
16418
|
+
current_version: 1,
|
|
16419
|
+
history: JSON.stringify([
|
|
16420
|
+
{ version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
16421
|
+
]),
|
|
16422
|
+
rights_mode: "private",
|
|
16423
|
+
created_by: authResult.user.id
|
|
16424
|
+
});
|
|
16425
|
+
} catch (err) {
|
|
16426
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
16427
|
+
return;
|
|
16428
|
+
}
|
|
16429
|
+
res.json({ name, version: 1, created: true });
|
|
16430
|
+
});
|
|
16431
|
+
app.get("/skills/registry/:name", async (req, res) => {
|
|
16432
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16433
|
+
if (!authResult.user?.id) {
|
|
16434
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16435
|
+
return;
|
|
16436
|
+
}
|
|
16437
|
+
const { db } = await postgresClient();
|
|
16438
|
+
const skill = await resolveSkillByName(db, req.params.name);
|
|
16439
|
+
if (!skill) {
|
|
16440
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
16441
|
+
return;
|
|
16442
|
+
}
|
|
16443
|
+
if (!await canAccessSkill(db, skill, "read", authResult.user)) {
|
|
16444
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
16445
|
+
return;
|
|
16446
|
+
}
|
|
16447
|
+
res.json({
|
|
16448
|
+
name: skill.name,
|
|
16449
|
+
description: skill.description ?? "",
|
|
16450
|
+
tags: Array.isArray(skill.tags) ? skill.tags : [],
|
|
16451
|
+
current_version: skill.current_version ?? 1,
|
|
16452
|
+
history: Array.isArray(skill.history) ? skill.history : []
|
|
16453
|
+
});
|
|
16454
|
+
});
|
|
15625
16455
|
app.post("/skills/:skillId/init", async (req, res) => {
|
|
15626
16456
|
const authResult = await requestValidators.authenticate(req);
|
|
15627
16457
|
if (!authResult.user?.id) {
|
|
@@ -15669,8 +16499,8 @@ ${style.markdown}` : params.prompt;
|
|
|
15669
16499
|
}
|
|
15670
16500
|
const { skillId } = req.params;
|
|
15671
16501
|
const { extension, contentType } = req.body ?? {};
|
|
15672
|
-
if (extension !== ".zip" && extension !== ".md") {
|
|
15673
|
-
res.status(400).json({ detail: 'extension must be ".zip" or ".
|
|
16502
|
+
if (extension !== ".zip" && extension !== ".md" && extension !== ".skill") {
|
|
16503
|
+
res.status(400).json({ detail: 'extension must be ".zip", ".md", or ".skill".' });
|
|
15674
16504
|
return;
|
|
15675
16505
|
}
|
|
15676
16506
|
if (!contentType || typeof contentType !== "string") {
|
|
@@ -15683,7 +16513,7 @@ ${style.markdown}` : params.prompt;
|
|
|
15683
16513
|
res.status(404).json({ detail: "Skill not found." });
|
|
15684
16514
|
return;
|
|
15685
16515
|
}
|
|
15686
|
-
const stagingKey = `user_${authResult.user.id}/skills/_staging/${
|
|
16516
|
+
const stagingKey = `user_${authResult.user.id}/skills/_staging/${randomUUID3()}${extension}`;
|
|
15687
16517
|
const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${stagingKey}` : stagingKey;
|
|
15688
16518
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
15689
16519
|
res.json({ uploadUrl, stagingKey });
|
|
@@ -15809,19 +16639,22 @@ ${style.markdown}` : params.prompt;
|
|
|
15809
16639
|
}
|
|
15810
16640
|
const versionPrefix = `skills/${skillId}/v${version}/`;
|
|
15811
16641
|
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
15812
|
-
const
|
|
16642
|
+
const asSkill = req.query.format === "skill";
|
|
16643
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
16644
|
+
const zip = new JSZip3();
|
|
15813
16645
|
let fileCount = 0;
|
|
15814
16646
|
for (const file of files) {
|
|
15815
16647
|
const prefixIndex = file.key.indexOf(versionPrefix);
|
|
15816
16648
|
const relativePath = prefixIndex >= 0 ? file.key.slice(prefixIndex + versionPrefix.length) : file.key;
|
|
15817
16649
|
if (!relativePath) continue;
|
|
15818
16650
|
const bytes = await getS3ObjectBytes(file.key, config);
|
|
15819
|
-
|
|
16651
|
+
const archivePath = asSkill ? `${safeName}/${relativePath}` : relativePath;
|
|
16652
|
+
zip.file(archivePath, bytes);
|
|
15820
16653
|
fileCount += 1;
|
|
15821
16654
|
}
|
|
15822
16655
|
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15823
16656
|
zip.file(
|
|
15824
|
-
"version.txt",
|
|
16657
|
+
asSkill ? `${safeName}/version.txt` : "version.txt",
|
|
15825
16658
|
[
|
|
15826
16659
|
`Skill: ${skill.name ?? skillId}`,
|
|
15827
16660
|
`Skill id: ${skillId}`,
|
|
@@ -15832,13 +16665,9 @@ ${style.markdown}` : params.prompt;
|
|
|
15832
16665
|
].join("\n")
|
|
15833
16666
|
);
|
|
15834
16667
|
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
15835
|
-
const
|
|
15836
|
-
const filename = `${safeName}-v${version}.zip`;
|
|
16668
|
+
const filename = asSkill ? `${safeName}.skill` : `${safeName}-v${version}.zip`;
|
|
15837
16669
|
res.setHeader("Content-Type", "application/zip");
|
|
15838
|
-
res.setHeader(
|
|
15839
|
-
"Content-Disposition",
|
|
15840
|
-
`attachment; filename="${filename}"`
|
|
15841
|
-
);
|
|
16670
|
+
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
15842
16671
|
res.send(buffer);
|
|
15843
16672
|
});
|
|
15844
16673
|
app.post("/skills/:skillId/sign", async (req, res) => {
|
|
@@ -16138,6 +16967,12 @@ ${style.markdown}` : params.prompt;
|
|
|
16138
16967
|
const fullSessionPrefix = `${generalPrefix}${userSessionPrefix}`;
|
|
16139
16968
|
return { userSessionPrefix, fullSessionPrefix };
|
|
16140
16969
|
}
|
|
16970
|
+
const loadSessionFilesAuth = async (req, res, sessionId, rights) => {
|
|
16971
|
+
const authed = await loadAuthedSession(req, res, sessionId, rights);
|
|
16972
|
+
if (!authed) return null;
|
|
16973
|
+
const ownerId = authed.session.user ?? authed.user.id;
|
|
16974
|
+
return { ...authed, ownerId, ...buildSessionPrefixes(ownerId, sessionId) };
|
|
16975
|
+
};
|
|
16141
16976
|
function sanitizeFilename(name) {
|
|
16142
16977
|
const trimmed = name.trim();
|
|
16143
16978
|
if (!trimmed) return "";
|
|
@@ -16146,11 +16981,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16146
16981
|
return trimmed.replace(/[\\/]/g, "_");
|
|
16147
16982
|
}
|
|
16148
16983
|
app.get("/sessions/:sessionId/files", async (req, res) => {
|
|
16149
|
-
const authResult = await requestValidators.authenticate(req);
|
|
16150
|
-
if (!authResult.user?.id) {
|
|
16151
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16152
|
-
return;
|
|
16153
|
-
}
|
|
16154
16984
|
const sessionId = req.params.sessionId;
|
|
16155
16985
|
if (!sessionId) {
|
|
16156
16986
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16160,10 +16990,9 @@ ${style.markdown}` : params.prompt;
|
|
|
16160
16990
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
16161
16991
|
return;
|
|
16162
16992
|
}
|
|
16163
|
-
const
|
|
16164
|
-
|
|
16165
|
-
|
|
16166
|
-
);
|
|
16993
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
16994
|
+
if (!authed) return;
|
|
16995
|
+
const { userSessionPrefix } = authed;
|
|
16167
16996
|
let objects;
|
|
16168
16997
|
try {
|
|
16169
16998
|
objects = await listS3ObjectsByPrefix(userSessionPrefix, config);
|
|
@@ -16193,11 +17022,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16193
17022
|
app.post(
|
|
16194
17023
|
"/sessions/:sessionId/files/upload-sign",
|
|
16195
17024
|
async (req, res) => {
|
|
16196
|
-
const authResult = await requestValidators.authenticate(req);
|
|
16197
|
-
if (!authResult.user?.id) {
|
|
16198
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16199
|
-
return;
|
|
16200
|
-
}
|
|
16201
17025
|
const sessionId = req.params.sessionId;
|
|
16202
17026
|
if (!sessionId) {
|
|
16203
17027
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16207,6 +17031,8 @@ ${style.markdown}` : params.prompt;
|
|
|
16207
17031
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
16208
17032
|
return;
|
|
16209
17033
|
}
|
|
17034
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
17035
|
+
if (!authed) return;
|
|
16210
17036
|
const { filename, contentType } = req.body ?? {};
|
|
16211
17037
|
if (!filename || typeof filename !== "string") {
|
|
16212
17038
|
res.status(400).json({ detail: "Missing filename in request body." });
|
|
@@ -16221,11 +17047,7 @@ ${style.markdown}` : params.prompt;
|
|
|
16221
17047
|
res.status(400).json({ detail: "Missing contentType in request body." });
|
|
16222
17048
|
return;
|
|
16223
17049
|
}
|
|
16224
|
-
const
|
|
16225
|
-
authResult.user.id,
|
|
16226
|
-
sessionId
|
|
16227
|
-
);
|
|
16228
|
-
const fullKey = `${fullSessionPrefix}${safeName}`;
|
|
17050
|
+
const fullKey = `${authed.fullSessionPrefix}${safeName}`;
|
|
16229
17051
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
16230
17052
|
res.json({ uploadUrl, key: fullKey });
|
|
16231
17053
|
}
|
|
@@ -16233,11 +17055,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16233
17055
|
app.post(
|
|
16234
17056
|
"/sessions/:sessionId/files/sync-to-sandbox",
|
|
16235
17057
|
async (req, res) => {
|
|
16236
|
-
const authResult = await requestValidators.authenticate(req);
|
|
16237
|
-
if (!authResult.user?.id) {
|
|
16238
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16239
|
-
return;
|
|
16240
|
-
}
|
|
16241
17058
|
const sessionId = req.params.sessionId;
|
|
16242
17059
|
if (!sessionId) {
|
|
16243
17060
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16248,18 +17065,16 @@ ${style.markdown}` : params.prompt;
|
|
|
16248
17065
|
res.status(400).json({ detail: "Missing key in request body." });
|
|
16249
17066
|
return;
|
|
16250
17067
|
}
|
|
16251
|
-
const
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
);
|
|
16255
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
17068
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
17069
|
+
if (!authed) return;
|
|
17070
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
16256
17071
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
16257
17072
|
return;
|
|
16258
17073
|
}
|
|
16259
17074
|
try {
|
|
16260
17075
|
const result = await downloadKeyIntoSandbox({
|
|
16261
17076
|
sessionId,
|
|
16262
|
-
userId:
|
|
17077
|
+
userId: authed.ownerId,
|
|
16263
17078
|
fullS3Key: key,
|
|
16264
17079
|
config
|
|
16265
17080
|
});
|
|
@@ -16273,11 +17088,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16273
17088
|
app.delete(
|
|
16274
17089
|
"/sessions/:sessionId/files",
|
|
16275
17090
|
async (req, res) => {
|
|
16276
|
-
const authResult = await requestValidators.authenticate(req);
|
|
16277
|
-
if (!authResult.user?.id) {
|
|
16278
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16279
|
-
return;
|
|
16280
|
-
}
|
|
16281
17091
|
const sessionId = req.params.sessionId;
|
|
16282
17092
|
if (!sessionId) {
|
|
16283
17093
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16288,11 +17098,9 @@ ${style.markdown}` : params.prompt;
|
|
|
16288
17098
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
16289
17099
|
return;
|
|
16290
17100
|
}
|
|
16291
|
-
const
|
|
16292
|
-
|
|
16293
|
-
|
|
16294
|
-
);
|
|
16295
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
17101
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
17102
|
+
if (!authed) return;
|
|
17103
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
16296
17104
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
16297
17105
|
return;
|
|
16298
17106
|
}
|
|
@@ -16311,11 +17119,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16311
17119
|
if (!req.headers.authorization && typeof req.query.auth === "string") {
|
|
16312
17120
|
req.headers.authorization = `Bearer ${req.query.auth}`;
|
|
16313
17121
|
}
|
|
16314
|
-
const authResult = await requestValidators.authenticate(req);
|
|
16315
|
-
if (!authResult.user?.id) {
|
|
16316
|
-
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16317
|
-
return;
|
|
16318
|
-
}
|
|
16319
17122
|
const sessionId = req.params.sessionId;
|
|
16320
17123
|
if (!sessionId) {
|
|
16321
17124
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16326,11 +17129,9 @@ ${style.markdown}` : params.prompt;
|
|
|
16326
17129
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
16327
17130
|
return;
|
|
16328
17131
|
}
|
|
16329
|
-
const
|
|
16330
|
-
|
|
16331
|
-
|
|
16332
|
-
);
|
|
16333
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
17132
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
17133
|
+
if (!authed) return;
|
|
17134
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
16334
17135
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
16335
17136
|
return;
|
|
16336
17137
|
}
|
|
@@ -16434,7 +17235,7 @@ ${style.markdown}` : params.prompt;
|
|
|
16434
17235
|
` - tracking.json tracking events linked to the user`,
|
|
16435
17236
|
``
|
|
16436
17237
|
].join("\n");
|
|
16437
|
-
const zip = new
|
|
17238
|
+
const zip = new JSZip3();
|
|
16438
17239
|
zip.file("README.txt", readme);
|
|
16439
17240
|
zip.file("user_data.json", JSON.stringify(userExport, null, 2));
|
|
16440
17241
|
zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
|
|
@@ -16714,7 +17515,7 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
|
|
|
16714
17515
|
|
|
16715
17516
|
// src/mcp/index.ts
|
|
16716
17517
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16717
|
-
import { randomUUID as
|
|
17518
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
16718
17519
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16719
17520
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
16720
17521
|
import "express";
|
|
@@ -16819,7 +17620,7 @@ var ExuluMCP = class {
|
|
|
16819
17620
|
throw new Error("Tool not found in converted tools array.");
|
|
16820
17621
|
}
|
|
16821
17622
|
const iterator = await convertedTool.execute(inputs, {
|
|
16822
|
-
toolCallId: tool2.id + "_" +
|
|
17623
|
+
toolCallId: tool2.id + "_" + randomUUID4(),
|
|
16823
17624
|
messages: []
|
|
16824
17625
|
});
|
|
16825
17626
|
let result;
|
|
@@ -17011,7 +17812,7 @@ var ExuluMCP = class {
|
|
|
17011
17812
|
transport = this.transports[sessionId];
|
|
17012
17813
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
17013
17814
|
transport = new StreamableHTTPServerTransport({
|
|
17014
|
-
sessionIdGenerator: () =>
|
|
17815
|
+
sessionIdGenerator: () => randomUUID4(),
|
|
17015
17816
|
onsessioninitialized: (sessionId2) => {
|
|
17016
17817
|
this.transports[sessionId2] = transport;
|
|
17017
17818
|
}
|
|
@@ -17992,7 +18793,7 @@ var ExuluEval = class {
|
|
|
17992
18793
|
|
|
17993
18794
|
// src/templates/evals/index.ts
|
|
17994
18795
|
import { z as z5 } from "zod";
|
|
17995
|
-
import { generateText as
|
|
18796
|
+
import { generateText as generateText8, Output as Output2 } from "ai";
|
|
17996
18797
|
var llmAsJudgeEval = () => {
|
|
17997
18798
|
if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
|
|
17998
18799
|
return new ExuluEval({
|
|
@@ -18037,7 +18838,7 @@ var llmAsJudgeEval = () => {
|
|
|
18037
18838
|
rbacBypass: true
|
|
18038
18839
|
});
|
|
18039
18840
|
console.log("[EXULU] prompt", prompt);
|
|
18040
|
-
const { output } = await
|
|
18841
|
+
const { output } = await generateText8({
|
|
18041
18842
|
temperature: 0,
|
|
18042
18843
|
model: resolved.languageModel,
|
|
18043
18844
|
system: "",
|
|
@@ -18460,7 +19261,7 @@ After asking a question, use the Question Read tool to check if the user has ans
|
|
|
18460
19261
|
|
|
18461
19262
|
// src/templates/tools/question/question-ask.ts
|
|
18462
19263
|
import z7 from "zod";
|
|
18463
|
-
import { randomUUID as
|
|
19264
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
18464
19265
|
var AnswerOptionSchema = z7.object({
|
|
18465
19266
|
id: z7.string().describe("Unique identifier for the answer option"),
|
|
18466
19267
|
text: z7.string().describe("The text of the answer option")
|
|
@@ -18514,15 +19315,15 @@ var QuestionAskTool = new ExuluTool({
|
|
|
18514
19315
|
throw new Error("You don't have access to this session " + session.id + ".");
|
|
18515
19316
|
}
|
|
18516
19317
|
const answerOptionsWithIds = answerOptions.map((text) => ({
|
|
18517
|
-
id:
|
|
19318
|
+
id: randomUUID5(),
|
|
18518
19319
|
text
|
|
18519
19320
|
}));
|
|
18520
19321
|
answerOptionsWithIds.push({
|
|
18521
|
-
id:
|
|
19322
|
+
id: randomUUID5(),
|
|
18522
19323
|
text: "None of the above..."
|
|
18523
19324
|
});
|
|
18524
19325
|
const newQuestion = {
|
|
18525
|
-
id:
|
|
19326
|
+
id: randomUUID5(),
|
|
18526
19327
|
question,
|
|
18527
19328
|
answerOptions: answerOptionsWithIds,
|
|
18528
19329
|
status: "pending"
|
|
@@ -21303,10 +22104,10 @@ var MarkdownChunker = class {
|
|
|
21303
22104
|
// ee/python/documents/processing/doc_processor.ts
|
|
21304
22105
|
import * as fs3 from "fs";
|
|
21305
22106
|
import * as path from "path";
|
|
21306
|
-
import { generateText as
|
|
22107
|
+
import { generateText as generateText9, Output as Output3 } from "ai";
|
|
21307
22108
|
import { z as z12 } from "zod";
|
|
21308
22109
|
import pLimit from "p-limit";
|
|
21309
|
-
import { randomUUID as
|
|
22110
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
21310
22111
|
import * as mammoth from "mammoth";
|
|
21311
22112
|
import TurndownService from "turndown";
|
|
21312
22113
|
import WordExtractor from "word-extractor";
|
|
@@ -21760,7 +22561,7 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
21760
22561
|
|
|
21761
22562
|
### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
|
|
21762
22563
|
`;
|
|
21763
|
-
const result = await
|
|
22564
|
+
const result = await generateText9({
|
|
21764
22565
|
model,
|
|
21765
22566
|
output: Output3.object({
|
|
21766
22567
|
schema: z12.object({
|
|
@@ -22181,7 +22982,7 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
22181
22982
|
if (!fileType) {
|
|
22182
22983
|
throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
|
|
22183
22984
|
}
|
|
22184
|
-
const UUID =
|
|
22985
|
+
const UUID = randomUUID6();
|
|
22185
22986
|
let buffer;
|
|
22186
22987
|
if (Buffer.isBuffer(file)) {
|
|
22187
22988
|
filePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
@@ -22211,7 +23012,7 @@ async function documentProcessor({
|
|
|
22211
23012
|
if (!license["advanced-document-processing"]) {
|
|
22212
23013
|
throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
|
|
22213
23014
|
}
|
|
22214
|
-
const uuid =
|
|
23015
|
+
const uuid = randomUUID6();
|
|
22215
23016
|
const tempDir = path.join(process.cwd(), "temp", uuid);
|
|
22216
23017
|
const localFilesAndFoldersToDelete = [tempDir];
|
|
22217
23018
|
console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
|