@exulu/backend 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -415,7 +415,12 @@ function getS3Client(config) {
415
415
  credentials: {
416
416
  accessKeyId: config.fileUploads.s3key,
417
417
  secretAccessKey: config.fileUploads.s3secret
418
- }
418
+ },
419
+ // AWS SDK >= 3.729 injects x-amz-checksum-crc32 (of an empty body) into
420
+ // presigned PUT URLs, which S3-compatible stores like MinIO reject on
421
+ // upload with a checksum mismatch. WHEN_REQUIRED disables that default.
422
+ requestChecksumCalculation: "WHEN_REQUIRED",
423
+ responseChecksumValidation: "WHEN_REQUIRED"
419
424
  });
420
425
  return s3Client;
421
426
  }
@@ -1667,6 +1672,9 @@ async function tagUpdate(input) {
1667
1672
  budget_duration: input.budget_duration
1668
1673
  });
1669
1674
  }
1675
+ async function budgetUpdate(budget_id, patch) {
1676
+ await call("/budget/update", { budget_id, ...patch });
1677
+ }
1670
1678
  async function tagDelete(name) {
1671
1679
  await call("/tag/delete", { name });
1672
1680
  }
@@ -1675,8 +1683,9 @@ function extractBudget(raw) {
1675
1683
  const max_budget = bt.max_budget ?? raw?.max_budget ?? null;
1676
1684
  const budget_duration = bt.budget_duration ?? raw?.budget_duration ?? null;
1677
1685
  const budget_reset_at = bt.budget_reset_at ?? raw?.budget_reset_at ?? null;
1686
+ const budget_id = bt.budget_id ?? raw?.budget_id ?? null;
1678
1687
  const spend = typeof raw?.spend === "number" ? raw.spend : typeof bt.spend === "number" ? bt.spend : 0;
1679
- return { max_budget, budget_duration, budget_reset_at, spend };
1688
+ return { max_budget, budget_duration, budget_reset_at, budget_id, spend };
1680
1689
  }
1681
1690
  async function listTags() {
1682
1691
  const { url, masterKey } = litellmBase();
@@ -1709,7 +1718,8 @@ async function listTagBudgets() {
1709
1718
  spend: b.spend,
1710
1719
  max_budget: b.max_budget,
1711
1720
  budget_duration: b.budget_duration,
1712
- budget_reset_at: b.budget_reset_at
1721
+ budget_reset_at: b.budget_reset_at,
1722
+ budget_id: b.budget_id
1713
1723
  };
1714
1724
  }
1715
1725
  const names = Object.keys(map);
@@ -1755,7 +1765,8 @@ async function tagInfo(names) {
1755
1765
  spend: b.spend,
1756
1766
  max_budget: b.max_budget,
1757
1767
  budget_duration: b.budget_duration,
1758
- budget_reset_at: b.budget_reset_at
1768
+ budget_reset_at: b.budget_reset_at,
1769
+ budget_id: b.budget_id
1759
1770
  };
1760
1771
  }
1761
1772
  return out;
@@ -1986,7 +1997,16 @@ async function setBudgetSettings(settings) {
1986
1997
  }).onConflict("config_key").merge({ config_value: JSON.stringify(settings) });
1987
1998
  return settings;
1988
1999
  }
1989
- async function upsertBudget(tag, max_budget, budget_duration) {
2000
+ function parseResetAt(raw) {
2001
+ if (raw === void 0 || raw === null || raw === "") {
2002
+ return { valid: true, value: void 0 };
2003
+ }
2004
+ if (typeof raw !== "string") return { valid: false };
2005
+ const t = Date.parse(raw);
2006
+ if (Number.isNaN(t)) return { valid: false };
2007
+ return { valid: true, value: new Date(t).toISOString() };
2008
+ }
2009
+ async function upsertBudget(tag, max_budget, budget_duration, budget_reset_at) {
1990
2010
  const info = await tagInfo([tag]);
1991
2011
  try {
1992
2012
  if (info[tag]) {
@@ -2001,6 +2021,15 @@ async function upsertBudget(tag, max_budget, budget_duration) {
2001
2021
  await tagUpdate({ name: tag, max_budget, budget_duration });
2002
2022
  }
2003
2023
  }
2024
+ if (budget_reset_at) {
2025
+ const after = await tagInfo([tag]);
2026
+ const budgetId = after[tag]?.budget_id ?? null;
2027
+ if (budgetId) {
2028
+ await budgetUpdate(budgetId, { budget_reset_at });
2029
+ } else {
2030
+ console.warn(`[EXULU] upsertBudget: no budget_id for ${tag}; reset date not applied`);
2031
+ }
2032
+ }
2004
2033
  invalidateBudgetCaches(tag);
2005
2034
  }
2006
2035
  function invalidateBudgetCaches(tag) {
@@ -3303,6 +3332,15 @@ var init_system_dependencies = __esm({
3303
3332
  macos: "brew install poppler"
3304
3333
  }
3305
3334
  },
3335
+ {
3336
+ check: { kind: "binary", binary: "pdftotext" },
3337
+ displayName: "Poppler (pdftotext)",
3338
+ purpose: "parse_document tool: extracting page-marked text from PDFs",
3339
+ installHints: {
3340
+ debian: "apt-get install -y poppler-utils",
3341
+ macos: "brew install poppler"
3342
+ }
3343
+ },
3306
3344
  {
3307
3345
  check: { kind: "npm-global", packageName: "docx" },
3308
3346
  displayName: "docx (npm global)",
@@ -4140,6 +4178,595 @@ var init_session_file_read_tool = __esm({
4140
4178
  }
4141
4179
  });
4142
4180
 
4181
+ // src/templates/tools/document-render-helpers.ts
4182
+ async function pdfToText(pdf) {
4183
+ const dir = await (0, import_promises2.mkdtemp)((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "exulu-parse-"));
4184
+ try {
4185
+ const inputPath = (0, import_node_path5.join)(dir, "input.pdf");
4186
+ await (0, import_promises2.writeFile)(inputPath, pdf);
4187
+ const { stdout } = await execFileAsync("pdftotext", ["-layout", inputPath, "-"], {
4188
+ timeout: 6e4,
4189
+ maxBuffer: MAX_STDOUT_BYTES
4190
+ });
4191
+ return stdout;
4192
+ } finally {
4193
+ await (0, import_promises2.rm)(dir, { recursive: true, force: true });
4194
+ }
4195
+ }
4196
+ async function renderPdfPageToPng(pdf, page, scaleTo) {
4197
+ const dir = await (0, import_promises2.mkdtemp)((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "exulu-render-"));
4198
+ try {
4199
+ const inputPath = (0, import_node_path5.join)(dir, "input.pdf");
4200
+ await (0, import_promises2.writeFile)(inputPath, pdf);
4201
+ try {
4202
+ await execFileAsync(
4203
+ "pdftoppm",
4204
+ ["-png", "-f", String(page), "-l", String(page), "-scale-to", String(scaleTo), inputPath, (0, import_node_path5.join)(dir, "page")],
4205
+ { timeout: 6e4, maxBuffer: MAX_STDOUT_BYTES }
4206
+ );
4207
+ } catch (err) {
4208
+ if (err?.code === 99) return null;
4209
+ throw err;
4210
+ }
4211
+ const produced = (await (0, import_promises2.readdir)(dir)).find((f) => f.startsWith("page") && f.endsWith(".png"));
4212
+ if (!produced) return null;
4213
+ return await (0, import_promises2.readFile)((0, import_node_path5.join)(dir, produced));
4214
+ } finally {
4215
+ await (0, import_promises2.rm)(dir, { recursive: true, force: true });
4216
+ }
4217
+ }
4218
+ var import_node_child_process4, import_node_util3, import_promises2, import_node_os, import_node_path5, execFileAsync, MAX_STDOUT_BYTES;
4219
+ var init_document_render_helpers = __esm({
4220
+ "src/templates/tools/document-render-helpers.ts"() {
4221
+ "use strict";
4222
+ init_cjs_shims();
4223
+ import_node_child_process4 = require("child_process");
4224
+ import_node_util3 = require("util");
4225
+ import_promises2 = require("fs/promises");
4226
+ import_node_os = require("os");
4227
+ import_node_path5 = require("path");
4228
+ execFileAsync = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
4229
+ MAX_STDOUT_BYTES = 64 * 1024 * 1024;
4230
+ }
4231
+ });
4232
+
4233
+ // src/templates/tools/parse-document-tool.ts
4234
+ var import_zod6, import_node_path6, import_officeparser, DEFAULT_LIMIT2, MAX_CONTENT_CHARS2, MIN_CHARS_PER_PAGE, OFFICE_EXTENSIONS, pagesPattern, createParseDocumentTool;
4235
+ var init_parse_document_tool = __esm({
4236
+ "src/templates/tools/parse-document-tool.ts"() {
4237
+ "use strict";
4238
+ init_cjs_shims();
4239
+ import_zod6 = require("zod");
4240
+ import_node_path6 = require("path");
4241
+ import_officeparser = require("officeparser");
4242
+ init_tool();
4243
+ init_uppy();
4244
+ init_document_render_helpers();
4245
+ DEFAULT_LIMIT2 = 250;
4246
+ MAX_CONTENT_CHARS2 = 16e3;
4247
+ MIN_CHARS_PER_PAGE = 20;
4248
+ OFFICE_EXTENSIONS = /* @__PURE__ */ new Set([
4249
+ ".docx",
4250
+ ".doc",
4251
+ ".xlsx",
4252
+ ".xls",
4253
+ ".pptx",
4254
+ ".ppt",
4255
+ ".odt",
4256
+ ".ods",
4257
+ ".odp",
4258
+ ".rtf"
4259
+ ]);
4260
+ pagesPattern = /^(\d+)(?:-(\d+))?$/;
4261
+ createParseDocumentTool = ({
4262
+ sessionID,
4263
+ user,
4264
+ exuluConfig
4265
+ }) => {
4266
+ if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
4267
+ const parseDocumentExecute = async ({
4268
+ filename,
4269
+ pages,
4270
+ offset,
4271
+ limit
4272
+ }) => {
4273
+ const safeName = String(filename ?? "").trim();
4274
+ if (!safeName || safeName.includes("..") || safeName.includes("/") || safeName.includes("\\")) {
4275
+ return {
4276
+ error: "Invalid filename \u2014 pass the bare file name exactly as listed in the session files (no paths)."
4277
+ };
4278
+ }
4279
+ const ext = (0, import_node_path6.extname)(safeName).toLowerCase();
4280
+ if (ext !== ".pdf" && !OFFICE_EXTENSIONS.has(ext)) {
4281
+ return {
4282
+ error: `Unsupported extension "${ext}" \u2014 parse_document handles PDF and Office formats. For plain-text files use read_session_file.`
4283
+ };
4284
+ }
4285
+ if (pages && ext !== ".pdf") {
4286
+ return { error: `The pages option is only supported for PDF files \u2014 "${ext}" documents are extracted whole.` };
4287
+ }
4288
+ const uploads = exuluConfig.fileUploads;
4289
+ const generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
4290
+ const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
4291
+ try {
4292
+ const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
4293
+ const res = await fetch(url);
4294
+ if (!res.ok) {
4295
+ return { error: `Could not read session file "${safeName}" (status ${res.status}). Check the exact file name.` };
4296
+ }
4297
+ const bytes = Buffer.from(await res.arrayBuffer());
4298
+ let fullText;
4299
+ let totalPages;
4300
+ if (ext === ".pdf") {
4301
+ const raw = await pdfToText(bytes);
4302
+ const pageTexts = raw.replace(/\f$/, "").split("\f");
4303
+ totalPages = pageTexts.length;
4304
+ const nonWhitespace = raw.replace(/\s/g, "").length;
4305
+ if (nonWhitespace < totalPages * MIN_CHARS_PER_PAGE) {
4306
+ return {
4307
+ error: `"${safeName}" has no extractable text layer (likely a scan or image-based PDF). Use view_document_page to look at pages visually, or suggest the user add the document to a knowledge base with a document processor for full OCR.`
4308
+ };
4309
+ }
4310
+ let range = [1, totalPages];
4311
+ if (pages) {
4312
+ const m = pagesPattern.exec(pages.trim());
4313
+ if (!m) return { error: `Invalid pages "${pages}" \u2014 use "3" or "2-5".` };
4314
+ range = [Number(m[1]), Number(m[2] ?? m[1])];
4315
+ if (range[0] < 1 || range[0] > range[1]) {
4316
+ return { error: `Invalid pages "${pages}" \u2014 start must be at least 1 and not greater than the end.` };
4317
+ }
4318
+ if (range[0] > totalPages) {
4319
+ return { error: `Page range starts at ${range[0]} but "${safeName}" has only ${totalPages} page${totalPages === 1 ? "" : "s"}.` };
4320
+ }
4321
+ }
4322
+ fullText = pageTexts.map((text, i) => ({ page: i + 1, text })).filter(({ page }) => page >= range[0] && page <= range[1]).map(({ page, text }) => `--- page ${page} ---
4323
+ ${text.trim()}`).join("\n");
4324
+ } else {
4325
+ const extracted = await (0, import_officeparser.parseOfficeAsync)(bytes, {
4326
+ outputErrorToConsole: false,
4327
+ newlineDelimiter: "\n"
4328
+ });
4329
+ fullText = String(extracted);
4330
+ }
4331
+ const lines = fullText.split("\n");
4332
+ const start = (offset ?? 1) - 1;
4333
+ const requested = limit ?? DEFAULT_LIMIT2;
4334
+ const sliced = lines.slice(start, start + requested);
4335
+ let content = sliced.join("\n");
4336
+ let linesReturned = sliced.length;
4337
+ if (content.length > MAX_CONTENT_CHARS2) {
4338
+ content = content.slice(0, MAX_CONTENT_CHARS2);
4339
+ linesReturned = Math.max(1, content.split("\n").length - 1);
4340
+ content = content + "\n[slice truncated \u2014 request fewer lines]";
4341
+ }
4342
+ return {
4343
+ content,
4344
+ ...totalPages !== void 0 ? { totalPages } : {},
4345
+ totalLines: lines.length,
4346
+ offset: start + 1,
4347
+ linesReturned
4348
+ };
4349
+ } catch (err) {
4350
+ return { error: `Failed to parse "${safeName}": ${err instanceof Error ? err.message : "unknown error"}` };
4351
+ }
4352
+ };
4353
+ return ExuluTool.internal({
4354
+ id: "parse_document",
4355
+ name: "parse_document",
4356
+ needsApproval: false,
4357
+ description: `Extract the text of an uploaded PDF or Office document from this session's files, with "--- page N ---" markers for PDFs so you can locate content by page. Free and fast (no OCR): works only on documents with a real text layer. To SEE a page or an image inside a document, use view_document_page.`,
4358
+ inputSchema: import_zod6.z.object({
4359
+ filename: import_zod6.z.string().describe('Exact session file name, e.g. "report.pdf"'),
4360
+ pages: import_zod6.z.string().optional().describe('PDF page or range to extract, e.g. "2" or "1-5" (default: all pages) (PDF only)'),
4361
+ offset: import_zod6.z.number().int().min(1).optional().describe("1-based first output line to read (default 1)"),
4362
+ limit: import_zod6.z.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT2})`)
4363
+ }),
4364
+ type: "function",
4365
+ category: "session",
4366
+ config: [],
4367
+ // Same shape mismatch as read_session_file / memory-tool: internal utility
4368
+ // tools return richer objects than ExuluTool's retrieval-flavored execute
4369
+ // type; the AI SDK passes the object through verbatim.
4370
+ execute: parseDocumentExecute
4371
+ });
4372
+ };
4373
+ }
4374
+ });
4375
+
4376
+ // src/sessions/pdf-preview-cache.ts
4377
+ function sanitizeEtag(raw) {
4378
+ return raw.replace(/^"|"$/g, "").replace(/[^a-zA-Z0-9_-]/g, "_");
4379
+ }
4380
+ async function getPdfPreviewBytes(opts) {
4381
+ const { sourceKey, etag, config } = opts;
4382
+ const safeEtag = sanitizeEtag(etag);
4383
+ if (!safeEtag) {
4384
+ throw new PreviewRenderError(`Invalid ETag for ${sourceKey}`);
4385
+ }
4386
+ const cachedPath = (0, import_node_path7.join)(CACHE_ROOT, `${safeEtag}.pdf`);
4387
+ if ((0, import_node_fs6.existsSync)(cachedPath)) {
4388
+ return (0, import_promises3.readFile)(cachedPath);
4389
+ }
4390
+ const existing = inFlight.get(safeEtag);
4391
+ if (existing) return existing;
4392
+ const promise = (async () => {
4393
+ try {
4394
+ await (0, import_promises3.mkdir)(CACHE_IN, { recursive: true });
4395
+ await (0, import_promises3.mkdir)(CACHE_OUT, { recursive: true });
4396
+ const ext = ((0, import_node_path7.extname)(sourceKey) || ".docx").toLowerCase();
4397
+ const inputPath = (0, import_node_path7.join)(CACHE_IN, `${safeEtag}${ext}`);
4398
+ const outputPath = (0, import_node_path7.join)(CACHE_OUT, `${safeEtag}.pdf`);
4399
+ try {
4400
+ const bytes = await getS3ObjectBytes(sourceKey, config);
4401
+ await (0, import_promises3.writeFile)(inputPath, bytes);
4402
+ try {
4403
+ await execAsync3(
4404
+ `soffice --headless --convert-to pdf "${inputPath}" --outdir "${CACHE_OUT}"`,
4405
+ { timeout: 6e4, maxBuffer: 16 * 1024 * 1024 }
4406
+ );
4407
+ } catch (err) {
4408
+ throw new PreviewRenderError(
4409
+ `LibreOffice conversion failed for ${sourceKey} (etag ${etag}): ${err?.stderr ?? err?.message ?? "unknown error"}`
4410
+ );
4411
+ }
4412
+ if (!(0, import_node_fs6.existsSync)(outputPath)) {
4413
+ throw new PreviewRenderError(
4414
+ `LibreOffice produced no output for ${sourceKey} (etag ${etag})`
4415
+ );
4416
+ }
4417
+ await (0, import_promises3.rename)(outputPath, cachedPath);
4418
+ return await (0, import_promises3.readFile)(cachedPath);
4419
+ } finally {
4420
+ await (0, import_promises3.rm)(inputPath, { force: true });
4421
+ }
4422
+ } finally {
4423
+ inFlight.delete(safeEtag);
4424
+ }
4425
+ })();
4426
+ inFlight.set(safeEtag, promise);
4427
+ return promise;
4428
+ }
4429
+ var import_node_child_process5, import_node_fs6, import_promises3, import_node_path7, import_node_util4, execAsync3, CACHE_ROOT, CACHE_IN, CACHE_OUT, inFlight, PreviewRenderError;
4430
+ var init_pdf_preview_cache = __esm({
4431
+ "src/sessions/pdf-preview-cache.ts"() {
4432
+ "use strict";
4433
+ init_cjs_shims();
4434
+ import_node_child_process5 = require("child_process");
4435
+ import_node_fs6 = require("fs");
4436
+ import_promises3 = require("fs/promises");
4437
+ import_node_path7 = require("path");
4438
+ import_node_util4 = require("util");
4439
+ init_uppy();
4440
+ execAsync3 = (0, import_node_util4.promisify)(import_node_child_process5.exec);
4441
+ CACHE_ROOT = "/tmp/exulu-pdf-cache";
4442
+ CACHE_IN = (0, import_node_path7.join)(CACHE_ROOT, "_in");
4443
+ CACHE_OUT = (0, import_node_path7.join)(CACHE_ROOT, "_out");
4444
+ inFlight = /* @__PURE__ */ new Map();
4445
+ PreviewRenderError = class extends Error {
4446
+ constructor(message) {
4447
+ super(message);
4448
+ this.name = "PreviewRenderError";
4449
+ }
4450
+ };
4451
+ }
4452
+ });
4453
+
4454
+ // src/exulu/litellm/catalog.ts
4455
+ var catalog_exports = {};
4456
+ __export(catalog_exports, {
4457
+ __resetLiteLLMCatalogCacheForTesting: () => __resetLiteLLMCatalogCacheForTesting,
4458
+ fetchLiteLLMCatalog: () => fetchLiteLLMCatalog,
4459
+ findLiteLLMModel: () => findLiteLLMModel
4460
+ });
4461
+ var CACHE_TTL_MS, _cache, __resetLiteLLMCatalogCacheForTesting, fetchLiteLLMCatalog, findLiteLLMModel;
4462
+ var init_catalog = __esm({
4463
+ "src/exulu/litellm/catalog.ts"() {
4464
+ "use strict";
4465
+ init_cjs_shims();
4466
+ CACHE_TTL_MS = 3e4;
4467
+ __resetLiteLLMCatalogCacheForTesting = () => {
4468
+ _cache = void 0;
4469
+ };
4470
+ fetchLiteLLMCatalog = async () => {
4471
+ if (process.env.EXULU_USE_LITELLM !== "true") return [];
4472
+ if (_cache && _cache.expiresAt > Date.now()) {
4473
+ return _cache.items;
4474
+ }
4475
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
4476
+ const port = process.env.LITELLM_PORT ?? "4000";
4477
+ const masterKey = process.env.LITELLM_MASTER_KEY;
4478
+ if (!masterKey) return [];
4479
+ try {
4480
+ const res = await fetch(`http://${host}:${port}/model/info`, {
4481
+ method: "GET",
4482
+ headers: { Authorization: `Bearer ${masterKey}` }
4483
+ });
4484
+ if (!res.ok) {
4485
+ console.error(
4486
+ `[EXULU] litellmCatalog: LiteLLM /model/info returned ${res.status}`
4487
+ );
4488
+ return [];
4489
+ }
4490
+ const json = await res.json();
4491
+ const items = (Array.isArray(json?.data) ? json.data : []).map((m) => ({
4492
+ // filter out trailing * from model_name
4493
+ model_name: m.model_name.replace(/\*$/, ""),
4494
+ upstream_model: m.litellm_params?.model ?? null,
4495
+ tags: Array.isArray(m.model_info?.tags) ? m.model_info.tags : [],
4496
+ brand: m.model_info?.brand ?? null,
4497
+ type: m.model_info?.type ?? null,
4498
+ region: m.model_info?.region ?? null,
4499
+ max_tokens: m.model_info?.max_tokens ?? null,
4500
+ max_input_tokens: m.model_info?.max_input_tokens ?? null,
4501
+ input_cost_per_million_tokens: m.model_info?.input_cost_per_token * 1e6,
4502
+ output_cost_per_million_tokens: m.model_info?.output_cost_per_token * 1e6,
4503
+ active: m.model_info?.active ?? true,
4504
+ max_output_tokens: m.model_info?.max_output_tokens ?? null,
4505
+ supports_vision: !!m.model_info?.supports_vision,
4506
+ supports_function_calling: !!m.model_info?.supports_function_calling,
4507
+ supports_pdf_input: !!m.model_info?.supports_pdf_input,
4508
+ supports_audio_input: !!m.model_info?.supports_audio_input,
4509
+ sizes: Array.isArray(m.model_info?.sizes) ? m.model_info.sizes : null,
4510
+ qualities: Array.isArray(m.model_info?.qualities) ? m.model_info.qualities : null,
4511
+ supports_edit: !!m.model_info?.supports_edit,
4512
+ max_n: typeof m.model_info?.max_n === "number" ? m.model_info.max_n : null
4513
+ }));
4514
+ const map = /* @__PURE__ */ new Map();
4515
+ for (const item of items) {
4516
+ const key = `${item.model_name}-${item.upstream_model}`;
4517
+ if (map.has(key)) {
4518
+ map.get(key).tags.push(...item.tags);
4519
+ } else {
4520
+ map.set(key, item);
4521
+ }
4522
+ }
4523
+ const uniqueItems = Array.from(map.values());
4524
+ _cache = { expiresAt: Date.now() + CACHE_TTL_MS, items: uniqueItems };
4525
+ return uniqueItems.filter((m) => m.type !== "speech_to_text" && m.type !== "text_to_speech");
4526
+ } catch (err) {
4527
+ console.error("[EXULU] litellmCatalog: failed to fetch /model/info:", err);
4528
+ return [];
4529
+ }
4530
+ };
4531
+ findLiteLLMModel = async (modelName) => {
4532
+ if (!modelName) return void 0;
4533
+ const items = await fetchLiteLLMCatalog();
4534
+ return items.find((m) => m.model_name === modelName);
4535
+ };
4536
+ }
4537
+ });
4538
+
4539
+ // src/exulu/tool-image-attachments.ts
4540
+ function sweep() {
4541
+ const cutoff = Date.now() - TTL_MS;
4542
+ for (const [id, entry] of stash) {
4543
+ if (entry.stashedAt < cutoff) stash.delete(id);
4544
+ }
4545
+ while (stash.size > MAX_ENTRIES) {
4546
+ const oldest = stash.keys().next().value;
4547
+ if (oldest === void 0) break;
4548
+ stash.delete(oldest);
4549
+ }
4550
+ let totalBytes = 0;
4551
+ for (const entry of stash.values()) totalBytes += entry.data.length;
4552
+ while (totalBytes > MAX_TOTAL_BYTES) {
4553
+ const oldest = stash.keys().next().value;
4554
+ if (oldest === void 0) break;
4555
+ totalBytes -= stash.get(oldest).data.length;
4556
+ stash.delete(oldest);
4557
+ }
4558
+ }
4559
+ function stashToolImage(toolCallId, image) {
4560
+ stash.set(toolCallId, { ...image, stashedAt: Date.now() });
4561
+ sweep();
4562
+ }
4563
+ function stashedIdsInMessage(message) {
4564
+ if (message?.role !== "tool" || !Array.isArray(message.content)) return [];
4565
+ return message.content.filter(
4566
+ (p) => p?.type === "tool-result" && typeof p.toolCallId === "string" && p.toolName === STASH_TOOL_NAME && stash.has(p.toolCallId)
4567
+ ).map((p) => p.toolCallId);
4568
+ }
4569
+ function injectedTextFor(id, label) {
4570
+ return `${INJECTED_IMAGE_PREFIX}${id}: ${label}]`;
4571
+ }
4572
+ function firstTextPart(message) {
4573
+ if (message?.role !== "user" || !Array.isArray(message.content)) return void 0;
4574
+ const first = message.content[0];
4575
+ return first?.type === "text" ? first.text : void 0;
4576
+ }
4577
+ function imageAttachmentGuard() {
4578
+ return ({ messages }) => {
4579
+ sweep();
4580
+ if (!Array.isArray(messages) || messages.length === 0 || stash.size === 0) return void 0;
4581
+ let changed = false;
4582
+ const next = [];
4583
+ for (let i = 0; i < messages.length; i++) {
4584
+ const message = messages[i];
4585
+ next.push(message);
4586
+ const ids = stashedIdsInMessage(message);
4587
+ if (ids.length === 0) continue;
4588
+ const alreadyInjected = [];
4589
+ for (let j = i + 1; j < messages.length; j++) {
4590
+ const text = firstTextPart(messages[j]);
4591
+ if (text === void 0 || !text.startsWith(INJECTED_IMAGE_PREFIX)) break;
4592
+ alreadyInjected.push(text);
4593
+ }
4594
+ for (const id of ids) {
4595
+ if (alreadyInjected.some((text) => text.startsWith(`${INJECTED_IMAGE_PREFIX}${id}:`))) continue;
4596
+ const image = stash.get(id);
4597
+ changed = true;
4598
+ next.push({
4599
+ role: "user",
4600
+ content: [
4601
+ { type: "text", text: injectedTextFor(id, image.label) },
4602
+ { type: "image", image: image.data, mediaType: image.mediaType }
4603
+ ]
4604
+ });
4605
+ }
4606
+ }
4607
+ return changed ? { messages: next } : void 0;
4608
+ };
4609
+ }
4610
+ var INJECTED_IMAGE_PREFIX, stash, MAX_ENTRIES, TTL_MS, MAX_TOTAL_BYTES, STASH_TOOL_NAME;
4611
+ var init_tool_image_attachments = __esm({
4612
+ "src/exulu/tool-image-attachments.ts"() {
4613
+ "use strict";
4614
+ init_cjs_shims();
4615
+ INJECTED_IMAGE_PREFIX = "[Image attached from tool call ";
4616
+ stash = /* @__PURE__ */ new Map();
4617
+ MAX_ENTRIES = 100;
4618
+ TTL_MS = 30 * 60 * 1e3;
4619
+ MAX_TOTAL_BYTES = 1e8;
4620
+ STASH_TOOL_NAME = "view_document_page";
4621
+ }
4622
+ });
4623
+
4624
+ // src/templates/tools/view-document-page-tool.ts
4625
+ var import_zod7, import_node_path8, MAX_IMAGE_BYTES, SCALE_PRIMARY, SCALE_FALLBACK, IMAGE_MEDIA_TYPES, OFFICE_EXTENSIONS2, createViewDocumentPageTool;
4626
+ var init_view_document_page_tool = __esm({
4627
+ "src/templates/tools/view-document-page-tool.ts"() {
4628
+ "use strict";
4629
+ init_cjs_shims();
4630
+ import_zod7 = require("zod");
4631
+ import_node_path8 = require("path");
4632
+ init_tool();
4633
+ init_uppy();
4634
+ init_pdf_preview_cache();
4635
+ init_catalog();
4636
+ init_tool_image_attachments();
4637
+ init_document_render_helpers();
4638
+ MAX_IMAGE_BYTES = 375e4;
4639
+ SCALE_PRIMARY = 1568;
4640
+ SCALE_FALLBACK = 1024;
4641
+ IMAGE_MEDIA_TYPES = {
4642
+ ".png": "image/png",
4643
+ ".jpg": "image/jpeg",
4644
+ ".jpeg": "image/jpeg",
4645
+ ".gif": "image/gif",
4646
+ ".webp": "image/webp"
4647
+ };
4648
+ OFFICE_EXTENSIONS2 = /* @__PURE__ */ new Set([
4649
+ ".docx",
4650
+ ".doc",
4651
+ ".xlsx",
4652
+ ".xls",
4653
+ ".pptx",
4654
+ ".ppt",
4655
+ ".odt",
4656
+ ".ods",
4657
+ ".odp",
4658
+ ".rtf"
4659
+ ]);
4660
+ createViewDocumentPageTool = ({
4661
+ sessionID,
4662
+ user,
4663
+ exuluConfig
4664
+ }) => {
4665
+ if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
4666
+ const viewDocumentPageExecute = async ({ filename, page, model }, options) => {
4667
+ const safeName = String(filename ?? "").trim();
4668
+ if (!safeName || safeName.includes("..") || safeName.includes("/") || safeName.includes("\\")) {
4669
+ return {
4670
+ error: "Invalid filename \u2014 pass the bare file name exactly as listed in the session files (no paths)."
4671
+ };
4672
+ }
4673
+ const ext = (0, import_node_path8.extname)(safeName).toLowerCase();
4674
+ const isImage = ext in IMAGE_MEDIA_TYPES;
4675
+ const isPdf = ext === ".pdf";
4676
+ const isOffice = OFFICE_EXTENSIONS2.has(ext);
4677
+ if (!isImage && !isPdf && !isOffice) {
4678
+ return { error: `Unsupported extension "${ext}" \u2014 view_document_page handles PDF, Office, and image files.` };
4679
+ }
4680
+ const modelId = typeof model === "string" ? model : model?.modelId;
4681
+ if (!modelId) {
4682
+ console.warn("[EXULU] view_document_page: no model id available for vision gating \u2014 proceeding optimistically.");
4683
+ }
4684
+ if (modelId) {
4685
+ try {
4686
+ const entry = await findLiteLLMModel(modelId);
4687
+ if (entry && entry.supports_vision === false) {
4688
+ return {
4689
+ error: `The current model "${modelId}" does not support images, so this page cannot be shown to you. Use parse_document for the text, or tell the user a vision-capable model is required.`
4690
+ };
4691
+ }
4692
+ } catch {
4693
+ }
4694
+ }
4695
+ const uploads = exuluConfig.fileUploads;
4696
+ const generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
4697
+ const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
4698
+ const pageNumber = page ?? 1;
4699
+ try {
4700
+ let imageBytes;
4701
+ let mediaType = "image/png";
4702
+ if (isImage) {
4703
+ imageBytes = await getS3ObjectBytes(key, exuluConfig);
4704
+ mediaType = IMAGE_MEDIA_TYPES[ext];
4705
+ if (imageBytes.length > MAX_IMAGE_BYTES) {
4706
+ return {
4707
+ error: `"${safeName}" is too large to attach (${imageBytes.length} bytes, max ${MAX_IMAGE_BYTES}). Ask the user for a smaller version of the image.`
4708
+ };
4709
+ }
4710
+ } else {
4711
+ let pdfBytes;
4712
+ if (isPdf) {
4713
+ pdfBytes = await getS3ObjectBytes(key, exuluConfig);
4714
+ } else {
4715
+ const etag = await getS3ObjectEtag(key, exuluConfig);
4716
+ if (!etag) {
4717
+ return { error: `Could not read session file "${safeName}". Check the exact file name.` };
4718
+ }
4719
+ pdfBytes = await getPdfPreviewBytes({ sourceKey: key, etag, config: exuluConfig });
4720
+ }
4721
+ let rendered = await renderPdfPageToPng(pdfBytes, pageNumber, SCALE_PRIMARY);
4722
+ if (!rendered) {
4723
+ return { error: `Could not render page ${pageNumber} of "${safeName}" \u2014 the document may have fewer pages.` };
4724
+ }
4725
+ if (rendered.length > MAX_IMAGE_BYTES) {
4726
+ rendered = await renderPdfPageToPng(pdfBytes, pageNumber, SCALE_FALLBACK);
4727
+ }
4728
+ if (!rendered || rendered.length > MAX_IMAGE_BYTES) {
4729
+ return { error: `Page ${pageNumber} of "${safeName}" is too complex to attach within the image size limit.` };
4730
+ }
4731
+ imageBytes = rendered;
4732
+ }
4733
+ if (!options?.toolCallId) {
4734
+ return { error: "Internal error: missing toolCallId \u2014 the image cannot be attached." };
4735
+ }
4736
+ stashToolImage(options.toolCallId, {
4737
+ data: imageBytes.toString("base64"),
4738
+ mediaType,
4739
+ label: isImage ? safeName : `${safeName} page ${pageNumber}`
4740
+ });
4741
+ return {
4742
+ attached: true,
4743
+ filename: safeName,
4744
+ page: pageNumber,
4745
+ note: "The rendered image follows this tool result as an attached user message \u2014 analyze it there. If no image message follows, the attachment has expired; call this tool again to re-render it."
4746
+ };
4747
+ } catch (err) {
4748
+ return { error: `Failed to render "${safeName}": ${err instanceof Error ? err.message : "unknown error"}` };
4749
+ }
4750
+ };
4751
+ return ExuluTool.internal({
4752
+ id: "view_document_page",
4753
+ name: "view_document_page",
4754
+ needsApproval: false,
4755
+ description: "LOOK at a page of an uploaded PDF/Office document, or at an uploaded image, from this session's files. The rendered image is attached as a user message directly after this tool result so you can visually analyze photos, charts, scans, and layouts. Use parse_document first to find which page you need. Requires a vision-capable model.",
4756
+ inputSchema: import_zod7.z.object({
4757
+ filename: import_zod7.z.string().describe('Exact session file name, e.g. "report.pdf" or "screenshot.png"'),
4758
+ page: import_zod7.z.number().int().min(1).optional().describe("Page number to render (default 1; ignored for image files)")
4759
+ }),
4760
+ type: "function",
4761
+ category: "session",
4762
+ config: [],
4763
+ // Same execute-shape cast as read_session_file / parse_document.
4764
+ execute: viewDocumentPageExecute
4765
+ });
4766
+ };
4767
+ }
4768
+ });
4769
+
4143
4770
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
4144
4771
  var convert_exulu_tools_to_ai_sdk_tools_exports = {};
4145
4772
  __export(convert_exulu_tools_to_ai_sdk_tools_exports, {
@@ -4168,6 +4795,8 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4168
4795
  init_truncate_tool_output();
4169
4796
  init_tool_output_offload();
4170
4797
  init_session_file_read_tool();
4798
+ init_parse_document_tool();
4799
+ init_view_document_page_tool();
4171
4800
  init_context_budget();
4172
4801
  OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
4173
4802
  generateS3Key = (filename) => `${(0, import_node_crypto4.randomUUID)()}-${filename}`;
@@ -4348,6 +4977,14 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4348
4977
  if (sessionFileReadTool && !disabled.has(sessionFileReadTool.id)) {
4349
4978
  currentTools.push(sessionFileReadTool);
4350
4979
  }
4980
+ const parseDocumentTool = createParseDocumentTool({ sessionID, user, exuluConfig });
4981
+ if (parseDocumentTool && !disabled.has(parseDocumentTool.id)) {
4982
+ currentTools.push(parseDocumentTool);
4983
+ }
4984
+ const viewDocumentPageTool = createViewDocumentPageTool({ sessionID, user, exuluConfig });
4985
+ if (viewDocumentPageTool && !disabled.has(viewDocumentPageTool.id)) {
4986
+ currentTools.push(viewDocumentPageTool);
4987
+ }
4351
4988
  console.log("[EXULU] Creating agentic search tool", contexts?.length, model);
4352
4989
  if (contexts?.length && model && !disabled.has("agentic_context_search")) {
4353
4990
  const index = currentTools.findIndex((tool4) => tool4.id === "agentic_context_search");
@@ -4615,13 +5252,13 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
4615
5252
  });
4616
5253
 
4617
5254
  // src/exulu/tool.ts
4618
- var import_ai3, import_zod6, import_node_crypto5, PUBLIC_TOOL_TYPES, ExuluTool;
5255
+ var import_ai3, import_zod8, import_node_crypto5, PUBLIC_TOOL_TYPES, ExuluTool;
4619
5256
  var init_tool = __esm({
4620
5257
  "src/exulu/tool.ts"() {
4621
5258
  "use strict";
4622
5259
  init_cjs_shims();
4623
5260
  import_ai3 = require("ai");
4624
- import_zod6 = require("zod");
5261
+ import_zod8 = require("zod");
4625
5262
  init_sanitize_name();
4626
5263
  import_node_crypto5 = require("crypto");
4627
5264
  init_singleton();
@@ -4678,7 +5315,7 @@ var init_tool = __esm({
4678
5315
  this.type = type;
4679
5316
  this.tool = (0, import_ai3.tool)({
4680
5317
  description,
4681
- inputSchema: inputSchema || import_zod6.z.object({}),
5318
+ inputSchema: inputSchema || import_zod8.z.object({}),
4682
5319
  execute: oauth ? wrapExecuteWithOauth(id, oauth, execute2) : execute2
4683
5320
  });
4684
5321
  }
@@ -4971,62 +5608,62 @@ function effectiveKbSettings(profile, ctx) {
4971
5608
  keywordPrefilter: preset.keywordPrefilter
4972
5609
  };
4973
5610
  }
4974
- var import_zod7, KB_KINDS, DEFAULT_PREFILTER_CUTOFF, RRF_K, CHUNK_GROUP_MAX, kbProfileSchema, knowledgeBasesSchema, routingRuleSchema, routingSchema, identifierSetSchema, vocabularySchema, memorySchema, tuningSchema, boolVal, strVal, KIND_PRESETS;
5611
+ var import_zod9, KB_KINDS, DEFAULT_PREFILTER_CUTOFF, RRF_K, CHUNK_GROUP_MAX, kbProfileSchema, knowledgeBasesSchema, routingRuleSchema, routingSchema, identifierSetSchema, vocabularySchema, memorySchema, tuningSchema, boolVal, strVal, KIND_PRESETS;
4975
5612
  var init_config = __esm({
4976
5613
  "ee/agentic-retrieval/pipeline/config.ts"() {
4977
5614
  "use strict";
4978
5615
  init_cjs_shims();
4979
- import_zod7 = require("zod");
5616
+ import_zod9 = require("zod");
4980
5617
  KB_KINDS = ["documents", "conversations", "records"];
4981
5618
  DEFAULT_PREFILTER_CUTOFF = 2.5;
4982
5619
  RRF_K = 60;
4983
5620
  CHUNK_GROUP_MAX = 10;
4984
- kbProfileSchema = import_zod7.z.object({
4985
- enabled: import_zod7.z.boolean().default(true),
4986
- kind: import_zod7.z.enum(KB_KINDS).default("documents"),
4987
- instructions: import_zod7.z.string().default(""),
4988
- overrides: import_zod7.z.object({
4989
- limit: import_zod7.z.number().int().positive().optional(),
4990
- expand: import_zod7.z.number().int().min(0).optional(),
4991
- multiQuery: import_zod7.z.boolean().optional(),
4992
- hyde: import_zod7.z.boolean().optional()
5621
+ kbProfileSchema = import_zod9.z.object({
5622
+ enabled: import_zod9.z.boolean().default(true),
5623
+ kind: import_zod9.z.enum(KB_KINDS).default("documents"),
5624
+ instructions: import_zod9.z.string().default(""),
5625
+ overrides: import_zod9.z.object({
5626
+ limit: import_zod9.z.number().int().positive().optional(),
5627
+ expand: import_zod9.z.number().int().min(0).optional(),
5628
+ multiQuery: import_zod9.z.boolean().optional(),
5629
+ hyde: import_zod9.z.boolean().optional()
4993
5630
  }).default({})
4994
5631
  });
4995
- knowledgeBasesSchema = import_zod7.z.record(import_zod7.z.string(), kbProfileSchema);
4996
- routingRuleSchema = import_zod7.z.object({
4997
- id: import_zod7.z.string(),
4998
- label: import_zod7.z.string(),
4999
- description: import_zod7.z.string(),
5000
- main: import_zod7.z.array(import_zod7.z.string()),
5001
- fallback: import_zod7.z.array(import_zod7.z.string()).default([])
5632
+ knowledgeBasesSchema = import_zod9.z.record(import_zod9.z.string(), kbProfileSchema);
5633
+ routingRuleSchema = import_zod9.z.object({
5634
+ id: import_zod9.z.string(),
5635
+ label: import_zod9.z.string(),
5636
+ description: import_zod9.z.string(),
5637
+ main: import_zod9.z.array(import_zod9.z.string()),
5638
+ fallback: import_zod9.z.array(import_zod9.z.string()).default([])
5002
5639
  });
5003
- routingSchema = import_zod7.z.object({ rules: import_zod7.z.array(routingRuleSchema).default([]) });
5004
- identifierSetSchema = import_zod7.z.object({
5005
- name: import_zod7.z.string(),
5006
- description: import_zod7.z.string().default(""),
5007
- examples: import_zod7.z.array(import_zod7.z.string()).default([]),
5008
- strategy: import_zod7.z.enum(["fuzzy", "exact"]),
5009
- contexts: import_zod7.z.array(import_zod7.z.string()).default([])
5640
+ routingSchema = import_zod9.z.object({ rules: import_zod9.z.array(routingRuleSchema).default([]) });
5641
+ identifierSetSchema = import_zod9.z.object({
5642
+ name: import_zod9.z.string(),
5643
+ description: import_zod9.z.string().default(""),
5644
+ examples: import_zod9.z.array(import_zod9.z.string()).default([]),
5645
+ strategy: import_zod9.z.enum(["fuzzy", "exact"]),
5646
+ contexts: import_zod9.z.array(import_zod9.z.string()).default([])
5010
5647
  });
5011
- vocabularySchema = import_zod7.z.object({
5012
- glossary: import_zod7.z.array(import_zod7.z.object({ term: import_zod7.z.string(), meaning: import_zod7.z.string() })).default([]),
5013
- identifiers: import_zod7.z.array(identifierSetSchema).default([]),
5014
- rewrites: import_zod7.z.array(import_zod7.z.object({ find: import_zod7.z.string(), replace: import_zod7.z.string() })).default([]),
5015
- styleHint: import_zod7.z.string().default("")
5648
+ vocabularySchema = import_zod9.z.object({
5649
+ glossary: import_zod9.z.array(import_zod9.z.object({ term: import_zod9.z.string(), meaning: import_zod9.z.string() })).default([]),
5650
+ identifiers: import_zod9.z.array(identifierSetSchema).default([]),
5651
+ rewrites: import_zod9.z.array(import_zod9.z.object({ find: import_zod9.z.string(), replace: import_zod9.z.string() })).default([]),
5652
+ styleHint: import_zod9.z.string().default("")
5016
5653
  });
5017
- memorySchema = import_zod7.z.object({
5018
- enabled: import_zod7.z.boolean().default(true),
5019
- override: import_zod7.z.boolean().default(false),
5020
- filePrioritization: import_zod7.z.boolean().default(false),
5021
- queryAugmentation: import_zod7.z.boolean().default(true)
5654
+ memorySchema = import_zod9.z.object({
5655
+ enabled: import_zod9.z.boolean().default(true),
5656
+ override: import_zod9.z.boolean().default(false),
5657
+ filePrioritization: import_zod9.z.boolean().default(false),
5658
+ queryAugmentation: import_zod9.z.boolean().default(true)
5022
5659
  });
5023
- tuningSchema = import_zod7.z.object({
5024
- topK: import_zod7.z.number().int().positive().default(5),
5025
- fallbackThreshold: import_zod7.z.number().min(0).max(1).default(0.95),
5026
- pinBoost: import_zod7.z.number().min(0).max(1).default(0.15),
5027
- identifierBoost: import_zod7.z.number().min(0).max(1).default(0.15),
5028
- pageWindow: import_zod7.z.number().int().min(0).default(1),
5029
- maxQueriesPerContext: import_zod7.z.number().int().positive().default(5)
5660
+ tuningSchema = import_zod9.z.object({
5661
+ topK: import_zod9.z.number().int().positive().default(5),
5662
+ fallbackThreshold: import_zod9.z.number().min(0).max(1).default(0.95),
5663
+ pinBoost: import_zod9.z.number().min(0).max(1).default(0.15),
5664
+ identifierBoost: import_zod9.z.number().min(0).max(1).default(0.15),
5665
+ pageWindow: import_zod9.z.number().int().min(0).default(1),
5666
+ maxQueriesPerContext: import_zod9.z.number().int().positive().default(5)
5030
5667
  });
5031
5668
  boolVal = (v) => v === true || v === "true" || v === 1;
5032
5669
  strVal = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
@@ -5273,9 +5910,9 @@ async function resolveIdentifierPins({
5273
5910
  system: set.strategy === "exact" ? EXACT_EXTRACTION_PROMPT(set) : FUZZY_EXTRACTION_PROMPT(set),
5274
5911
  messages: [{ role: "user", content: question }],
5275
5912
  output: import_ai4.Output.object({
5276
- schema: import_zod8.z.object({
5277
- hasMatches: import_zod8.z.boolean(),
5278
- matches: import_zod8.z.array(import_zod8.z.string()).optional()
5913
+ schema: import_zod10.z.object({
5914
+ hasMatches: import_zod10.z.boolean(),
5915
+ matches: import_zod10.z.array(import_zod10.z.string()).optional()
5279
5916
  })
5280
5917
  }),
5281
5918
  maxOutputTokens: 300
@@ -5323,14 +5960,14 @@ async function resolveIdentifierPins({
5323
5960
  );
5324
5961
  return { pinsByContext, exactPinsByContext, steps };
5325
5962
  }
5326
- var import_fuse, import_ai4, import_zod8, itemCaches, ensureItemsCache, FUZZY_EXTRACTION_PROMPT, EXACT_EXTRACTION_PROMPT;
5963
+ var import_fuse, import_ai4, import_zod10, itemCaches, ensureItemsCache, FUZZY_EXTRACTION_PROMPT, EXACT_EXTRACTION_PROMPT;
5327
5964
  var init_prefilter = __esm({
5328
5965
  "ee/agentic-retrieval/pipeline/prefilter.ts"() {
5329
5966
  "use strict";
5330
5967
  init_cjs_shims();
5331
5968
  import_fuse = __toESM(require("fuse.js"), 1);
5332
5969
  import_ai4 = require("ai");
5333
- import_zod8 = require("zod");
5970
+ import_zod10 = require("zod");
5334
5971
  init_with_retry();
5335
5972
  init_text_utils();
5336
5973
  init_config();
@@ -5415,11 +6052,11 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
5415
6052
  system: buildDocPagePrompt(knownIdentifiers),
5416
6053
  messages: [{ role: "user", content: question }],
5417
6054
  output: import_ai5.Output.object({
5418
- schema: import_zod9.z.object({
5419
- hasFilenameHint: import_zod9.z.boolean(),
5420
- filenameHints: import_zod9.z.array(import_zod9.z.string()).optional(),
5421
- hasPageHint: import_zod9.z.boolean(),
5422
- pageNumber: import_zod9.z.number().int().nullable().optional()
6055
+ schema: import_zod11.z.object({
6056
+ hasFilenameHint: import_zod11.z.boolean(),
6057
+ filenameHints: import_zod11.z.array(import_zod11.z.string()).optional(),
6058
+ hasPageHint: import_zod11.z.boolean(),
6059
+ pageNumber: import_zod11.z.number().int().nullable().optional()
5423
6060
  })
5424
6061
  }),
5425
6062
  maxOutputTokens: 300
@@ -5446,9 +6083,9 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
5446
6083
  temperature: 0,
5447
6084
  system: kbSystemPrompt,
5448
6085
  output: import_ai5.Output.object({
5449
- schema: import_zod9.z.object({
5450
- explicitlyRequestedKnowledgeBases: import_zod9.z.array(
5451
- import_zod9.z.enum(enabledContexts.map((c) => c.id))
6086
+ schema: import_zod11.z.object({
6087
+ explicitlyRequestedKnowledgeBases: import_zod11.z.array(
6088
+ import_zod11.z.enum(enabledContexts.map((c) => c.id))
5452
6089
  )
5453
6090
  })
5454
6091
  }),
@@ -5546,9 +6183,9 @@ ${extraInstructions}
5546
6183
  system: classifyPrompt,
5547
6184
  messages: [{ role: "user", content: question }],
5548
6185
  output: import_ai5.Output.object({
5549
- schema: import_zod9.z.object({
5550
- ruleId: import_zod9.z.enum(ruleIds),
5551
- reason: import_zod9.z.string()
6186
+ schema: import_zod11.z.object({
6187
+ ruleId: import_zod11.z.enum(ruleIds),
6188
+ reason: import_zod11.z.string()
5552
6189
  })
5553
6190
  }),
5554
6191
  maxOutputTokens: 200
@@ -5607,13 +6244,13 @@ ${extraInstructions}
5607
6244
  };
5608
6245
  }
5609
6246
  }
5610
- var import_ai5, import_zod9, MAX_USER_PIN_MATCHES, buildDocPagePrompt;
6247
+ var import_ai5, import_zod11, MAX_USER_PIN_MATCHES, buildDocPagePrompt;
5611
6248
  var init_routing = __esm({
5612
6249
  "ee/agentic-retrieval/pipeline/routing.ts"() {
5613
6250
  "use strict";
5614
6251
  init_cjs_shims();
5615
6252
  import_ai5 = require("ai");
5616
- import_zod9 = require("zod");
6253
+ import_zod11 = require("zod");
5617
6254
  init_with_retry();
5618
6255
  init_prefilter();
5619
6256
  init_text_utils();
@@ -5885,8 +6522,8 @@ async function runMemoryPhase({
5885
6522
  }
5886
6523
  ],
5887
6524
  output: import_ai6.Output.object({
5888
- schema: import_zod10.z.object({
5889
- relevantChunkIds: import_zod10.z.array(import_zod10.z.string()).describe(
6525
+ schema: import_zod12.z.object({
6526
+ relevantChunkIds: import_zod12.z.array(import_zod12.z.string()).describe(
5890
6527
  "The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
5891
6528
  )
5892
6529
  })
@@ -6002,17 +6639,17 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
6002
6639
  }
6003
6640
  ],
6004
6641
  output: import_ai6.Output.object({
6005
- schema: import_zod10.z.object({
6006
- overrides: import_zod10.z.boolean().describe(
6642
+ schema: import_zod12.z.object({
6643
+ overrides: import_zod12.z.boolean().describe(
6007
6644
  "True ONLY if a memory chunk directly and sufficiently answers the user's question and should be authoritative over the documents. Be strict; when unsure, false."
6008
6645
  ),
6009
- confidence: import_zod10.z.enum(["high", "medium", "low"]).describe(
6646
+ confidence: import_zod12.z.enum(["high", "medium", "low"]).describe(
6010
6647
  "Confidence that the selected memory chunk(s) fully and directly answer the question."
6011
6648
  ),
6012
- authoritativeChunkIds: import_zod10.z.array(import_zod10.z.string()).describe(
6649
+ authoritativeChunkIds: import_zod12.z.array(import_zod12.z.string()).describe(
6013
6650
  "The chunk_ids of the memory chunk(s) that directly answer the question. Empty if overrides is false."
6014
6651
  ),
6015
- reason: import_zod10.z.string().describe(
6652
+ reason: import_zod12.z.string().describe(
6016
6653
  "One short sentence: why this memory does or does not directly answer the question."
6017
6654
  )
6018
6655
  })
@@ -6043,9 +6680,9 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
6043
6680
  system: "You are a helpful assistant that will strictly follow the user's instructions.",
6044
6681
  messages: [{ role: "user", content: PROMPT_EXTRACT_PRIORITIZED_FILES }],
6045
6682
  output: import_ai6.Output.object({
6046
- schema: import_zod10.z.object({
6047
- shouldPrioritizeFiles: import_zod10.z.boolean(),
6048
- fileNameHints: import_zod10.z.array(import_zod10.z.string()).optional()
6683
+ schema: import_zod12.z.object({
6684
+ shouldPrioritizeFiles: import_zod12.z.boolean(),
6685
+ fileNameHints: import_zod12.z.array(import_zod12.z.string()).optional()
6049
6686
  })
6050
6687
  }),
6051
6688
  maxOutputTokens: 300
@@ -6064,10 +6701,10 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
6064
6701
  system: "You are a helpful assistant that will strictly follow the user's instructions.",
6065
6702
  messages: [{ role: "user", content: QUERY_AUGMENTATION_PROMPT }],
6066
6703
  output: import_ai6.Output.object({
6067
- schema: import_zod10.z.object({
6068
- updatedUserQuestion: import_zod10.z.string(),
6069
- updatedRelevantKeywords: import_zod10.z.array(import_zod10.z.string()),
6070
- updatedImportantKeyword: import_zod10.z.string()
6704
+ schema: import_zod12.z.object({
6705
+ updatedUserQuestion: import_zod12.z.string(),
6706
+ updatedRelevantKeywords: import_zod12.z.array(import_zod12.z.string()),
6707
+ updatedImportantKeyword: import_zod12.z.string()
6071
6708
  })
6072
6709
  }),
6073
6710
  maxOutputTokens: 600
@@ -6153,13 +6790,13 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
6153
6790
  return neutralResult(question, keywords, importantKeyword);
6154
6791
  }
6155
6792
  }
6156
- var import_ai6, import_zod10, MEMORY_OVERRIDE_MIN_CONFIDENCE, MEMORY_SYNTHETIC_RERANK_SCORE, ITEM_CACHE_TTL_MS, memoryItemCache;
6793
+ var import_ai6, import_zod12, MEMORY_OVERRIDE_MIN_CONFIDENCE, MEMORY_SYNTHETIC_RERANK_SCORE, ITEM_CACHE_TTL_MS, memoryItemCache;
6157
6794
  var init_memory = __esm({
6158
6795
  "ee/agentic-retrieval/pipeline/memory.ts"() {
6159
6796
  "use strict";
6160
6797
  init_cjs_shims();
6161
6798
  import_ai6 = require("ai");
6162
- import_zod10 = require("zod");
6799
+ import_zod12 = require("zod");
6163
6800
  init_with_retry();
6164
6801
  init_multi_query();
6165
6802
  init_prefilter();
@@ -6655,11 +7292,11 @@ function createAgenticRetrievalTool(opts) {
6655
7292
  default: '{"topK":5,"fallbackThreshold":0.95,"pinBoost":0.15,"identifierBoost":0.15,"pageWindow":1,"maxQueriesPerContext":5}'
6656
7293
  }
6657
7294
  ],
6658
- inputSchema: import_zod11.z.object({
6659
- userQuery: import_zod11.z.string().describe("The original unaltered question from the user"),
6660
- relevantKeywords: import_zod11.z.array(import_zod11.z.string()).describe("Keywords extracted from the user's question relevant to the search"),
6661
- importantKeyword: import_zod11.z.string().describe("The single most important keyword from the user's question"),
6662
- confirmedContextIds: import_zod11.z.array(import_zod11.z.string()).optional().describe(
7295
+ inputSchema: import_zod13.z.object({
7296
+ userQuery: import_zod13.z.string().describe("The original unaltered question from the user"),
7297
+ relevantKeywords: import_zod13.z.array(import_zod13.z.string()).describe("Keywords extracted from the user's question relevant to the search"),
7298
+ importantKeyword: import_zod13.z.string().describe("The single most important keyword from the user's question"),
7299
+ confirmedContextIds: import_zod13.z.array(import_zod13.z.string()).optional().describe(
6663
7300
  "Knowledge base IDs explicitly confirmed by the user to be used in the retrieval. When present, only searches these contexts."
6664
7301
  )
6665
7302
  }),
@@ -7050,12 +7687,12 @@ Verified answer:
7050
7687
  }
7051
7688
  });
7052
7689
  }
7053
- var import_zod11;
7690
+ var import_zod13;
7054
7691
  var init_pipeline = __esm({
7055
7692
  "ee/agentic-retrieval/pipeline/index.ts"() {
7056
7693
  "use strict";
7057
7694
  init_cjs_shims();
7058
- import_zod11 = require("zod");
7695
+ import_zod13 = require("zod");
7059
7696
  init_tool();
7060
7697
  init_entitlements();
7061
7698
  init_resolve_reranker();
@@ -7073,91 +7710,6 @@ var init_pipeline = __esm({
7073
7710
  }
7074
7711
  });
7075
7712
 
7076
- // src/exulu/litellm/catalog.ts
7077
- var catalog_exports = {};
7078
- __export(catalog_exports, {
7079
- __resetLiteLLMCatalogCacheForTesting: () => __resetLiteLLMCatalogCacheForTesting,
7080
- fetchLiteLLMCatalog: () => fetchLiteLLMCatalog,
7081
- findLiteLLMModel: () => findLiteLLMModel
7082
- });
7083
- var CACHE_TTL_MS, _cache, __resetLiteLLMCatalogCacheForTesting, fetchLiteLLMCatalog, findLiteLLMModel;
7084
- var init_catalog = __esm({
7085
- "src/exulu/litellm/catalog.ts"() {
7086
- "use strict";
7087
- init_cjs_shims();
7088
- CACHE_TTL_MS = 3e4;
7089
- __resetLiteLLMCatalogCacheForTesting = () => {
7090
- _cache = void 0;
7091
- };
7092
- fetchLiteLLMCatalog = async () => {
7093
- if (process.env.EXULU_USE_LITELLM !== "true") return [];
7094
- if (_cache && _cache.expiresAt > Date.now()) {
7095
- return _cache.items;
7096
- }
7097
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
7098
- const port = process.env.LITELLM_PORT ?? "4000";
7099
- const masterKey = process.env.LITELLM_MASTER_KEY;
7100
- if (!masterKey) return [];
7101
- try {
7102
- const res = await fetch(`http://${host}:${port}/model/info`, {
7103
- method: "GET",
7104
- headers: { Authorization: `Bearer ${masterKey}` }
7105
- });
7106
- if (!res.ok) {
7107
- console.error(
7108
- `[EXULU] litellmCatalog: LiteLLM /model/info returned ${res.status}`
7109
- );
7110
- return [];
7111
- }
7112
- const json = await res.json();
7113
- const items = (Array.isArray(json?.data) ? json.data : []).map((m) => ({
7114
- // filter out trailing * from model_name
7115
- model_name: m.model_name.replace(/\*$/, ""),
7116
- upstream_model: m.litellm_params?.model ?? null,
7117
- tags: Array.isArray(m.model_info?.tags) ? m.model_info.tags : [],
7118
- brand: m.model_info?.brand ?? null,
7119
- type: m.model_info?.type ?? null,
7120
- region: m.model_info?.region ?? null,
7121
- max_tokens: m.model_info?.max_tokens ?? null,
7122
- max_input_tokens: m.model_info?.max_input_tokens ?? null,
7123
- input_cost_per_million_tokens: m.model_info?.input_cost_per_token * 1e6,
7124
- output_cost_per_million_tokens: m.model_info?.output_cost_per_token * 1e6,
7125
- active: m.model_info?.active ?? true,
7126
- max_output_tokens: m.model_info?.max_output_tokens ?? null,
7127
- supports_vision: !!m.model_info?.supports_vision,
7128
- supports_function_calling: !!m.model_info?.supports_function_calling,
7129
- supports_pdf_input: !!m.model_info?.supports_pdf_input,
7130
- supports_audio_input: !!m.model_info?.supports_audio_input,
7131
- sizes: Array.isArray(m.model_info?.sizes) ? m.model_info.sizes : null,
7132
- qualities: Array.isArray(m.model_info?.qualities) ? m.model_info.qualities : null,
7133
- supports_edit: !!m.model_info?.supports_edit,
7134
- max_n: typeof m.model_info?.max_n === "number" ? m.model_info.max_n : null
7135
- }));
7136
- const map = /* @__PURE__ */ new Map();
7137
- for (const item of items) {
7138
- const key = `${item.model_name}-${item.upstream_model}`;
7139
- if (map.has(key)) {
7140
- map.get(key).tags.push(...item.tags);
7141
- } else {
7142
- map.set(key, item);
7143
- }
7144
- }
7145
- const uniqueItems = Array.from(map.values());
7146
- _cache = { expiresAt: Date.now() + CACHE_TTL_MS, items: uniqueItems };
7147
- return uniqueItems.filter((m) => m.type !== "speech_to_text" && m.type !== "text_to_speech");
7148
- } catch (err) {
7149
- console.error("[EXULU] litellmCatalog: failed to fetch /model/info:", err);
7150
- return [];
7151
- }
7152
- };
7153
- findLiteLLMModel = async (modelName) => {
7154
- if (!modelName) return void 0;
7155
- const items = await fetchLiteLLMCatalog();
7156
- return items.find((m) => m.model_name === modelName);
7157
- };
7158
- }
7159
- });
7160
-
7161
7713
  // src/index.ts
7162
7714
  var index_exports = {};
7163
7715
  __export(index_exports, {
@@ -18415,28 +18967,14 @@ var BundleValidationError = class extends Error {
18415
18967
  function isUnsafePath(path2) {
18416
18968
  if (!path2) return true;
18417
18969
  if (path2.startsWith("/")) return true;
18418
- return path2.split("/").some((segment) => segment === "..");
18419
- }
18420
- function isOsJunkPath(path2) {
18421
- if (path2.startsWith("__MACOSX/")) return true;
18422
- const basename = path2.split("/").pop() ?? "";
18423
- return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
18424
- }
18425
- async function extractBundleToS3(opts) {
18426
- const { bytes, skillId, isZip, config } = opts;
18427
- if (!isZip) {
18428
- await uploadFile(
18429
- bytes,
18430
- `skills/${skillId}/v1/SKILL.md`,
18431
- config,
18432
- { contentType: "text/markdown" },
18433
- void 0,
18434
- void 0,
18435
- true
18436
- // global=true so the key isn't user-prefixed (skill files are shared)
18437
- );
18438
- return { filesCount: 1 };
18439
- }
18970
+ return path2.split("/").some((segment) => segment === "..");
18971
+ }
18972
+ function isOsJunkPath(path2) {
18973
+ if (path2.startsWith("__MACOSX/")) return true;
18974
+ const basename = path2.split("/").pop() ?? "";
18975
+ return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
18976
+ }
18977
+ async function extractZipToPrefix(bytes, prefix, config) {
18440
18978
  let zip;
18441
18979
  try {
18442
18980
  zip = await import_jszip.default.loadAsync(bytes);
@@ -18500,7 +19038,7 @@ async function extractBundleToS3(opts) {
18500
19038
  }
18501
19039
  let filesCount = 0;
18502
19040
  for (const { relPath, content } of prepared) {
18503
- const s3Key = `skills/${skillId}/v1/${relPath}`;
19041
+ const s3Key = `${prefix}${relPath}`;
18504
19042
  await uploadFile(
18505
19043
  content,
18506
19044
  s3Key,
@@ -18509,86 +19047,80 @@ async function extractBundleToS3(opts) {
18509
19047
  void 0,
18510
19048
  void 0,
18511
19049
  true
18512
- // global=true — see SKILL.md case above
19050
+ // global=true — skill files are shared across users
18513
19051
  );
18514
19052
  filesCount += 1;
18515
19053
  }
18516
19054
  return { filesCount };
18517
19055
  }
19056
+ async function extractBundleToS3(opts) {
19057
+ const { bytes, skillId, isZip, config } = opts;
19058
+ if (!isZip) {
19059
+ await uploadFile(
19060
+ bytes,
19061
+ `skills/${skillId}/v1/SKILL.md`,
19062
+ config,
19063
+ { contentType: "text/markdown" },
19064
+ void 0,
19065
+ void 0,
19066
+ true
19067
+ // global=true so the key isn't user-prefixed (skill files are shared)
19068
+ );
19069
+ return { filesCount: 1 };
19070
+ }
19071
+ return extractZipToPrefix(bytes, `skills/${skillId}/v1/`, config);
19072
+ }
19073
+ async function extractBundleToVersion(opts) {
19074
+ const { bytes, skillId, version, config } = opts;
19075
+ return extractZipToPrefix(bytes, `skills/${skillId}/v${version}/`, config);
19076
+ }
18518
19077
 
18519
- // src/sessions/pdf-preview-cache.ts
19078
+ // src/skills/frontmatter.ts
18520
19079
  init_cjs_shims();
18521
- var import_node_child_process4 = require("child_process");
18522
- var import_node_fs6 = require("fs");
18523
- var import_promises2 = require("fs/promises");
18524
- var import_node_path5 = require("path");
18525
- var import_node_util3 = require("util");
18526
- init_uppy();
18527
- var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.exec);
18528
- var CACHE_ROOT = "/tmp/exulu-pdf-cache";
18529
- var CACHE_IN = (0, import_node_path5.join)(CACHE_ROOT, "_in");
18530
- var CACHE_OUT = (0, import_node_path5.join)(CACHE_ROOT, "_out");
18531
- var inFlight = /* @__PURE__ */ new Map();
18532
- var PreviewRenderError = class extends Error {
18533
- constructor(message) {
18534
- super(message);
18535
- this.name = "PreviewRenderError";
19080
+ var import_jszip2 = __toESM(require("jszip"), 1);
19081
+ function parseFrontmatter(md) {
19082
+ const match = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
19083
+ if (!match) return {};
19084
+ const block = match[1];
19085
+ const out = {};
19086
+ for (const line of block.split(/\r?\n/)) {
19087
+ const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
19088
+ if (!m) continue;
19089
+ const key = m[1];
19090
+ let v = m[2].trim();
19091
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
19092
+ v = v.slice(1, -1);
19093
+ }
19094
+ out[key] = v;
18536
19095
  }
18537
- };
18538
- function sanitizeEtag(raw) {
18539
- return raw.replace(/^"|"$/g, "").replace(/[^a-zA-Z0-9_-]/g, "_");
19096
+ return out;
18540
19097
  }
18541
- async function getPdfPreviewBytes(opts) {
18542
- const { sourceKey, etag, config } = opts;
18543
- const safeEtag = sanitizeEtag(etag);
18544
- if (!safeEtag) {
18545
- throw new PreviewRenderError(`Invalid ETag for ${sourceKey}`);
18546
- }
18547
- const cachedPath = (0, import_node_path5.join)(CACHE_ROOT, `${safeEtag}.pdf`);
18548
- if ((0, import_node_fs6.existsSync)(cachedPath)) {
18549
- return (0, import_promises2.readFile)(cachedPath);
19098
+ async function parseSkillFrontmatter(zipBytes) {
19099
+ let zip;
19100
+ try {
19101
+ zip = await import_jszip2.default.loadAsync(zipBytes);
19102
+ } catch {
19103
+ return {};
18550
19104
  }
18551
- const existing = inFlight.get(safeEtag);
18552
- if (existing) return existing;
18553
- const promise = (async () => {
18554
- try {
18555
- await (0, import_promises2.mkdir)(CACHE_IN, { recursive: true });
18556
- await (0, import_promises2.mkdir)(CACHE_OUT, { recursive: true });
18557
- const ext = ((0, import_node_path5.extname)(sourceKey) || ".docx").toLowerCase();
18558
- const inputPath = (0, import_node_path5.join)(CACHE_IN, `${safeEtag}${ext}`);
18559
- const outputPath = (0, import_node_path5.join)(CACHE_OUT, `${safeEtag}.pdf`);
18560
- try {
18561
- const bytes = await getS3ObjectBytes(sourceKey, config);
18562
- await (0, import_promises2.writeFile)(inputPath, bytes);
18563
- try {
18564
- await execAsync3(
18565
- `soffice --headless --convert-to pdf "${inputPath}" --outdir "${CACHE_OUT}"`,
18566
- { timeout: 6e4, maxBuffer: 16 * 1024 * 1024 }
18567
- );
18568
- } catch (err) {
18569
- throw new PreviewRenderError(
18570
- `LibreOffice conversion failed for ${sourceKey} (etag ${etag}): ${err?.stderr ?? err?.message ?? "unknown error"}`
18571
- );
18572
- }
18573
- if (!(0, import_node_fs6.existsSync)(outputPath)) {
18574
- throw new PreviewRenderError(
18575
- `LibreOffice produced no output for ${sourceKey} (etag ${etag})`
18576
- );
18577
- }
18578
- await (0, import_promises2.rename)(outputPath, cachedPath);
18579
- return await (0, import_promises2.readFile)(cachedPath);
18580
- } finally {
18581
- await (0, import_promises2.rm)(inputPath, { force: true });
18582
- }
18583
- } finally {
18584
- inFlight.delete(safeEtag);
18585
- }
18586
- })();
18587
- inFlight.set(safeEtag, promise);
18588
- return promise;
19105
+ const paths = [];
19106
+ zip.forEach((p, entry) => {
19107
+ if (!entry.dir) paths.push(p);
19108
+ });
19109
+ const heads = new Set(paths.map((p) => p.split("/")[0]).filter(Boolean));
19110
+ let strip = (p) => p;
19111
+ if (heads.size === 1) {
19112
+ const head = [...heads][0] + "/";
19113
+ if (paths.every((p) => p.startsWith(head))) strip = (p) => p.slice(head.length);
19114
+ }
19115
+ const skillPath = paths.find((p) => strip(p) === "SKILL.md");
19116
+ if (!skillPath) return {};
19117
+ const md = await zip.file(skillPath).async("string");
19118
+ const fm = parseFrontmatter(md);
19119
+ return { name: fm.name, description: fm.description };
18589
19120
  }
18590
19121
 
18591
19122
  // src/exulu/routes.ts
19123
+ init_pdf_preview_cache();
18592
19124
  init_create_sandbox();
18593
19125
  var import_utils5 = require("@apollo/utils.keyvaluecache");
18594
19126
  var import_body_parser = __toESM(require("body-parser"), 1);
@@ -18598,7 +19130,7 @@ var import_fs3 = __toESM(require("fs"), 1);
18598
19130
  var import_node_crypto9 = require("crypto");
18599
19131
  var import_api2 = require("@opentelemetry/api");
18600
19132
  init_check_record_access();
18601
- var import_jszip2 = __toESM(require("jszip"), 1);
19133
+ var import_jszip3 = __toESM(require("jszip"), 1);
18602
19134
  var import_ai15 = require("ai");
18603
19135
  var import_cookie_parser = __toESM(require("cookie-parser"), 1);
18604
19136
  init_statistics2();
@@ -18734,6 +19266,7 @@ function composePrepareSteps(...guards) {
18734
19266
  }
18735
19267
 
18736
19268
  // src/exulu/provider.ts
19269
+ init_tool_image_attachments();
18737
19270
  init_sanitize_tool_name();
18738
19271
 
18739
19272
  // src/exulu/auto-decline-stale-approvals.ts
@@ -18765,7 +19298,7 @@ var autoDeclineStaleApprovals = (messages) => {
18765
19298
  };
18766
19299
 
18767
19300
  // src/exulu/provider.ts
18768
- var import_zod12 = require("zod");
19301
+ var import_zod14 = require("zod");
18769
19302
  init_tool();
18770
19303
  init_resolve_model();
18771
19304
  init_statistics2();
@@ -18785,7 +19318,7 @@ init_check_record_access();
18785
19318
  init_client();
18786
19319
  init_statistics();
18787
19320
  init_convert_exulu_tools_to_ai_sdk_tools();
18788
- var import_officeparser = require("officeparser");
19321
+ var import_officeparser2 = require("officeparser");
18789
19322
  init_singleton();
18790
19323
  init_entitlements();
18791
19324
 
@@ -18914,9 +19447,9 @@ var ExuluProvider = class {
18914
19447
  name: `${agent.name}`,
18915
19448
  type: "agent",
18916
19449
  category: "agents",
18917
- inputSchema: import_zod12.z.object({
18918
- prompt: import_zod12.z.string().describe("The prompt (usually a question for the agent) to send to the agent."),
18919
- information: import_zod12.z.string().describe("A summary of relevant context / information from the current session")
19450
+ inputSchema: import_zod14.z.object({
19451
+ prompt: import_zod14.z.string().describe("The prompt (usually a question for the agent) to send to the agent."),
19452
+ information: import_zod14.z.string().describe("A summary of relevant context / information from the current session")
18920
19453
  }),
18921
19454
  description: `This tool calls an agent named: ${agent.name}. The agent does the following: ${agent.description}.`,
18922
19455
  config: [],
@@ -19223,7 +19756,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
19223
19756
  // Stop after the image_generation tool fires — the widget IS the
19224
19757
  // assistant's response, no follow-up text turn is wanted (same
19225
19758
  // reasoning as question_ask: the UI artifact is the message).
19226
- prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
19759
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
19227
19760
  stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
19228
19761
  });
19229
19762
  console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
@@ -19286,7 +19819,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
19286
19819
  }),
19287
19820
  maxRetries: 2,
19288
19821
  tools,
19289
- prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
19822
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
19290
19823
  stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
19291
19824
  });
19292
19825
  if (statistics) {
@@ -19380,7 +19913,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
19380
19913
  };
19381
19914
  }
19382
19915
  const arrayBuffer = await response.arrayBuffer();
19383
- const extractedText = await (0, import_officeparser.parseOfficeAsync)(arrayBuffer, {
19916
+ const extractedText = await (0, import_officeparser2.parseOfficeAsync)(arrayBuffer, {
19384
19917
  outputErrorToConsole: false,
19385
19918
  newlineDelimiter: "\n"
19386
19919
  });
@@ -19701,7 +20234,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
19701
20234
  );
19702
20235
  },
19703
20236
  // todo allow configuring the step budget per skill
19704
- prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
20237
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
19705
20238
  stopWhen: [(0, import_ai11.stepCountIs)(turnBudget), (0, import_ai11.hasToolCall)("image_generation")]
19706
20239
  });
19707
20240
  return {
@@ -19756,12 +20289,12 @@ var saveChat = async ({
19756
20289
  // src/exulu/suggestions.ts
19757
20290
  init_cjs_shims();
19758
20291
  var import_ai12 = require("ai");
19759
- var import_zod13 = require("zod");
20292
+ var import_zod15 = require("zod");
19760
20293
  var SUGGESTIONS_SYSTEM_PROMPT = "You generate short follow-up message suggestions for the user. You are NOT continuing the conversation as the assistant \u2014 you are predicting what the user might want to say next. Suggest up to 3 short follow-up questions or messages the user might want to send next. Each suggestion must be written from the user's perspective (first person) and be 12 words or fewer. You MUST submit your answer by calling the `submit_suggestions` tool exactly once. Do not emit any plain text \u2014 only the tool call.";
19761
20294
  var submitSuggestionsTool = (0, import_ai12.tool)({
19762
20295
  description: "Submit the final list of follow-up message suggestions for the user. Must be called exactly once. Each suggestion is written from the user's perspective (first person) and is 12 words or fewer.",
19763
- inputSchema: import_zod13.z.object({
19764
- suggestions: import_zod13.z.array(import_zod13.z.string()).max(3)
20296
+ inputSchema: import_zod15.z.object({
20297
+ suggestions: import_zod15.z.array(import_zod15.z.string()).max(3)
19765
20298
  })
19766
20299
  });
19767
20300
  var MAX_CHARS_PER_MESSAGE = 1e4;
@@ -20323,7 +20856,7 @@ See docs/superpowers/specs/2026-05-31-in-chat-image-generation-design.md for the
20323
20856
  };
20324
20857
 
20325
20858
  // src/exulu/routes.ts
20326
- var import_node_path6 = require("path");
20859
+ var import_node_path9 = require("path");
20327
20860
  init_tags();
20328
20861
  init_admin_client();
20329
20862
  init_env();
@@ -20452,6 +20985,7 @@ init_resolve_model();
20452
20985
  init_sanitize_tool_name();
20453
20986
  init_context_budget();
20454
20987
  init_supervisor();
20988
+ init_tool_image_attachments();
20455
20989
  function convertOpenAIToolsToAiSdkTools(tools) {
20456
20990
  return Object.fromEntries(
20457
20991
  tools.map((t) => {
@@ -20858,7 +21392,7 @@ ${project.description}` : ""}` : "",
20858
21392
  messages: coreMessages,
20859
21393
  tools: hasTools ? activeTools : void 0,
20860
21394
  maxRetries: 2,
20861
- prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
21395
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
20862
21396
  stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)],
20863
21397
  onError: (error) => {
20864
21398
  console.error("[OPENAI GATEWAY] stream error:", error);
@@ -20898,7 +21432,7 @@ ${project.description}` : ""}` : "",
20898
21432
  messages: coreMessages,
20899
21433
  tools: hasTools ? activeTools : void 0,
20900
21434
  maxRetries: 2,
20901
- prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
21435
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
20902
21436
  stopWhen: clientTools.length > 0 ? void 0 : [(0, import_ai14.stepCountIs)(turnBudget)]
20903
21437
  });
20904
21438
  res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
@@ -21113,6 +21647,158 @@ var contentHeadersFor = (key, contentType, filename) => {
21113
21647
  };
21114
21648
  var getSharedArtifactByName = (db2, name) => db2("shared_artifacts").where({ name }).first();
21115
21649
 
21650
+ // src/skills/skill-access.ts
21651
+ init_cjs_shims();
21652
+ init_check_record_access();
21653
+ async function resolveSkillByName(db2, name) {
21654
+ const row = await db2("skills").where({ name }).first();
21655
+ return row ?? null;
21656
+ }
21657
+ async function canAccessSkill(db2, skill, action, user) {
21658
+ const rbac = await RBACResolver(db2, "skill", skill.id, skill.rights_mode || "private");
21659
+ return checkRecordAccess({ ...skill, RBAC: rbac }, action, user);
21660
+ }
21661
+ async function filterReadableSkills(db2, skills, user) {
21662
+ const out = [];
21663
+ for (const s of skills) {
21664
+ if (await canAccessSkill(db2, s, "read", user)) out.push(s);
21665
+ }
21666
+ return out;
21667
+ }
21668
+
21669
+ // src/skills/bootstrap/imp-skills.ts
21670
+ init_cjs_shims();
21671
+
21672
+ // src/skills/bootstrap/clients.ts
21673
+ init_cjs_shims();
21674
+ var CLIENT_MANIFEST = [
21675
+ { id: "agents", dir: ".agents/skills" },
21676
+ // cross-agent standard (symlink canonical store)
21677
+ { id: "claude", dir: ".claude/skills" },
21678
+ { id: "windsurf", dir: ".windsurf/skills" },
21679
+ { id: "continue", dir: ".continue/skills" },
21680
+ { id: "roo", dir: ".roo/skills" },
21681
+ { id: "kilocode", dir: ".kilocode/skills" },
21682
+ { id: "crush", dir: ".crush/skills" },
21683
+ { id: "goose", dir: ".goose/skills" },
21684
+ { id: "qwen", dir: ".qwen/skills" },
21685
+ { id: "iflow", dir: ".iflow/skills" },
21686
+ { id: "junie", dir: ".junie/skills" },
21687
+ { id: "kiro", dir: ".kiro/skills" },
21688
+ { id: "trae", dir: ".trae/skills" },
21689
+ { id: "augment", dir: ".augment/skills" },
21690
+ { id: "factory", dir: ".factory/skills" },
21691
+ { id: "devin", dir: ".devin/skills" },
21692
+ { id: "openhands", dir: ".openhands/skills" },
21693
+ { id: "pi", dir: ".pi/skills" },
21694
+ { id: "cortex", dir: ".cortex/skills" },
21695
+ { id: "zencoder", dir: ".zencoder/skills" },
21696
+ { id: "codebuddy", dir: ".codebuddy/skills" },
21697
+ { id: "codestudio", dir: ".codestudio/skills" },
21698
+ { id: "commandcode", dir: ".commandcode/skills" },
21699
+ { id: "codemaker", dir: ".codemaker/skills" },
21700
+ { id: "codeartsdoer", dir: ".codeartsdoer/skills" },
21701
+ { id: "lingma", dir: ".lingma/skills" },
21702
+ { id: "qoder", dir: ".qoder/skills" },
21703
+ { id: "rovodev", dir: ".rovodev/skills" },
21704
+ { id: "moxby", dir: ".moxby/skills" },
21705
+ { id: "mux", dir: ".mux/skills" },
21706
+ { id: "neovate", dir: ".neovate/skills" },
21707
+ { id: "ona", dir: ".ona/skills" },
21708
+ { id: "pochi", dir: ".pochi/skills" },
21709
+ { id: "reasonix", dir: ".reasonix/skills" },
21710
+ { id: "terramind", dir: ".terramind/skills" },
21711
+ { id: "tinycloud", dir: ".tinycloud/skills" },
21712
+ { id: "vibe", dir: ".vibe/skills" },
21713
+ { id: "adal", dir: ".adal/skills" },
21714
+ { id: "aider-desk", dir: ".aider-desk/skills" },
21715
+ { id: "autohand", dir: ".autohand/skills" },
21716
+ { id: "bob", dir: ".bob/skills" },
21717
+ { id: "hermes", dir: ".hermes/skills" },
21718
+ { id: "inferencesh", dir: ".inferencesh/skills" },
21719
+ { id: "jazz", dir: ".jazz/skills" },
21720
+ { id: "kode", dir: ".kode/skills" },
21721
+ { id: "mcpjam", dir: ".mcpjam/skills" },
21722
+ { id: "forge", dir: ".forge/skills" },
21723
+ { id: "tabnine", dir: ".tabnine/agent/skills" }
21724
+ // exception: nested under agent/
21725
+ ];
21726
+
21727
+ // src/skills/bootstrap/imp-sh.generated.ts
21728
+ init_cjs_shims();
21729
+ var IMP_SH_B64 = "IyEvYmluL3NoCiMgaW1wIOKAlCBoZWxwZXIgZm9yIHRoZSBjZW50cmFsIHNraWxsIGxpYnJhcnkuIFRoZSBhZ2VudCBpbnZva2VzIHRoaXMKIyAobmV2ZXIgcmF3IGN1cmwpOiB0aGUgdG9rZW4gaXMgcmVhZCBmcm9tIHRoZSBjb25maWcgZmlsZSBoZXJlIGFuZCBzZW50IHZpYQojIGB4LWFwaS1rZXk6IEJlYXJlcmAsIHNvIGl0IG5ldmVyIGVudGVycyB0aGUgbW9kZWwgY29udGV4dC4gQ2xpZW50IGZhbi1vdXQKIyAoY29weS9zeW1saW5rIGFjcm9zcyBhZ2VudCBjbGllbnRzKSBpcyBkZXRlcm1pbmlzdGljLgpzZXQgLWV1CgpDT05GSUdfRElSPSIkSE9NRS8uY29uZmlnL2ltcCIKQ09ORklHX0ZJTEU9IiRDT05GSUdfRElSL3NraWxscy5qc29uIgoKZGllKCkgeyBwcmludGYgJ2ltcDogJXNcbicgIiQqIiA+JjI7IGV4aXQgMTsgfQppbmZvKCkgeyBwcmludGYgJyVzXG4nICIkKiIgPiYyOyB9Cgpqc29uX3N0cigpIHsgIyBqc29uX3N0ciA8a2V5PiA8ZmlsZT4g4oCUIGZsYXQgImtleSI6InZhbHVlIgogIHNlZCAtbiAicy8uKlwiJDFcIltbOnNwYWNlOl1dKjpbWzpzcGFjZTpdXSpcIlxcKFteXCJdKlxcKVwiLiovXFwxL3AiICIkMiIgfCBoZWFkIC1uMQp9CgpbIC1mICIkQ09ORklHX0ZJTEUiIF0gfHwgZGllICJub3QgY29uZmlndXJlZCDigJQgcnVuOiBjdXJsIC1mc1NMIDxiYXNlX3VybD4vYXBpL3NraWxscy9pbnN0YWxsLnNoIHwgc2giCgpCQVNFX1VSTD0iJChqc29uX3N0ciBiYXNlX3VybCAiJENPTkZJR19GSUxFIikiCkJBQ0tFTkQ9IiQoanNvbl9zdHIgYmFja2VuZCAiJENPTkZJR19GSUxFIikiCkFQSV9LRVk9IiQoanNvbl9zdHIgYXBpX2tleSAiJENPTkZJR19GSUxFIikiCkxJTktfTU9ERT0iJChqc29uX3N0ciBsaW5rX21vZGUgIiRDT05GSUdfRklMRSIpIgpTQ09QRT0iJChqc29uX3N0ciBzY29wZSAiJENPTkZJR19GSUxFIikiCkNMSUVOVFM9IiQoc2VkIC1uICdzLy4qImNsaWVudHMiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlxbXChbXl1dKlwpXF0uKi9cMS9wJyAiJENPTkZJR19GSUxFIiB8IHRyICcsJyAnICcgfCB0ciAtZCAnIicgfCB0ciAtcyAnICcpIgoKWyAtbiAiJENMSUVOVFMiIF0gfHwgQ0xJRU5UUz0iYWdlbnRzIgpbIC1uICIkTElOS19NT0RFIiBdIHx8IExJTktfTU9ERT0iY29weSIKWyAtbiAiJFNDT1BFIiBdIHx8IFNDT1BFPSJwcm9qZWN0IgoKaWYgWyAteiAiJEJBQ0tFTkQiIF07IHRoZW4KICBbIC1uICIkQkFTRV9VUkwiIF0gfHwgZGllICJjb25maWcgaGFzIG5vIGJhY2tlbmQgYW5kIG5vIGJhc2VfdXJsIgogIEJBQ0tFTkQ9IiQoY3VybCAtZnNTTCAiJEJBU0VfVVJML2FwaS9jb25maWciIHwgc2VkIC1uICdzLy4qImJhY2tlbmQiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKiJcKFteIl0qXCkiLiovXDEvcCcgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJEJBQ0tFTkQiIF0gfHwgZGllICJjb3VsZCBub3QgcmVzb2x2ZSBiYWNrZW5kIGZyb20gJEJBU0VfVVJML2FwaS9jb25maWciCmZpCkJBQ0tFTkQ9IiQocHJpbnRmICclcycgIiRCQUNLRU5EIiB8IHNlZCAnczovKiQ6OicpIgoKUk9PVD0iJFBXRCIKWyAiJFNDT1BFIiA9ICJob21lIiBdICYmIFJPT1Q9IiRIT01FIgoKIyBkaXJfZm9yIENMSUVOVF9JRCAtPiByZWxhdGl2ZSBza2lsbCBkaXIgKGdlbmVyYXRlZCBmcm9tIHRoZSBtYW5pZmVzdCBpbiB0aGUKIyByZWFsIGJ1aWxkOyBhIHJlcHJlc2VudGF0aXZlIHN1YnNldCBoZXJlIGZvciBsb2NhbCB0ZXN0aW5nKS4KZGlyX2ZvcigpIHsKICBjYXNlICIkMSIgaW4KX19ESVJfRk9SX0NBU0VTX18KICAgICopIHJldHVybiAxIDs7CiAgZXNhYwp9CgojIE9uZSAiLi4vIiBwZXIgcGF0aCBjb21wb25lbnQgb2YgJDEgKGEgY2xpZW50IGRpciByZWxhdGl2ZSB0byBST09UKSwgc28KIyBzeW1saW5rcyBhcmUgcmVsYXRpdmUgYW5kIHN1cnZpdmUgYmVpbmcgY29tbWl0dGVkIGFuZCBjaGVja2VkIG91dCBlbHNld2hlcmUuCnJlbF90b19yb290KCkgewogIF91cD0iIjsgX29sZGlmcz0kSUZTOyBJRlM9LwogIGZvciBfc2VnIGluICQxOyBkbyBbIC1uICIkX3NlZyIgXSAmJiBfdXA9Ii4uLyRfdXAiOyBkb25lCiAgSUZTPSRfb2xkaWZzOyBwcmludGYgJyVzJyAiJF91cCIKfQoKYXBpKCkgeyAjIGFwaSA8TUVUSE9EPiA8cGF0aD4gW2V4dHJhIGN1cmwgYXJncy4uLl0gLT4gYm9keSBvbiBzdGRvdXQKICBtPSIkMSI7IHA9IiQyIjsgc2hpZnQgMgogIGN1cmwgLWZzUyAtWCAiJG0iICIkQkFDS0VORCRwIiAtSCAieC1hcGkta2V5OiBCZWFyZXIgJEFQSV9LRVkiICIkQCIKfQoKbWV0YV92ZXJzaW9uKCkgeyAjIG1ldGFfdmVyc2lvbiA8bmFtZT4gLT4gY3VycmVudF92ZXJzaW9uIGZyb20gcmVnaXN0cnkKICBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyQxIiAyPi9kZXYvbnVsbCBcCiAgICB8IHNlZCAtbiAncy8uKiJjdXJyZW50X3ZlcnNpb24iW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlwoWzAtOV1bMC05XSpcKS4qL1wxL3AnIHwgaGVhZCAtbjEKfQoKX21hbmFnZWQoKSB7ICMgX21hbmFnZWQgPGRpcj4gLT4gMCBpZiBzYWZlIHRvIG92ZXJ3cml0ZSAob3VycyBvciBzeW1saW5rIG9yIGFic2VudCkKICBkPSIkMSIKICBpZiBbIC1lICIkZCIgXSAmJiBbICEgLUwgIiRkIiBdICYmIFsgISAtZiAiJGQvLmltcC1za2lsbC5qc29uIiBdOyB0aGVuCiAgICBpbmZvICJza2lwICRkIChleGlzdHMsIG5vdCBtYW5hZ2VkIGJ5IGltcCkiOyByZXR1cm4gMQogIGZpCiAgcmV0dXJuIDAKfQoKX3B1dF9yZWFsKCkgeyAjIF9wdXRfcmVhbCA8ZGVzdD4gPHNyY2Rpcj4gPG1hcmtlci1qc29uPgogIF9tYW5hZ2VkICIkMSIgfHwgcmV0dXJuIDAKICBybSAtcmYgIiQxIjsgbWtkaXIgLXAgIiQxIjsgY3AgLVIgIiQyLy4iICIkMS8iCiAgcHJpbnRmICclc1xuJyAiJDMiID4gIiQxLy5pbXAtc2tpbGwuanNvbiIKfQoKcGxhY2Vfc2tpbGwoKSB7ICMgcGxhY2Vfc2tpbGwgPG5hbWU+IDxzcmNkaXI+IDx2ZXJzaW9uPgogIHBuYW1lPSIkMSI7IHBzcmM9IiQyIjsgcHZlcj0iJHszOi0xfSIKICBtYXJrZXI9J3sgIm5hbWUiOiAiJyIkcG5hbWUiJyIsICJ2ZXJzaW9uIjogJyIkcHZlciInLCAic291cmNlIjogIiciJEJBQ0tFTkQiJyIgfScKICBjYW5vbj0iJFJPT1QvLmFnZW50cy9za2lsbHMvJHBuYW1lIgogIFsgIiRMSU5LX01PREUiID0gInN5bWxpbmsiIF0gJiYgX3B1dF9yZWFsICIkY2Fub24iICIkcHNyYyIgIiRtYXJrZXIiCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgeyBpbmZvICJ1bmtub3duIGNsaWVudDogJGlkIjsgY29udGludWU7IH0KICAgIHBhcmVudD0iJFJPT1QvJGQiOyBkZXN0PSIkcGFyZW50LyRwbmFtZSIKICAgIGlmIFsgIiRMSU5LX01PREUiID0gInN5bWxpbmsiIF0gJiYgWyAiJGlkIiAhPSAiYWdlbnRzIiBdOyB0aGVuCiAgICAgIF9tYW5hZ2VkICIkZGVzdCIgfHwgY29udGludWUKICAgICAgbWtkaXIgLXAgIiRwYXJlbnQiOyBybSAtcmYgIiRkZXN0IgogICAgICByZWw9IiQocmVsX3RvX3Jvb3QgIiRkIikuYWdlbnRzL3NraWxscy8kcG5hbWUiCiAgICAgIGlmIGxuIC1zICIkcmVsIiAiJGRlc3QiIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAibGlua2VkICRkZXN0IC0+ICRyZWwiCiAgICAgIGVsc2UKICAgICAgICBpbmZvICJzeW1saW5rIHVuc3VwcG9ydGVkIGF0ICRkZXN0OyBjb3B5aW5nIgogICAgICAgIF9wdXRfcmVhbCAiJGRlc3QiICIkcHNyYyIgIiRtYXJrZXIiCiAgICAgIGZpCiAgICBlbGlmIFsgIiRMSU5LX01PREUiID0gImNvcHkiIF07IHRoZW4KICAgICAgX3B1dF9yZWFsICIkZGVzdCIgIiRwc3JjIiAiJG1hcmtlciIKICAgIGZpCiAgICAjIHN5bWxpbmsgKyBhZ2VudHM6IGFscmVhZHkgcGxhY2VkIGFzIHRoZSBjYW5vbmljYWwgc3RvcmUgYWJvdmUuCiAgZG9uZQp9CgppbnN0YWxsZWRfbmFtZXMoKSB7ICMgdW5pcXVlIHNraWxsIG5hbWVzIHRoYXQgY2Fycnkgb3VyIG1hcmtlciB1bmRlciBST09UCiAgeyBmaW5kICIkUk9PVC8uYWdlbnRzL3NraWxscyIgLW1heGRlcHRoIDIgLW5hbWUgLmltcC1za2lsbC5qc29uIDI+L2Rldi9udWxsCiAgICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICAgIGQ9IiQoZGlyX2ZvciAiJGlkIikiIHx8IGNvbnRpbnVlCiAgICAgIGZpbmQgIiRST09ULyRkIiAtbWF4ZGVwdGggMiAtbmFtZSAuaW1wLXNraWxsLmpzb24gMj4vZGV2L251bGwKICAgIGRvbmUKICB9IHwgd2hpbGUgcmVhZCAtciBtOyBkbyBqc29uX3N0ciBuYW1lICIkbSI7IGRvbmUgfCBzb3J0IC11Cn0KCm1hcmtlcl92ZXJzaW9uKCkgeyAjIG1hcmtlcl92ZXJzaW9uIDxuYW1lPgogIGZvciBiYXNlIGluICIkUk9PVC8uYWdlbnRzL3NraWxscyI7IGRvCiAgICBbIC1mICIkYmFzZS8kMS8uaW1wLXNraWxsLmpzb24iIF0gJiYgeyBqc29uX3N0ciB2ZXJzaW9uICIkYmFzZS8kMS8uaW1wLXNraWxsLmpzb24iOyByZXR1cm47IH0KICBkb25lCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgY29udGludWUKICAgIGY9IiRST09ULyRkLyQxLy5pbXAtc2tpbGwuanNvbiIKICAgIFsgLWYgIiRmIiBdICYmIHsganNvbl9zdHIgdmVyc2lvbiAiJGYiOyByZXR1cm47IH0KICBkb25lCn0KCmRvX2luc3RhbGwoKSB7ICMgZG9faW5zdGFsbCA8bmFtZT4KICBuYW1lPSIkMSIKICBUTVA9IiQobWt0ZW1wIC1kKSIKICBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyRuYW1lL2Rvd25sb2FkIiAtLW91dHB1dCAiJFRNUC9za2lsbC56aXAiIFwKICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJkb3dubG9hZCBmYWlsZWQgZm9yICckbmFtZScgKDQwMyA9IG5vIGFjY2VzcywgNDA0ID0gdW5rbm93bikiOyB9CiAgbWtkaXIgLXAgIiRUTVAveCIKICB1bnppcCAtcSAiJFRNUC9za2lsbC56aXAiIC1kICIkVE1QL3giIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJiYWQgYXJjaGl2ZSBmb3IgJyRuYW1lJyI7IH0KICBzcmM9IiQoZmluZCAiJFRNUC94IiAtbWluZGVwdGggMSAtbWF4ZGVwdGggMSAtdHlwZSBkIHwgaGVhZCAtbjEpIgogIFsgLW4gIiRzcmMiIF0gfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgInVuZXhwZWN0ZWQgYXJjaGl2ZSBsYXlvdXQgZm9yICckbmFtZSciOyB9CiAgdmVyPSIkKG1ldGFfdmVyc2lvbiAiJG5hbWUiKSIKICBwbGFjZV9za2lsbCAiJG5hbWUiICIkc3JjIiAiJHt2ZXI6LTF9IgogIHJtIC1yZiAiJFRNUCIKICBpbmZvICJpbnN0YWxsZWQgJG5hbWUgKHYke3ZlcjotMX0pIFskTElOS19NT0RFXSBpbnRvOiAkQ0xJRU5UUyIKfQoKdXNhZ2UoKSB7CiAgY2F0ID4mMiA8PEVPRgppbXAg4oCUIHNraWxsIGxpYnJhcnkgaGVscGVyCiAgaW1wIGxpc3QgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0IHNraWxscyB5b3UgY2FuIGFjY2VzcyAoSlNPTikKICBpbXAgZ2V0IDxuYW1lPiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNob3cgb25lIHNraWxsJ3MgbWV0YWRhdGEgKEpTT04pCiAgaW1wIGluc3RhbGwgPG5hbWU+ICAgICAgICAgICAgICAgICAgICAgICBpbnN0YWxsL3JlZnJlc2ggYSBza2lsbCBpbnRvIHlvdXIgYWdlbnQgY2xpZW50cwogIGltcCB1cGRhdGUgWzxuYW1lPl0gICAgICAgICAgICAgICAgICAgICAgdXBkYXRlIGluc3RhbGxlZCBza2lsbHMgKGFsbCwgb3Igb25lKSB0byBsYXRlc3QKICBpbXAgcHVibGlzaCA8bmFtZT4gPGRpcj4gPHB1YmxpY3xwcml2YXRlPiBwdWJsaXNoIGEgbG9jYWwgc2tpbGwgZm9sZGVyIGFzIDxuYW1lPgogIGltcCBjb25maWcgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2hvdyByZXNvbHZlZCBiYWNrZW5kIC8gc2NvcGUgLyBjbGllbnRzIChubyBzZWNyZXRzKQpFT0YKfQoKY21kPSIkezE6LWhlbHB9IgpbICQjIC1ndCAwIF0gJiYgc2hpZnQgfHwgdHJ1ZQoKY2FzZSAiJGNtZCIgaW4KICBsaXN0KSBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5IiA7OwogIGdldCkgWyAkIyAtZ2UgMSBdIHx8IGRpZSAidXNhZ2U6IGltcCBnZXQgPG5hbWU+IjsgYXBpIEdFVCAiL3NraWxscy9yZWdpc3RyeS8kMSIgOzsKICBpbnN0YWxsKSBbICQjIC1nZSAxIF0gfHwgZGllICJ1c2FnZTogaW1wIGluc3RhbGwgPG5hbWU+IjsgZG9faW5zdGFsbCAiJDEiIDs7CiAgdXBkYXRlKQogICAgaWYgWyAkIyAtZ2UgMSBdOyB0aGVuIG5hbWVzPSIkMSI7IGVsc2UgbmFtZXM9IiQoaW5zdGFsbGVkX25hbWVzKSI7IGZpCiAgICBbIC1uICIkbmFtZXMiIF0gfHwgeyBpbmZvICJubyBpbXAtbWFuYWdlZCBza2lsbHMgZm91bmQgdW5kZXIgJFJPT1QiOyBleGl0IDA7IH0KICAgIGZvciBuIGluICRuYW1lczsgZG8KICAgICAgY3VyPSIkKG1hcmtlcl92ZXJzaW9uICIkbiIpIgogICAgICBsYXRlc3Q9IiQobWV0YV92ZXJzaW9uICIkbiIpIgogICAgICBbIC1uICIkbGF0ZXN0IiBdIHx8IHsgaW5mbyAic2tpcCAkbiAobm90IGluIHJlZ2lzdHJ5KSI7IGNvbnRpbnVlOyB9CiAgICAgIGlmIFsgLXogIiRjdXIiIF0gfHwgWyAiJGxhdGVzdCIgLWd0ICIkY3VyIiBdIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAidXBkYXRpbmcgJG46IHYke2N1cjotP30gLT4gdiRsYXRlc3QiOyBkb19pbnN0YWxsICIkbiIKICAgICAgZWxzZQogICAgICAgIGluZm8gIiRuIHVwIHRvIGRhdGUgKHYkY3VyKSIKICAgICAgZmkKICAgIGRvbmUKICAgIDs7CiAgcHVibGlzaCkKICAgIFsgJCMgLWdlIDMgXSB8fCBkaWUgInVzYWdlOiBpbXAgcHVibGlzaCA8bmFtZT4gPGRpcj4gPHB1YmxpY3xwcml2YXRlPiIKICAgIG5hbWU9IiQxIjsgZm9sZGVyPSIkMiI7IHZpc2liaWxpdHk9IiQzIgogICAgY2FzZSAiJHZpc2liaWxpdHkiIGluCiAgICAgIHB1YmxpY3xwcml2YXRlKSA7OwogICAgICAqKSBkaWUgInZpc2liaWxpdHkgbXVzdCBiZSAncHVibGljJyBvciAncHJpdmF0ZScgKGFzayB0aGUgdXNlciB3aGljaCB0aGV5IHdhbnQpIiA7OwogICAgZXNhYwogICAgWyAtZCAiJGZvbGRlciIgXSB8fCBkaWUgIm5vIHN1Y2ggZm9sZGVyOiAkZm9sZGVyIgogICAgWyAtZiAiJGZvbGRlci9TS0lMTC5tZCIgXSB8fCBkaWUgIiRmb2xkZXIgaGFzIG5vIFNLSUxMLm1kIGF0IGl0cyByb290IgogICAgY29tbWFuZCAtdiB6aXAgPi9kZXYvbnVsbCAyPiYxIHx8IGRpZSAidGhlICd6aXAnIGNvbW1hbmQgaXMgcmVxdWlyZWQgdG8gcHVibGlzaCIKICAgIFRNUD0iJChta3RlbXAgLWQpIgogICAgKCBjZCAiJChkaXJuYW1lICIkZm9sZGVyIikiIFwKICAgICAgJiYgemlwIC1xIC1yIC1YICIkVE1QL3NraWxsLnppcCIgIiQoYmFzZW5hbWUgIiRmb2xkZXIiKSIgXAogICAgICAgICAgIC14ICcqLy5pbXAtc2tpbGwuanNvbicgJyovLmdpdC8qJyAnKi5EU19TdG9yZScgJyovX19NQUNPU1gvKicgKSBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJjb3VsZCBub3QgemlwICRmb2xkZXIiOyB9CiAgICBhcGkgUE9TVCAiL3NraWxscy9yZWdpc3RyeS8kbmFtZT92aXNpYmlsaXR5PSR2aXNpYmlsaXR5IiAtSCAiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi96aXAiIFwKICAgICAgLS1kYXRhLWJpbmFyeSBAIiRUTVAvc2tpbGwuemlwIiBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJwdWJsaXNoIGZhaWxlZCAoNDAzID0gbm8gd3JpdGUgYWNjZXNzLCA0MDkgPSBuYW1lIHRha2VuKSI7IH0KICAgIHJtIC1yZiAiJFRNUCIKICAgIGluZm8gInB1Ymxpc2hlZCAkbmFtZSAoJHZpc2liaWxpdHkpIgogICAgOzsKICBjb25maWcpCiAgICBwcmludGYgJ2JhY2tlbmQ9JXNcbnNjb3BlPSVzXG5saW5rX21vZGU9JXNcbmNsaWVudHM9JXNcbmFwaV9rZXk9JXNcbicgXAogICAgICAiJEJBQ0tFTkQiICIkU0NPUEUiICIkTElOS19NT0RFIiAiJENMSUVOVFMiIFwKICAgICAgIiQoWyAtbiAiJEFQSV9LRVkiIF0gJiYgZWNobyBzZXQgfHwgZWNobyBNSVNTSU5HKSIKICAgIDs7CiAgaGVscHwtLWhlbHB8LWgpIHVzYWdlIDs7CiAgKikgaW5mbyAidW5rbm93biBjb21tYW5kOiAkY21kIjsgdXNhZ2U7IGV4aXQgMiA7Owplc2FjCg==";
21730
+
21731
+ // src/skills/bootstrap/imp-skills.ts
21732
+ var BOOTSTRAP_CLIENTS_JSON = JSON.stringify(CLIENT_MANIFEST, null, 2);
21733
+ var DIR_FOR_CASES = CLIENT_MANIFEST.map(
21734
+ (c) => ` ${c.id}) printf '%s' '${c.dir}' ;;`
21735
+ ).join("\n");
21736
+ var BOOTSTRAP_IMP_SH = Buffer.from(IMP_SH_B64, "base64").toString("utf8").replace("__DIR_FOR_CASES__", DIR_FOR_CASES);
21737
+ var BOOTSTRAP_SKILL_MD = `---
21738
+ name: imp-skills
21739
+ description: Install, update, and publish skills from this instance's central skill library. Use when the user asks to install a skill, get the latest version of a skill, list IMP skills, or publish a skill to IMP.
21740
+ ---
21741
+
21742
+ # IMP Skills
21743
+
21744
+ Bridge to the IMP central skill library. **All operations go through the
21745
+ bundled helper script \u2014 do not hand-write curl or copy files yourself.** The
21746
+ script reads the API token from config (keeping it out of this conversation) and
21747
+ handles the multi-client copy/symlink fan-out deterministically.
21748
+
21749
+ ## The helper
21750
+
21751
+ Run the script next to this file, \`scripts/imp\`, with \`sh\` and the absolute
21752
+ path of this skill's directory:
21753
+
21754
+ \`\`\`
21755
+ sh "<this-skill-dir>/scripts/imp" <command>
21756
+ \`\`\`
21757
+
21758
+ Commands:
21759
+ - \`list\` \u2014 skills you can access (JSON on stdout)
21760
+ - \`get <name>\` \u2014 one skill's metadata (JSON)
21761
+ - \`install <name>\` \u2014 install/refresh a skill into the user's agent clients
21762
+ - \`update [<name>]\` \u2014 update every installed skill, or just \`<name>\`, to latest
21763
+ - \`publish <name> <folder> <public|private>\` \u2014 publish a local skill folder as \`<name>\`
21764
+ - \`config\` \u2014 show resolved backend / scope / clients (prints no secrets)
21765
+
21766
+ The token, backend URL, target clients, copy-vs-symlink mode, and scope all come
21767
+ from \`~/.config/imp/skills.json\` (written by the installer). Never print the
21768
+ \`api_key\` or read it into your reply \u2014 the script uses it internally.
21769
+
21770
+ ## Requests \u2192 commands
21771
+
21772
+ - "list / search skills" \u2192 \`imp list\`, then filter the JSON for the user.
21773
+ - "install skill X" / "add the X skill" \u2192 \`imp install X\`.
21774
+ - "update / get the latest version [of X]" \u2192 \`imp update [X]\`.
21775
+ - "publish / upload this skill as X" \u2192 confirm the target name with the user; for
21776
+ an existing skill run \`imp get X\` first and confirm a new version is intended.
21777
+ For a NEW skill you MUST ask the user whether it should be \`public\` (visible
21778
+ to everyone on the instance) or \`private\` (only them) \u2014 never pick a
21779
+ visibility yourself; then \`imp publish X <folder> <public|private>\`.
21780
+
21781
+ ## Not configured yet?
21782
+
21783
+ If \`imp config\` reports it's not configured (or \`~/.config/imp/skills.json\`
21784
+ is missing), tell the user to run the installer \u2014 it sets everything up
21785
+ interactively (base URL, API key, target clients, copy/symlink):
21786
+
21787
+ \`\`\`
21788
+ curl -fsSL <base_url>/api/skills/install.sh | sh
21789
+ \`\`\`
21790
+
21791
+ \`<base_url>\` is their instance's frontend URL (e.g. https://ai.open.de). They
21792
+ can create an API key at \`<base_url>/token\`.
21793
+
21794
+ ## Errors
21795
+
21796
+ - install: \`403\` = no access to that skill; \`404\` = unknown name.
21797
+ - publish: \`400\` = missing/invalid visibility (new skills need public|private);
21798
+ \`403\` = you can see it but lack write access; \`409\` = the name is taken by a
21799
+ skill you can't access.
21800
+ `;
21801
+
21116
21802
  // src/exulu/routes.ts
21117
21803
  var REQUEST_SIZE_LIMIT = "50mb";
21118
21804
  var getExuluVersionNumber = async () => {
@@ -21186,6 +21872,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
21186
21872
  }
21187
21873
  next();
21188
21874
  });
21875
+ const rawZip = import_express5.default.raw({ type: ["application/zip", "application/octet-stream", "application/x-zip-compressed", "application/x-zip"], limit: "50mb" });
21189
21876
  console.log(`
21190
21877
  \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
21191
21878
  \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
@@ -22140,7 +22827,7 @@ ${customInstructions}` : agent.instructions;
22140
22827
  const imageModelsByName = (() => {
22141
22828
  if (!isLiteLLMEnabled() || !config?.fileUploads) return /* @__PURE__ */ new Map();
22142
22829
  try {
22143
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path6.resolve)(process.cwd(), "./config.litellm.yaml");
22830
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path9.resolve)(process.cwd(), "./config.litellm.yaml");
22144
22831
  const models2 = parseImageGenerationModels(configPath);
22145
22832
  return new Map(models2.map((m) => [m.model_name, m]));
22146
22833
  } catch (err) {
@@ -22838,7 +23525,9 @@ ${style.markdown}` : params.prompt;
22838
23525
  const budget_duration = String(body?.budget_duration ?? "");
22839
23526
  if (!Number.isFinite(max_budget) || max_budget <= 0) return null;
22840
23527
  if (!BUDGET_ALLOWED_DURATIONS.has(budget_duration)) return null;
22841
- return { max_budget, budget_duration };
23528
+ const reset = parseResetAt(body?.budget_reset_at);
23529
+ if (!reset.valid) return null;
23530
+ return { max_budget, budget_duration, budget_reset_at: reset.value };
22842
23531
  };
22843
23532
  const parseBudgetSettingsBody = (body) => {
22844
23533
  if (!body || typeof body !== "object") return null;
@@ -22908,7 +23597,7 @@ ${style.markdown}` : params.prompt;
22908
23597
  }
22909
23598
  const body = parseBudgetBody(req.body);
22910
23599
  if (!body) {
22911
- res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
23600
+ res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
22912
23601
  return;
22913
23602
  }
22914
23603
  const entityIds = Array.isArray(req.body?.entityIds) ? req.body.entityIds : [];
@@ -22920,7 +23609,7 @@ ${style.markdown}` : params.prompt;
22920
23609
  for (const id of entityIds) {
22921
23610
  const tag = budgetTagFor(entityType, id);
22922
23611
  try {
22923
- await upsertBudget(tag, body.max_budget, body.budget_duration);
23612
+ await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
22924
23613
  results.push({ entityId: String(id), ok: true });
22925
23614
  } catch (err) {
22926
23615
  results.push({
@@ -22945,12 +23634,12 @@ ${style.markdown}` : params.prompt;
22945
23634
  }
22946
23635
  const body = parseBudgetBody(req.body);
22947
23636
  if (!body) {
22948
- res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
23637
+ res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
22949
23638
  return;
22950
23639
  }
22951
23640
  const tag = budgetTagFor(entityType, req.params.entityId ?? "");
22952
23641
  try {
22953
- await upsertBudget(tag, body.max_budget, body.budget_duration);
23642
+ await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
22954
23643
  const info = await tagInfo([tag]);
22955
23644
  res.status(200).json({ budget: info[tag] ?? null });
22956
23645
  } catch (err) {
@@ -23413,6 +24102,206 @@ ${style.markdown}` : params.prompt;
23413
24102
  }
23414
24103
  return root;
23415
24104
  }
24105
+ app.get("/skills/agent/bootstrap", async (_req, res) => {
24106
+ try {
24107
+ const zip = new import_jszip3.default();
24108
+ zip.file("imp-skills/SKILL.md", BOOTSTRAP_SKILL_MD);
24109
+ zip.file("imp-skills/references/clients.json", BOOTSTRAP_CLIENTS_JSON);
24110
+ zip.file("imp-skills/scripts/imp", BOOTSTRAP_IMP_SH, {
24111
+ unixPermissions: 493
24112
+ });
24113
+ const buffer = await zip.generateAsync({
24114
+ type: "nodebuffer",
24115
+ platform: "UNIX"
24116
+ });
24117
+ res.setHeader("Content-Type", "application/zip");
24118
+ res.setHeader("Content-Disposition", 'attachment; filename="imp-skills.zip"');
24119
+ res.send(buffer);
24120
+ } catch (err) {
24121
+ console.error("[SKILLS] Failed to build bootstrap zip", err);
24122
+ res.status(500).json({ detail: "Failed to build bootstrap skill." });
24123
+ }
24124
+ });
24125
+ app.get("/skills/registry", async (req, res) => {
24126
+ const authResult = await requestValidators.authenticate(req);
24127
+ if (!authResult.user?.id) {
24128
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
24129
+ return;
24130
+ }
24131
+ const { db: db2 } = await postgresClient();
24132
+ const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
24133
+ const all = await db2("skills").select("*");
24134
+ const readable = await filterReadableSkills(db2, all, authResult.user);
24135
+ const skills = readable.filter((s) => {
24136
+ if (!tag) return true;
24137
+ const tags = Array.isArray(s.tags) ? s.tags : [];
24138
+ return tags.includes(tag);
24139
+ }).map((s) => ({
24140
+ name: s.name,
24141
+ description: s.description ?? "",
24142
+ tags: Array.isArray(s.tags) ? s.tags : [],
24143
+ current_version: s.current_version ?? 1,
24144
+ updated_at: s.updatedAt ?? s.updated_at ?? null
24145
+ }));
24146
+ res.json({ skills });
24147
+ });
24148
+ app.get("/skills/registry/:name/download", async (req, res) => {
24149
+ const authResult = await requestValidators.authenticate(req);
24150
+ if (!authResult.user?.id) {
24151
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
24152
+ return;
24153
+ }
24154
+ const { db: db2 } = await postgresClient();
24155
+ const skill = await resolveSkillByName(db2, req.params.name);
24156
+ if (!skill) {
24157
+ res.status(404).json({ detail: "Skill not found." });
24158
+ return;
24159
+ }
24160
+ if (!await canAccessSkill(db2, skill, "read", authResult.user)) {
24161
+ res.status(403).json({ detail: "You don't have access to this skill." });
24162
+ return;
24163
+ }
24164
+ const vQuery = req.query.version;
24165
+ const version = !vQuery || vQuery === "latest" ? skill.current_version ?? 1 : Number(vQuery);
24166
+ if (!Number.isFinite(version) || version < 1) {
24167
+ res.status(400).json({ detail: "Invalid version." });
24168
+ return;
24169
+ }
24170
+ const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
24171
+ const versionPrefix = `skills/${skill.id}/v${version}/`;
24172
+ const files = await listS3ObjectsByPrefix(versionPrefix, config);
24173
+ if (files.length === 0) {
24174
+ res.status(404).json({ detail: `Version v${version} has no files.` });
24175
+ return;
24176
+ }
24177
+ const zip = new import_jszip3.default();
24178
+ for (const file of files) {
24179
+ const idx = file.key.indexOf(versionPrefix);
24180
+ const rel = idx >= 0 ? file.key.slice(idx + versionPrefix.length) : file.key;
24181
+ if (!rel) continue;
24182
+ const bytes = await getS3ObjectBytes(file.key, config);
24183
+ zip.file(`${safeName}/${rel}`, bytes);
24184
+ }
24185
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
24186
+ res.setHeader("Content-Type", "application/zip");
24187
+ res.setHeader("Content-Disposition", `attachment; filename="${safeName}.skill"`);
24188
+ res.send(buffer);
24189
+ });
24190
+ app.post("/skills/registry/:name", rawZip, async (req, res) => {
24191
+ const authResult = await requestValidators.authenticate(req);
24192
+ if (!authResult.user?.id) {
24193
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
24194
+ return;
24195
+ }
24196
+ const name = req.params.name;
24197
+ const bytes = req.body;
24198
+ if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
24199
+ res.status(400).json({ detail: "Empty body. Send the skill as a zip/.skill payload." });
24200
+ return;
24201
+ }
24202
+ const { db: db2 } = await postgresClient();
24203
+ const existing = await resolveSkillByName(db2, name);
24204
+ if (existing) {
24205
+ const canWrite = await canAccessSkill(db2, existing, "write", authResult.user);
24206
+ if (canWrite) {
24207
+ const nextVersion = (existing.current_version ?? 1) + 1;
24208
+ try {
24209
+ await extractBundleToVersion({ bytes, skillId: existing.id, version: nextVersion, config });
24210
+ } catch (err) {
24211
+ if (err instanceof BundleValidationError) {
24212
+ res.status(400).json({ detail: err.message });
24213
+ return;
24214
+ }
24215
+ console.error("[SKILLS] publish (new version) failed", err);
24216
+ res.status(500).json({ detail: "Failed to publish new version." });
24217
+ return;
24218
+ }
24219
+ const history = Array.isArray(existing.history) ? existing.history : [];
24220
+ await db2("skills").where({ id: existing.id }).update({
24221
+ current_version: nextVersion,
24222
+ history: JSON.stringify([
24223
+ ...history,
24224
+ { version: nextVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
24225
+ ])
24226
+ });
24227
+ res.json({ name, version: nextVersion, created: false });
24228
+ return;
24229
+ } else {
24230
+ const canRead = await canAccessSkill(db2, existing, "read", authResult.user);
24231
+ if (!canRead) {
24232
+ res.status(409).json({ detail: "That name is unavailable." });
24233
+ return;
24234
+ }
24235
+ res.status(403).json({ detail: "You don't have write access to this skill." });
24236
+ return;
24237
+ }
24238
+ }
24239
+ const visibility = req.query.visibility;
24240
+ if (visibility !== "public" && visibility !== "private") {
24241
+ res.status(400).json({
24242
+ detail: "Missing or invalid 'visibility'. New skills require ?visibility=public or ?visibility=private \u2014 ask the user which they want."
24243
+ });
24244
+ return;
24245
+ }
24246
+ const meta = await parseSkillFrontmatter(bytes);
24247
+ const skillId = (0, import_node_crypto9.randomUUID)();
24248
+ try {
24249
+ await extractBundleToVersion({ bytes, skillId, version: 1, config });
24250
+ } catch (err) {
24251
+ if (err instanceof BundleValidationError) {
24252
+ res.status(400).json({ detail: err.message });
24253
+ return;
24254
+ }
24255
+ console.error("[SKILLS] publish (create) failed", err);
24256
+ res.status(500).json({ detail: "Failed to publish skill." });
24257
+ return;
24258
+ }
24259
+ try {
24260
+ await db2("skills").insert({
24261
+ id: skillId,
24262
+ name,
24263
+ description: meta.description ?? "",
24264
+ s3folder: `skills/${skillId}`,
24265
+ tags: JSON.stringify([]),
24266
+ usage_count: 0,
24267
+ favorite_count: 0,
24268
+ current_version: 1,
24269
+ history: JSON.stringify([
24270
+ { version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
24271
+ ]),
24272
+ rights_mode: visibility,
24273
+ created_by: authResult.user.id
24274
+ });
24275
+ } catch (err) {
24276
+ res.status(409).json({ detail: "That name is unavailable." });
24277
+ return;
24278
+ }
24279
+ res.json({ name, version: 1, created: true });
24280
+ });
24281
+ app.get("/skills/registry/:name", async (req, res) => {
24282
+ const authResult = await requestValidators.authenticate(req);
24283
+ if (!authResult.user?.id) {
24284
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
24285
+ return;
24286
+ }
24287
+ const { db: db2 } = await postgresClient();
24288
+ const skill = await resolveSkillByName(db2, req.params.name);
24289
+ if (!skill) {
24290
+ res.status(404).json({ detail: "Skill not found." });
24291
+ return;
24292
+ }
24293
+ if (!await canAccessSkill(db2, skill, "read", authResult.user)) {
24294
+ res.status(403).json({ detail: "You don't have access to this skill." });
24295
+ return;
24296
+ }
24297
+ res.json({
24298
+ name: skill.name,
24299
+ description: skill.description ?? "",
24300
+ tags: Array.isArray(skill.tags) ? skill.tags : [],
24301
+ current_version: skill.current_version ?? 1,
24302
+ history: Array.isArray(skill.history) ? skill.history : []
24303
+ });
24304
+ });
23416
24305
  app.post("/skills/:skillId/init", async (req, res) => {
23417
24306
  const authResult = await requestValidators.authenticate(req);
23418
24307
  if (!authResult.user?.id) {
@@ -23460,8 +24349,8 @@ ${style.markdown}` : params.prompt;
23460
24349
  }
23461
24350
  const { skillId } = req.params;
23462
24351
  const { extension, contentType } = req.body ?? {};
23463
- if (extension !== ".zip" && extension !== ".md") {
23464
- res.status(400).json({ detail: 'extension must be ".zip" or ".md".' });
24352
+ if (extension !== ".zip" && extension !== ".md" && extension !== ".skill") {
24353
+ res.status(400).json({ detail: 'extension must be ".zip", ".md", or ".skill".' });
23465
24354
  return;
23466
24355
  }
23467
24356
  if (!contentType || typeof contentType !== "string") {
@@ -23600,19 +24489,22 @@ ${style.markdown}` : params.prompt;
23600
24489
  }
23601
24490
  const versionPrefix = `skills/${skillId}/v${version}/`;
23602
24491
  const files = await listS3ObjectsByPrefix(versionPrefix, config);
23603
- const zip = new import_jszip2.default();
24492
+ const asSkill = req.query.format === "skill";
24493
+ const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
24494
+ const zip = new import_jszip3.default();
23604
24495
  let fileCount = 0;
23605
24496
  for (const file of files) {
23606
24497
  const prefixIndex = file.key.indexOf(versionPrefix);
23607
24498
  const relativePath = prefixIndex >= 0 ? file.key.slice(prefixIndex + versionPrefix.length) : file.key;
23608
24499
  if (!relativePath) continue;
23609
24500
  const bytes = await getS3ObjectBytes(file.key, config);
23610
- zip.file(relativePath, bytes);
24501
+ const archivePath = asSkill ? `${safeName}/${relativePath}` : relativePath;
24502
+ zip.file(archivePath, bytes);
23611
24503
  fileCount += 1;
23612
24504
  }
23613
24505
  const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
23614
24506
  zip.file(
23615
- "version.txt",
24507
+ asSkill ? `${safeName}/version.txt` : "version.txt",
23616
24508
  [
23617
24509
  `Skill: ${skill.name ?? skillId}`,
23618
24510
  `Skill id: ${skillId}`,
@@ -23623,13 +24515,9 @@ ${style.markdown}` : params.prompt;
23623
24515
  ].join("\n")
23624
24516
  );
23625
24517
  const buffer = await zip.generateAsync({ type: "nodebuffer" });
23626
- const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
23627
- const filename = `${safeName}-v${version}.zip`;
24518
+ const filename = asSkill ? `${safeName}.skill` : `${safeName}-v${version}.zip`;
23628
24519
  res.setHeader("Content-Type", "application/zip");
23629
- res.setHeader(
23630
- "Content-Disposition",
23631
- `attachment; filename="${filename}"`
23632
- );
24520
+ res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
23633
24521
  res.send(buffer);
23634
24522
  });
23635
24523
  app.post("/skills/:skillId/sign", async (req, res) => {
@@ -23929,6 +24817,12 @@ ${style.markdown}` : params.prompt;
23929
24817
  const fullSessionPrefix = `${generalPrefix}${userSessionPrefix}`;
23930
24818
  return { userSessionPrefix, fullSessionPrefix };
23931
24819
  }
24820
+ const loadSessionFilesAuth = async (req, res, sessionId, rights) => {
24821
+ const authed = await loadAuthedSession(req, res, sessionId, rights);
24822
+ if (!authed) return null;
24823
+ const ownerId = authed.session.user ?? authed.user.id;
24824
+ return { ...authed, ownerId, ...buildSessionPrefixes(ownerId, sessionId) };
24825
+ };
23932
24826
  function sanitizeFilename(name) {
23933
24827
  const trimmed = name.trim();
23934
24828
  if (!trimmed) return "";
@@ -23937,11 +24831,6 @@ ${style.markdown}` : params.prompt;
23937
24831
  return trimmed.replace(/[\\/]/g, "_");
23938
24832
  }
23939
24833
  app.get("/sessions/:sessionId/files", async (req, res) => {
23940
- const authResult = await requestValidators.authenticate(req);
23941
- if (!authResult.user?.id) {
23942
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
23943
- return;
23944
- }
23945
24834
  const sessionId = req.params.sessionId;
23946
24835
  if (!sessionId) {
23947
24836
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -23951,10 +24840,9 @@ ${style.markdown}` : params.prompt;
23951
24840
  res.status(500).json({ detail: "File uploads are not configured." });
23952
24841
  return;
23953
24842
  }
23954
- const { userSessionPrefix } = buildSessionPrefixes(
23955
- authResult.user.id,
23956
- sessionId
23957
- );
24843
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
24844
+ if (!authed) return;
24845
+ const { userSessionPrefix } = authed;
23958
24846
  let objects;
23959
24847
  try {
23960
24848
  objects = await listS3ObjectsByPrefix(userSessionPrefix, config);
@@ -23984,11 +24872,6 @@ ${style.markdown}` : params.prompt;
23984
24872
  app.post(
23985
24873
  "/sessions/:sessionId/files/upload-sign",
23986
24874
  async (req, res) => {
23987
- const authResult = await requestValidators.authenticate(req);
23988
- if (!authResult.user?.id) {
23989
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
23990
- return;
23991
- }
23992
24875
  const sessionId = req.params.sessionId;
23993
24876
  if (!sessionId) {
23994
24877
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -23998,6 +24881,8 @@ ${style.markdown}` : params.prompt;
23998
24881
  res.status(500).json({ detail: "File uploads are not configured." });
23999
24882
  return;
24000
24883
  }
24884
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
24885
+ if (!authed) return;
24001
24886
  const { filename, contentType } = req.body ?? {};
24002
24887
  if (!filename || typeof filename !== "string") {
24003
24888
  res.status(400).json({ detail: "Missing filename in request body." });
@@ -24012,11 +24897,7 @@ ${style.markdown}` : params.prompt;
24012
24897
  res.status(400).json({ detail: "Missing contentType in request body." });
24013
24898
  return;
24014
24899
  }
24015
- const { userSessionPrefix, fullSessionPrefix } = buildSessionPrefixes(
24016
- authResult.user.id,
24017
- sessionId
24018
- );
24019
- const fullKey = `${fullSessionPrefix}${safeName}`;
24900
+ const fullKey = `${authed.fullSessionPrefix}${safeName}`;
24020
24901
  const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
24021
24902
  res.json({ uploadUrl, key: fullKey });
24022
24903
  }
@@ -24024,11 +24905,6 @@ ${style.markdown}` : params.prompt;
24024
24905
  app.post(
24025
24906
  "/sessions/:sessionId/files/sync-to-sandbox",
24026
24907
  async (req, res) => {
24027
- const authResult = await requestValidators.authenticate(req);
24028
- if (!authResult.user?.id) {
24029
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
24030
- return;
24031
- }
24032
24908
  const sessionId = req.params.sessionId;
24033
24909
  if (!sessionId) {
24034
24910
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -24039,18 +24915,16 @@ ${style.markdown}` : params.prompt;
24039
24915
  res.status(400).json({ detail: "Missing key in request body." });
24040
24916
  return;
24041
24917
  }
24042
- const { fullSessionPrefix } = buildSessionPrefixes(
24043
- authResult.user.id,
24044
- sessionId
24045
- );
24046
- if (!key.startsWith(fullSessionPrefix)) {
24918
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
24919
+ if (!authed) return;
24920
+ if (!key.startsWith(authed.fullSessionPrefix)) {
24047
24921
  res.status(403).json({ detail: "Key does not belong to this session." });
24048
24922
  return;
24049
24923
  }
24050
24924
  try {
24051
24925
  const result = await downloadKeyIntoSandbox({
24052
24926
  sessionId,
24053
- userId: authResult.user.id,
24927
+ userId: authed.ownerId,
24054
24928
  fullS3Key: key,
24055
24929
  config
24056
24930
  });
@@ -24064,11 +24938,6 @@ ${style.markdown}` : params.prompt;
24064
24938
  app.delete(
24065
24939
  "/sessions/:sessionId/files",
24066
24940
  async (req, res) => {
24067
- const authResult = await requestValidators.authenticate(req);
24068
- if (!authResult.user?.id) {
24069
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
24070
- return;
24071
- }
24072
24941
  const sessionId = req.params.sessionId;
24073
24942
  if (!sessionId) {
24074
24943
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -24079,11 +24948,9 @@ ${style.markdown}` : params.prompt;
24079
24948
  res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
24080
24949
  return;
24081
24950
  }
24082
- const { fullSessionPrefix } = buildSessionPrefixes(
24083
- authResult.user.id,
24084
- sessionId
24085
- );
24086
- if (!key.startsWith(fullSessionPrefix)) {
24951
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
24952
+ if (!authed) return;
24953
+ if (!key.startsWith(authed.fullSessionPrefix)) {
24087
24954
  res.status(403).json({ detail: "Key does not belong to this session." });
24088
24955
  return;
24089
24956
  }
@@ -24102,11 +24969,6 @@ ${style.markdown}` : params.prompt;
24102
24969
  if (!req.headers.authorization && typeof req.query.auth === "string") {
24103
24970
  req.headers.authorization = `Bearer ${req.query.auth}`;
24104
24971
  }
24105
- const authResult = await requestValidators.authenticate(req);
24106
- if (!authResult.user?.id) {
24107
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
24108
- return;
24109
- }
24110
24972
  const sessionId = req.params.sessionId;
24111
24973
  if (!sessionId) {
24112
24974
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -24117,11 +24979,9 @@ ${style.markdown}` : params.prompt;
24117
24979
  res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
24118
24980
  return;
24119
24981
  }
24120
- const { fullSessionPrefix } = buildSessionPrefixes(
24121
- authResult.user.id,
24122
- sessionId
24123
- );
24124
- if (!key.startsWith(fullSessionPrefix)) {
24982
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
24983
+ if (!authed) return;
24984
+ if (!key.startsWith(authed.fullSessionPrefix)) {
24125
24985
  res.status(403).json({ detail: "Key does not belong to this session." });
24126
24986
  return;
24127
24987
  }
@@ -24225,7 +25085,7 @@ ${style.markdown}` : params.prompt;
24225
25085
  ` - tracking.json tracking events linked to the user`,
24226
25086
  ``
24227
25087
  ].join("\n");
24228
- const zip = new import_jszip2.default();
25088
+ const zip = new import_jszip3.default();
24229
25089
  zip.file("README.txt", readme);
24230
25090
  zip.file("user_data.json", JSON.stringify(userExport, null, 2));
24231
25091
  zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
@@ -24516,7 +25376,7 @@ init_check_record_access();
24516
25376
  init_resolve_model();
24517
25377
  init_supervisor();
24518
25378
  init_client();
24519
- var import_zod14 = require("zod");
25379
+ var import_zod16 = require("zod");
24520
25380
  init_convert_exulu_tools_to_ai_sdk_tools();
24521
25381
  init_singleton();
24522
25382
  var SESSION_ID_HEADER = "mcp-session-id";
@@ -24590,7 +25450,7 @@ var ExuluMCP = class {
24590
25450
  title: tool4.name + " agent",
24591
25451
  description: tool4.description,
24592
25452
  inputSchema: {
24593
- inputs: tool4.inputSchema || import_zod14.z.object({})
25453
+ inputs: tool4.inputSchema || import_zod16.z.object({})
24594
25454
  }
24595
25455
  },
24596
25456
  async ({ inputs }, args) => {
@@ -24642,7 +25502,7 @@ var ExuluMCP = class {
24642
25502
  title: "Get List of Prompt Templates",
24643
25503
  description: "Retrieves a list of prompt templates available for this agent. Returns the name, description, and ID of each template.",
24644
25504
  inputSchema: {
24645
- inputs: import_zod14.z.object({})
25505
+ inputs: import_zod16.z.object({})
24646
25506
  }
24647
25507
  },
24648
25508
  async ({ inputs }, args) => {
@@ -24688,8 +25548,8 @@ var ExuluMCP = class {
24688
25548
  title: "Get Prompt Template Details",
24689
25549
  description: "Retrieves the full details of a specific prompt template by ID, including the actual template content with variables.",
24690
25550
  inputSchema: {
24691
- inputs: import_zod14.z.object({
24692
- id: import_zod14.z.string().describe("The ID of the prompt template to retrieve")
25551
+ inputs: import_zod16.z.object({
25552
+ id: import_zod16.z.string().describe("The ID of the prompt template to retrieve")
24693
25553
  })
24694
25554
  }
24695
25555
  },
@@ -25804,7 +26664,7 @@ var ExuluEval = class {
25804
26664
  // src/templates/evals/index.ts
25805
26665
  init_resolve_model();
25806
26666
  init_singleton();
25807
- var import_zod15 = require("zod");
26667
+ var import_zod17 = require("zod");
25808
26668
  var import_ai17 = require("ai");
25809
26669
  var llmAsJudgeEval = () => {
25810
26670
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
@@ -25857,8 +26717,8 @@ var llmAsJudgeEval = () => {
25857
26717
  prompt,
25858
26718
  maxRetries: 2,
25859
26719
  output: import_ai17.Output.object({
25860
- schema: import_zod15.z.object({
25861
- score: import_zod15.z.number().min(0).max(100).describe("The score between 0 and 100.")
26720
+ schema: import_zod17.z.object({
26721
+ score: import_zod17.z.number().min(0).max(100).describe("The score between 0 and 100.")
25862
26722
  })
25863
26723
  })
25864
26724
  });
@@ -26089,15 +26949,15 @@ Usage:
26089
26949
  - If no todos exist yet, an empty list will be returned`;
26090
26950
 
26091
26951
  // src/templates/tools/todo/todo.ts
26092
- var import_zod16 = __toESM(require("zod"), 1);
26952
+ var import_zod18 = __toESM(require("zod"), 1);
26093
26953
  init_tool();
26094
26954
  init_check_record_access();
26095
26955
  init_client();
26096
- var TodoSchema = import_zod16.default.object({
26097
- content: import_zod16.default.string().describe("Brief description of the task"),
26098
- status: import_zod16.default.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
26099
- priority: import_zod16.default.string().describe("Priority level of the task: high, medium, low"),
26100
- id: import_zod16.default.string().describe("Unique identifier for the todo item")
26956
+ var TodoSchema = import_zod18.default.object({
26957
+ content: import_zod18.default.string().describe("Brief description of the task"),
26958
+ status: import_zod18.default.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
26959
+ priority: import_zod18.default.string().describe("Priority level of the task: high, medium, low"),
26960
+ id: import_zod18.default.string().describe("Unique identifier for the todo item")
26101
26961
  });
26102
26962
  var TodoWriteTool = new ExuluTool({
26103
26963
  id: "todo_write",
@@ -26113,8 +26973,8 @@ var TodoWriteTool = new ExuluTool({
26113
26973
  default: todowrite_default
26114
26974
  }
26115
26975
  ],
26116
- inputSchema: import_zod16.default.object({
26117
- todos: import_zod16.default.array(TodoSchema).describe("The updated todo list")
26976
+ inputSchema: import_zod18.default.object({
26977
+ todos: import_zod18.default.array(TodoSchema).describe("The updated todo list")
26118
26978
  }),
26119
26979
  execute: async (inputs) => {
26120
26980
  const { sessionID, todos, user } = inputs;
@@ -26149,7 +27009,7 @@ var TodoReadTool = new ExuluTool({
26149
27009
  id: "todo_read",
26150
27010
  name: "Todo Read",
26151
27011
  description: "Use this tool to read your todo list",
26152
- inputSchema: import_zod16.default.object({}),
27012
+ inputSchema: import_zod18.default.object({}),
26153
27013
  type: "function",
26154
27014
  category: "todo",
26155
27015
  config: [
@@ -26194,7 +27054,7 @@ init_cjs_shims();
26194
27054
  var questionread_default = 'Use this tool to read questions you\'ve asked and check if they\'ve been answered by the user. This tool helps you track the status of questions and retrieve the user\'s selected answers.\n\n## When to Use This Tool\n\nUse this tool proactively in these situations:\n- After asking a question to check if the user has responded\n- To retrieve the user\'s answer before proceeding with implementation\n- To review all questions and answers in the current session\n- When you need to reference a previous answer\n\n## How It Works\n\n- This tool takes no parameters (leave the input blank or empty)\n- Returns an array of all questions in the session\n- Each question includes:\n - `id`: Unique identifier for the question\n - `question`: The question text\n - `answerOptions`: Array of answer options with their IDs and text\n - `status`: Either "pending" (not answered) or "answered"\n - `selectedAnswerId`: The ID of the chosen answer (only present if answered)\n\n## Usage Pattern\n\nTypically you\'ll:\n1. Use Question Ask to pose a question\n2. Wait for the user to respond\n3. Use Question Read to check the answer\n4. Find the selected answer by matching the `selectedAnswerId` with an option in `answerOptions`\n5. Proceed with implementation based on the user\'s choice\n\n## Example Response\n\n```json\n[\n {\n "id": "question123",\n "question": "Which authentication method would you like to implement?",\n "answerOptions": [\n { "id": "ans1", "text": "JWT tokens" },\n { "id": "ans2", "text": "OAuth 2.0" },\n { "id": "ans3", "text": "Session-based auth" },\n { "id": "ans4", "text": "None of the above..." }\n ],\n "status": "answered",\n "selectedAnswerId": "ans1"\n }\n]\n```\n\nIn this example, the user selected "JWT tokens" (id: ans1).\n\n## Important Notes\n\n- If no questions exist in the session, an empty array will be returned\n- Questions remain in the session even after being answered for reference\n- Use the `selectedAnswerId` to find which answer option the user chose by matching it against the `id` field in `answerOptions`\n';
26195
27055
 
26196
27056
  // src/templates/tools/question/question.ts
26197
- var import_zod18 = __toESM(require("zod"), 1);
27057
+ var import_zod20 = __toESM(require("zod"), 1);
26198
27058
  init_tool();
26199
27059
  init_client();
26200
27060
 
@@ -26286,21 +27146,21 @@ After asking a question, use the Question Read tool to check if the user has ans
26286
27146
  `;
26287
27147
 
26288
27148
  // src/templates/tools/question/question-ask.ts
26289
- var import_zod17 = __toESM(require("zod"), 1);
27149
+ var import_zod19 = __toESM(require("zod"), 1);
26290
27150
  init_tool();
26291
27151
  init_check_record_access();
26292
27152
  init_client();
26293
27153
  var import_node_crypto11 = require("crypto");
26294
- var AnswerOptionSchema = import_zod17.default.object({
26295
- id: import_zod17.default.string().describe("Unique identifier for the answer option"),
26296
- text: import_zod17.default.string().describe("The text of the answer option")
27154
+ var AnswerOptionSchema = import_zod19.default.object({
27155
+ id: import_zod19.default.string().describe("Unique identifier for the answer option"),
27156
+ text: import_zod19.default.string().describe("The text of the answer option")
26297
27157
  });
26298
- var _QuestionSchema = import_zod17.default.object({
26299
- id: import_zod17.default.string().describe("Unique identifier for the question"),
26300
- question: import_zod17.default.string().describe("The question to ask the user"),
26301
- answerOptions: import_zod17.default.array(AnswerOptionSchema).describe("Array of possible answer options"),
26302
- selectedAnswerId: import_zod17.default.string().optional().describe("The ID of the answer option selected by the user"),
26303
- status: import_zod17.default.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
27158
+ var _QuestionSchema = import_zod19.default.object({
27159
+ id: import_zod19.default.string().describe("Unique identifier for the question"),
27160
+ question: import_zod19.default.string().describe("The question to ask the user"),
27161
+ answerOptions: import_zod19.default.array(AnswerOptionSchema).describe("Array of possible answer options"),
27162
+ selectedAnswerId: import_zod19.default.string().optional().describe("The ID of the answer option selected by the user"),
27163
+ status: import_zod19.default.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
26304
27164
  });
26305
27165
  var QuestionAskTool = new ExuluTool({
26306
27166
  id: "question_ask",
@@ -26317,9 +27177,9 @@ var QuestionAskTool = new ExuluTool({
26317
27177
  default: questionask_default
26318
27178
  }
26319
27179
  ],
26320
- inputSchema: import_zod17.default.object({
26321
- question: import_zod17.default.string().describe("The question to ask the user"),
26322
- answerOptions: import_zod17.default.array(import_zod17.default.string()).describe("Array of possible answer options (strings)")
27180
+ inputSchema: import_zod19.default.object({
27181
+ question: import_zod19.default.string().describe("The question to ask the user"),
27182
+ answerOptions: import_zod19.default.array(import_zod19.default.string()).describe("Array of possible answer options (strings)")
26323
27183
  }),
26324
27184
  execute: async (inputs) => {
26325
27185
  const { sessionID, question, answerOptions, user } = inputs;
@@ -26392,7 +27252,7 @@ var QuestionReadTool = new ExuluTool({
26392
27252
  name: "Question Read",
26393
27253
  needsApproval: false,
26394
27254
  description: "Use this tool to read questions and their answers",
26395
- inputSchema: import_zod18.default.object({}),
27255
+ inputSchema: import_zod20.default.object({}),
26396
27256
  type: "function",
26397
27257
  category: "question",
26398
27258
  config: [
@@ -26424,15 +27284,15 @@ var questionTools = [QuestionAskTool, QuestionReadTool];
26424
27284
  // src/templates/tools/perplexity.ts
26425
27285
  init_cjs_shims();
26426
27286
  init_tool();
26427
- var import_zod19 = __toESM(require("zod"), 1);
27287
+ var import_zod21 = __toESM(require("zod"), 1);
26428
27288
  var import_perplexity_ai = __toESM(require("@perplexity-ai/perplexity_ai"), 1);
26429
27289
  var internetSearchTool = new ExuluTool({
26430
27290
  id: "internet_search",
26431
27291
  name: "Internet Search",
26432
27292
  description: "Search the internet for information.",
26433
- inputSchema: import_zod19.default.object({
26434
- query: import_zod19.default.string().describe("The query to the tool."),
26435
- search_recency_filter: import_zod19.default.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
27293
+ inputSchema: import_zod21.default.object({
27294
+ query: import_zod21.default.string().describe("The query to the tool."),
27295
+ search_recency_filter: import_zod21.default.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
26436
27296
  }),
26437
27297
  category: "internet_search",
26438
27298
  type: "web_search",
@@ -26527,7 +27387,7 @@ var perplexityTools = [internetSearchTool];
26527
27387
  init_cjs_shims();
26528
27388
  init_tool();
26529
27389
  var nodemailer = __toESM(require("nodemailer"), 1);
26530
- var import_zod20 = require("zod");
27390
+ var import_zod22 = require("zod");
26531
27391
  var transporter = null;
26532
27392
  function getTransporter(config) {
26533
27393
  if (!transporter) {
@@ -26551,11 +27411,11 @@ var emailTool = new ExuluTool({
26551
27411
  id: "email",
26552
27412
  name: "Email",
26553
27413
  description: "Send an email.",
26554
- inputSchema: import_zod20.z.object({
26555
- recipient: import_zod20.z.string().describe("The recipient of the email."),
26556
- subject: import_zod20.z.string().describe("The subject of the email."),
26557
- html: import_zod20.z.string().describe("The HTML body of the email."),
26558
- text: import_zod20.z.string().describe("The text body of the email.")
27414
+ inputSchema: import_zod22.z.object({
27415
+ recipient: import_zod22.z.string().describe("The recipient of the email."),
27416
+ subject: import_zod22.z.string().describe("The subject of the email."),
27417
+ html: import_zod22.z.string().describe("The HTML body of the email."),
27418
+ text: import_zod22.z.string().describe("The text body of the email.")
26559
27419
  }),
26560
27420
  type: "function",
26561
27421
  config: [{
@@ -26626,7 +27486,7 @@ init_tool();
26626
27486
  init_supervisor();
26627
27487
  init_client();
26628
27488
  init_check_record_access();
26629
- var import_zod21 = require("zod");
27489
+ var import_zod23 = require("zod");
26630
27490
  var _cachedImageModels;
26631
27491
  var setCachedImageModels = (models2) => {
26632
27492
  _cachedImageModels = models2;
@@ -26687,8 +27547,8 @@ var createImageGenerationWidgetTool = (models2) => {
26687
27547
  needsApproval: false,
26688
27548
  type: "function",
26689
27549
  config: [],
26690
- inputSchema: import_zod21.z.object({
26691
- prompt: import_zod21.z.string().describe(
27550
+ inputSchema: import_zod23.z.object({
27551
+ prompt: import_zod23.z.string().describe(
26692
27552
  "Initial image prompt. The user can edit it before generating."
26693
27553
  )
26694
27554
  }),
@@ -26727,7 +27587,7 @@ var createImageGenerationWidgetTool = (models2) => {
26727
27587
  };
26728
27588
 
26729
27589
  // src/exulu/app/index.ts
26730
- var import_node_path7 = require("path");
27590
+ var import_node_path10 = require("path");
26731
27591
 
26732
27592
  // src/validators/postgres-name.ts
26733
27593
  init_cjs_shims();
@@ -27113,7 +27973,7 @@ var ExuluApp = class {
27113
27973
  const imageGenerationTools = [];
27114
27974
  const s3Configured = !!config?.fileUploads && !!config.fileUploads.s3region && !!config.fileUploads.s3key && !!config.fileUploads.s3secret && !!config.fileUploads.s3Bucket;
27115
27975
  if (isLiteLLMEnabled() && s3Configured) {
27116
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path7.resolve)(process.cwd(), "./config.litellm.yaml");
27976
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path10.resolve)(process.cwd(), "./config.litellm.yaml");
27117
27977
  const imageModels = parseImageGenerationModels(configPath);
27118
27978
  if (imageModels.length > 0) {
27119
27979
  console.log(
@@ -28573,8 +29433,8 @@ init_cjs_shims();
28573
29433
  // src/exulu/litellm/db-init.ts
28574
29434
  init_cjs_shims();
28575
29435
  var import_node_fs9 = require("fs");
28576
- var import_node_path8 = require("path");
28577
- var import_node_child_process5 = require("child_process");
29436
+ var import_node_path11 = require("path");
29437
+ var import_node_child_process6 = require("child_process");
28578
29438
  var import_pg = require("pg");
28579
29439
 
28580
29440
  // src/exulu/litellm/db-setup-check.ts
@@ -28643,7 +29503,7 @@ ${WARNING_BANNER}`);
28643
29503
  };
28644
29504
  var log5 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
28645
29505
  var initLiteLLMDatabase = async (packageRoot) => {
28646
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path8.resolve)(process.cwd(), "./config.litellm.yaml");
29506
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path11.resolve)(process.cwd(), "./config.litellm.yaml");
28647
29507
  const safety = checkLiteLLMDatabaseSafety(configPath);
28648
29508
  if (safety.ok && safety.reason === "no-litellm-db-mode") return;
28649
29509
  if (!safety.ok && safety.reason === "unparseable-url") {
@@ -28781,9 +29641,9 @@ var initLiteLLMDatabase = async (packageRoot) => {
28781
29641
  ]);
28782
29642
  return;
28783
29643
  }
28784
- const venvBin = (0, import_node_path8.resolve)(packageRoot, "ee/python/.venv/bin");
28785
- const prismaCli = (0, import_node_path8.resolve)(venvBin, "prisma");
28786
- const venvLibDir = (0, import_node_path8.resolve)(packageRoot, "ee/python/.venv/lib");
29644
+ const venvBin = (0, import_node_path11.resolve)(packageRoot, "ee/python/.venv/bin");
29645
+ const prismaCli = (0, import_node_path11.resolve)(venvBin, "prisma");
29646
+ const venvLibDir = (0, import_node_path11.resolve)(packageRoot, "ee/python/.venv/lib");
28787
29647
  const pythonVersionDir = (0, import_node_fs9.existsSync)(venvLibDir) ? (0, import_node_fs9.readdirSync)(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
28788
29648
  if (!pythonVersionDir) {
28789
29649
  warn2([
@@ -28793,12 +29653,12 @@ var initLiteLLMDatabase = async (packageRoot) => {
28793
29653
  ]);
28794
29654
  return;
28795
29655
  }
28796
- const litellmProxyDir = (0, import_node_path8.resolve)(
29656
+ const litellmProxyDir = (0, import_node_path11.resolve)(
28797
29657
  venvLibDir,
28798
29658
  pythonVersionDir,
28799
29659
  "site-packages/litellm/proxy"
28800
29660
  );
28801
- const schemaPath = (0, import_node_path8.resolve)(litellmProxyDir, "schema.prisma");
29661
+ const schemaPath = (0, import_node_path11.resolve)(litellmProxyDir, "schema.prisma");
28802
29662
  if (!(0, import_node_fs9.existsSync)(prismaCli)) {
28803
29663
  warn2([
28804
29664
  `Prisma CLI not found at ${prismaCli}.`,
@@ -28815,7 +29675,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
28815
29675
  return;
28816
29676
  }
28817
29677
  log5("Running `prisma db push` against LiteLLM's schema\u2026");
28818
- const result = (0, import_node_child_process5.spawnSync)(prismaCli, ["db", "push", "--skip-generate"], {
29678
+ const result = (0, import_node_child_process6.spawnSync)(prismaCli, ["db", "push", "--skip-generate"], {
28819
29679
  cwd: litellmProxyDir,
28820
29680
  env: {
28821
29681
  ...process.env,
@@ -29387,14 +30247,14 @@ init_cjs_shims();
29387
30247
  var fs4 = __toESM(require("fs"), 1);
29388
30248
  var path = __toESM(require("path"), 1);
29389
30249
  var import_ai18 = require("ai");
29390
- var import_zod22 = require("zod");
30250
+ var import_zod24 = require("zod");
29391
30251
  var import_p_limit = __toESM(require("p-limit"), 1);
29392
30252
  var import_crypto2 = require("crypto");
29393
30253
  init_with_retry();
29394
30254
  var mammoth = __toESM(require("mammoth"), 1);
29395
30255
  var import_turndown = __toESM(require("turndown"), 1);
29396
30256
  var import_word_extractor = __toESM(require("word-extractor"), 1);
29397
- var import_officeparser2 = require("officeparser");
30257
+ var import_officeparser3 = require("officeparser");
29398
30258
  init_entitlements();
29399
30259
 
29400
30260
  // src/utils/python-executor.ts
@@ -29854,15 +30714,15 @@ If the page contains a flow-chart, schematic, technical drawing or control board
29854
30714
  const result = await (0, import_ai18.generateText)({
29855
30715
  model,
29856
30716
  output: import_ai18.Output.object({
29857
- schema: import_zod22.z.object({
29858
- needs_correction: import_zod22.z.boolean(),
29859
- corrected_text: import_zod22.z.string().nullable(),
29860
- current_page_table: import_zod22.z.object({
29861
- headers: import_zod22.z.array(import_zod22.z.string()),
29862
- is_continuation: import_zod22.z.boolean()
30717
+ schema: import_zod24.z.object({
30718
+ needs_correction: import_zod24.z.boolean(),
30719
+ corrected_text: import_zod24.z.string().nullable(),
30720
+ current_page_table: import_zod24.z.object({
30721
+ headers: import_zod24.z.array(import_zod24.z.string()),
30722
+ is_continuation: import_zod24.z.boolean()
29863
30723
  }).nullable(),
29864
- confidence: import_zod22.z.enum(["high", "medium", "low"]),
29865
- reasoning: import_zod22.z.string()
30724
+ confidence: import_zod24.z.enum(["high", "medium", "low"]),
30725
+ reasoning: import_zod24.z.string()
29866
30726
  })
29867
30727
  }),
29868
30728
  messages: [
@@ -30111,7 +30971,7 @@ ${setupResult.output || ""}`);
30111
30971
  const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
30112
30972
  json = JSON.parse(jsonContent);
30113
30973
  } else if (config?.processor.name === "officeparser") {
30114
- const text = await (0, import_officeparser2.parseOfficeAsync)(buffer, {
30974
+ const text = await (0, import_officeparser3.parseOfficeAsync)(buffer, {
30115
30975
  outputErrorToConsole: false,
30116
30976
  newlineDelimiter: "\n"
30117
30977
  });