@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.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  LITELLM_UI_PATH,
15
15
  LiteLLMAdminError,
16
16
  OAUTH_CALLBACK_PATH,
17
+ PreviewRenderError,
17
18
  ResolveModelError,
18
19
  STATISTICS_TYPE_ENUM,
19
20
  authentication,
@@ -37,6 +38,7 @@ import {
37
38
  exchangeCodeForTokens,
38
39
  exuluApp,
39
40
  getBudgetSettings,
41
+ getPdfPreviewBytes,
40
42
  getPresignedUrl,
41
43
  getS3ObjectBytes,
42
44
  getS3ObjectContent,
@@ -47,6 +49,7 @@ import {
47
49
  getToken,
48
50
  getUserBudgetView,
49
51
  guardExtractedFileText,
52
+ imageAttachmentGuard,
50
53
  invalidateBudgetCaches,
51
54
  isLiteLLMEnabled,
52
55
  listS3ObjectsByPrefix,
@@ -54,6 +57,7 @@ import {
54
57
  mapStreamErrorMessage,
55
58
  oauthRegistry,
56
59
  oauthTokenStore,
60
+ parseResetAt,
57
61
  postgresClient,
58
62
  provisionDefaultUserBudget,
59
63
  reportSystemDependencies,
@@ -73,7 +77,7 @@ import {
73
77
  upsertBudget,
74
78
  waitForLiteLLMReady,
75
79
  withRetry
76
- } from "./chunk-RVZWZNWG.js";
80
+ } from "./chunk-2242AI5L.js";
77
81
  import {
78
82
  findLiteLLMModel
79
83
  } from "./chunk-7CCMW3IW.js";
@@ -11134,21 +11138,7 @@ function isOsJunkPath(path2) {
11134
11138
  const basename = path2.split("/").pop() ?? "";
11135
11139
  return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
11136
11140
  }
11137
- async function extractBundleToS3(opts) {
11138
- const { bytes, skillId, isZip, config } = opts;
11139
- if (!isZip) {
11140
- await uploadFile(
11141
- bytes,
11142
- `skills/${skillId}/v1/SKILL.md`,
11143
- config,
11144
- { contentType: "text/markdown" },
11145
- void 0,
11146
- void 0,
11147
- true
11148
- // global=true so the key isn't user-prefixed (skill files are shared)
11149
- );
11150
- return { filesCount: 1 };
11151
- }
11141
+ async function extractZipToPrefix(bytes, prefix, config) {
11152
11142
  let zip;
11153
11143
  try {
11154
11144
  zip = await JSZip.loadAsync(bytes);
@@ -11212,7 +11202,7 @@ async function extractBundleToS3(opts) {
11212
11202
  }
11213
11203
  let filesCount = 0;
11214
11204
  for (const { relPath, content } of prepared) {
11215
- const s3Key = `skills/${skillId}/v1/${relPath}`;
11205
+ const s3Key = `${prefix}${relPath}`;
11216
11206
  await uploadFile(
11217
11207
  content,
11218
11208
  s3Key,
@@ -11221,81 +11211,75 @@ async function extractBundleToS3(opts) {
11221
11211
  void 0,
11222
11212
  void 0,
11223
11213
  true
11224
- // global=true — see SKILL.md case above
11214
+ // global=true — skill files are shared across users
11225
11215
  );
11226
11216
  filesCount += 1;
11227
11217
  }
11228
11218
  return { filesCount };
11229
11219
  }
11230
-
11231
- // src/sessions/pdf-preview-cache.ts
11232
- import { exec } from "child_process";
11233
- import { existsSync as existsSync3 } from "fs";
11234
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
11235
- import { extname, join } from "path";
11236
- import { promisify } from "util";
11237
- var execAsync = promisify(exec);
11238
- var CACHE_ROOT = "/tmp/exulu-pdf-cache";
11239
- var CACHE_IN = join(CACHE_ROOT, "_in");
11240
- var CACHE_OUT = join(CACHE_ROOT, "_out");
11241
- var inFlight = /* @__PURE__ */ new Map();
11242
- var PreviewRenderError = class extends Error {
11243
- constructor(message) {
11244
- super(message);
11245
- this.name = "PreviewRenderError";
11220
+ async function extractBundleToS3(opts) {
11221
+ const { bytes, skillId, isZip, config } = opts;
11222
+ if (!isZip) {
11223
+ await uploadFile(
11224
+ bytes,
11225
+ `skills/${skillId}/v1/SKILL.md`,
11226
+ config,
11227
+ { contentType: "text/markdown" },
11228
+ void 0,
11229
+ void 0,
11230
+ true
11231
+ // global=true so the key isn't user-prefixed (skill files are shared)
11232
+ );
11233
+ return { filesCount: 1 };
11246
11234
  }
11247
- };
11248
- function sanitizeEtag(raw) {
11249
- return raw.replace(/^"|"$/g, "").replace(/[^a-zA-Z0-9_-]/g, "_");
11235
+ return extractZipToPrefix(bytes, `skills/${skillId}/v1/`, config);
11250
11236
  }
11251
- async function getPdfPreviewBytes(opts) {
11252
- const { sourceKey, etag, config } = opts;
11253
- const safeEtag = sanitizeEtag(etag);
11254
- if (!safeEtag) {
11255
- throw new PreviewRenderError(`Invalid ETag for ${sourceKey}`);
11256
- }
11257
- const cachedPath = join(CACHE_ROOT, `${safeEtag}.pdf`);
11258
- if (existsSync3(cachedPath)) {
11259
- return readFile(cachedPath);
11260
- }
11261
- const existing = inFlight.get(safeEtag);
11262
- if (existing) return existing;
11263
- const promise = (async () => {
11264
- try {
11265
- await mkdir(CACHE_IN, { recursive: true });
11266
- await mkdir(CACHE_OUT, { recursive: true });
11267
- const ext = (extname(sourceKey) || ".docx").toLowerCase();
11268
- const inputPath = join(CACHE_IN, `${safeEtag}${ext}`);
11269
- const outputPath = join(CACHE_OUT, `${safeEtag}.pdf`);
11270
- try {
11271
- const bytes = await getS3ObjectBytes(sourceKey, config);
11272
- await writeFile(inputPath, bytes);
11273
- try {
11274
- await execAsync(
11275
- `soffice --headless --convert-to pdf "${inputPath}" --outdir "${CACHE_OUT}"`,
11276
- { timeout: 6e4, maxBuffer: 16 * 1024 * 1024 }
11277
- );
11278
- } catch (err) {
11279
- throw new PreviewRenderError(
11280
- `LibreOffice conversion failed for ${sourceKey} (etag ${etag}): ${err?.stderr ?? err?.message ?? "unknown error"}`
11281
- );
11282
- }
11283
- if (!existsSync3(outputPath)) {
11284
- throw new PreviewRenderError(
11285
- `LibreOffice produced no output for ${sourceKey} (etag ${etag})`
11286
- );
11287
- }
11288
- await rename(outputPath, cachedPath);
11289
- return await readFile(cachedPath);
11290
- } finally {
11291
- await rm(inputPath, { force: true });
11292
- }
11293
- } finally {
11294
- inFlight.delete(safeEtag);
11237
+ async function extractBundleToVersion(opts) {
11238
+ const { bytes, skillId, version, config } = opts;
11239
+ return extractZipToPrefix(bytes, `skills/${skillId}/v${version}/`, config);
11240
+ }
11241
+
11242
+ // src/skills/frontmatter.ts
11243
+ import JSZip2 from "jszip";
11244
+ function parseFrontmatter(md) {
11245
+ const match = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
11246
+ if (!match) return {};
11247
+ const block = match[1];
11248
+ const out = {};
11249
+ for (const line of block.split(/\r?\n/)) {
11250
+ const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
11251
+ if (!m) continue;
11252
+ const key = m[1];
11253
+ let v = m[2].trim();
11254
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
11255
+ v = v.slice(1, -1);
11295
11256
  }
11296
- })();
11297
- inFlight.set(safeEtag, promise);
11298
- return promise;
11257
+ out[key] = v;
11258
+ }
11259
+ return out;
11260
+ }
11261
+ async function parseSkillFrontmatter(zipBytes) {
11262
+ let zip;
11263
+ try {
11264
+ zip = await JSZip2.loadAsync(zipBytes);
11265
+ } catch {
11266
+ return {};
11267
+ }
11268
+ const paths = [];
11269
+ zip.forEach((p, entry) => {
11270
+ if (!entry.dir) paths.push(p);
11271
+ });
11272
+ const heads = new Set(paths.map((p) => p.split("/")[0]).filter(Boolean));
11273
+ let strip = (p) => p;
11274
+ if (heads.size === 1) {
11275
+ const head = [...heads][0] + "/";
11276
+ if (paths.every((p) => p.startsWith(head))) strip = (p) => p.slice(head.length);
11277
+ }
11278
+ const skillPath = paths.find((p) => strip(p) === "SKILL.md");
11279
+ if (!skillPath) return {};
11280
+ const md = await zip.file(skillPath).async("string");
11281
+ const fm = parseFrontmatter(md);
11282
+ return { name: fm.name, description: fm.description };
11299
11283
  }
11300
11284
 
11301
11285
  // src/exulu/routes.ts
@@ -11306,7 +11290,7 @@ import OpenAI from "openai";
11306
11290
  import fs2 from "fs";
11307
11291
  import { randomUUID as randomUUID3 } from "crypto";
11308
11292
  import "@opentelemetry/api";
11309
- import JSZip2 from "jszip";
11293
+ import JSZip3 from "jszip";
11310
11294
  import { createIdGenerator } from "ai";
11311
11295
  import cookieParser from "cookie-parser";
11312
11296
 
@@ -11913,7 +11897,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
11913
11897
  // Stop after the image_generation tool fires — the widget IS the
11914
11898
  // assistant's response, no follow-up text turn is wanted (same
11915
11899
  // reasoning as question_ask: the UI artifact is the message).
11916
- prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
11900
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
11917
11901
  stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
11918
11902
  });
11919
11903
  console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
@@ -11976,7 +11960,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
11976
11960
  }),
11977
11961
  maxRetries: 2,
11978
11962
  tools,
11979
- prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
11963
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
11980
11964
  stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
11981
11965
  });
11982
11966
  if (statistics) {
@@ -12391,7 +12375,7 @@ When a tool execution is not approved by the user, do not retry it unless explic
12391
12375
  );
12392
12376
  },
12393
12377
  // todo allow configuring the step budget per skill
12394
- prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget)),
12378
+ prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
12395
12379
  stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
12396
12380
  });
12397
12381
  return {
@@ -12875,7 +12859,7 @@ async function editImage(args) {
12875
12859
  }
12876
12860
 
12877
12861
  // src/exulu/litellm/parse-image-models.ts
12878
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
12862
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
12879
12863
  var stripComment3 = (line) => {
12880
12864
  const idx = line.indexOf("#");
12881
12865
  return idx >= 0 ? line.slice(0, idx) : line;
@@ -12898,7 +12882,7 @@ var parseInt103 = (raw) => {
12898
12882
  return Number.isInteger(n) ? n : void 0;
12899
12883
  };
12900
12884
  var parseImageGenerationModels = (configPath) => {
12901
- if (!existsSync4(configPath)) return [];
12885
+ if (!existsSync3(configPath)) return [];
12902
12886
  const text = readFileSync3(configPath, "utf8");
12903
12887
  const lines = text.split("\n");
12904
12888
  const entries = [];
@@ -13522,7 +13506,7 @@ ${project.description}` : ""}` : "",
13522
13506
  messages: coreMessages,
13523
13507
  tools: hasTools ? activeTools : void 0,
13524
13508
  maxRetries: 2,
13525
- prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
13509
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
13526
13510
  stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)],
13527
13511
  onError: (error) => {
13528
13512
  console.error("[OPENAI GATEWAY] stream error:", error);
@@ -13562,7 +13546,7 @@ ${project.description}` : ""}` : "",
13562
13546
  messages: coreMessages,
13563
13547
  tools: hasTools ? activeTools : void 0,
13564
13548
  maxRetries: 2,
13565
- prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget)),
13549
+ prepareStep: clientTools.length > 0 ? void 0 : composePrepareSteps(contextGuard(contextWindow), gatewayRetrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
13566
13550
  stopWhen: clientTools.length > 0 ? void 0 : [stepCountIs2(turnBudget)]
13567
13551
  });
13568
13552
  res.json(transformCompletion(text, usage.inputTokens ?? 0, usage.outputTokens ?? 0, ctx));
@@ -13763,6 +13747,151 @@ var contentHeadersFor = (key, contentType, filename) => {
13763
13747
  };
13764
13748
  var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
13765
13749
 
13750
+ // src/skills/skill-access.ts
13751
+ async function resolveSkillByName(db, name) {
13752
+ const row = await db("skills").where({ name }).first();
13753
+ return row ?? null;
13754
+ }
13755
+ async function canAccessSkill(db, skill, action, user) {
13756
+ const rbac = await RBACResolver(db, "skill", skill.id, skill.rights_mode || "private");
13757
+ return checkRecordAccess({ ...skill, RBAC: rbac }, action, user);
13758
+ }
13759
+ async function filterReadableSkills(db, skills, user) {
13760
+ const out = [];
13761
+ for (const s of skills) {
13762
+ if (await canAccessSkill(db, s, "read", user)) out.push(s);
13763
+ }
13764
+ return out;
13765
+ }
13766
+
13767
+ // src/skills/bootstrap/clients.ts
13768
+ var CLIENT_MANIFEST = [
13769
+ { id: "agents", dir: ".agents/skills" },
13770
+ // cross-agent standard (symlink canonical store)
13771
+ { id: "claude", dir: ".claude/skills" },
13772
+ { id: "windsurf", dir: ".windsurf/skills" },
13773
+ { id: "continue", dir: ".continue/skills" },
13774
+ { id: "roo", dir: ".roo/skills" },
13775
+ { id: "kilocode", dir: ".kilocode/skills" },
13776
+ { id: "crush", dir: ".crush/skills" },
13777
+ { id: "goose", dir: ".goose/skills" },
13778
+ { id: "qwen", dir: ".qwen/skills" },
13779
+ { id: "iflow", dir: ".iflow/skills" },
13780
+ { id: "junie", dir: ".junie/skills" },
13781
+ { id: "kiro", dir: ".kiro/skills" },
13782
+ { id: "trae", dir: ".trae/skills" },
13783
+ { id: "augment", dir: ".augment/skills" },
13784
+ { id: "factory", dir: ".factory/skills" },
13785
+ { id: "devin", dir: ".devin/skills" },
13786
+ { id: "openhands", dir: ".openhands/skills" },
13787
+ { id: "pi", dir: ".pi/skills" },
13788
+ { id: "cortex", dir: ".cortex/skills" },
13789
+ { id: "zencoder", dir: ".zencoder/skills" },
13790
+ { id: "codebuddy", dir: ".codebuddy/skills" },
13791
+ { id: "codestudio", dir: ".codestudio/skills" },
13792
+ { id: "commandcode", dir: ".commandcode/skills" },
13793
+ { id: "codemaker", dir: ".codemaker/skills" },
13794
+ { id: "codeartsdoer", dir: ".codeartsdoer/skills" },
13795
+ { id: "lingma", dir: ".lingma/skills" },
13796
+ { id: "qoder", dir: ".qoder/skills" },
13797
+ { id: "rovodev", dir: ".rovodev/skills" },
13798
+ { id: "moxby", dir: ".moxby/skills" },
13799
+ { id: "mux", dir: ".mux/skills" },
13800
+ { id: "neovate", dir: ".neovate/skills" },
13801
+ { id: "ona", dir: ".ona/skills" },
13802
+ { id: "pochi", dir: ".pochi/skills" },
13803
+ { id: "reasonix", dir: ".reasonix/skills" },
13804
+ { id: "terramind", dir: ".terramind/skills" },
13805
+ { id: "tinycloud", dir: ".tinycloud/skills" },
13806
+ { id: "vibe", dir: ".vibe/skills" },
13807
+ { id: "adal", dir: ".adal/skills" },
13808
+ { id: "aider-desk", dir: ".aider-desk/skills" },
13809
+ { id: "autohand", dir: ".autohand/skills" },
13810
+ { id: "bob", dir: ".bob/skills" },
13811
+ { id: "hermes", dir: ".hermes/skills" },
13812
+ { id: "inferencesh", dir: ".inferencesh/skills" },
13813
+ { id: "jazz", dir: ".jazz/skills" },
13814
+ { id: "kode", dir: ".kode/skills" },
13815
+ { id: "mcpjam", dir: ".mcpjam/skills" },
13816
+ { id: "forge", dir: ".forge/skills" },
13817
+ { id: "tabnine", dir: ".tabnine/agent/skills" }
13818
+ // exception: nested under agent/
13819
+ ];
13820
+
13821
+ // src/skills/bootstrap/imp-sh.generated.ts
13822
+ var IMP_SH_B64 = "IyEvYmluL3NoCiMgaW1wIOKAlCBoZWxwZXIgZm9yIHRoZSBjZW50cmFsIHNraWxsIGxpYnJhcnkuIFRoZSBhZ2VudCBpbnZva2VzIHRoaXMKIyAobmV2ZXIgcmF3IGN1cmwpOiB0aGUgdG9rZW4gaXMgcmVhZCBmcm9tIHRoZSBjb25maWcgZmlsZSBoZXJlIGFuZCBzZW50IHZpYQojIGB4LWFwaS1rZXk6IEJlYXJlcmAsIHNvIGl0IG5ldmVyIGVudGVycyB0aGUgbW9kZWwgY29udGV4dC4gQ2xpZW50IGZhbi1vdXQKIyAoY29weS9zeW1saW5rIGFjcm9zcyBhZ2VudCBjbGllbnRzKSBpcyBkZXRlcm1pbmlzdGljLgpzZXQgLWV1CgpDT05GSUdfRElSPSIkSE9NRS8uY29uZmlnL2ltcCIKQ09ORklHX0ZJTEU9IiRDT05GSUdfRElSL3NraWxscy5qc29uIgoKZGllKCkgeyBwcmludGYgJ2ltcDogJXNcbicgIiQqIiA+JjI7IGV4aXQgMTsgfQppbmZvKCkgeyBwcmludGYgJyVzXG4nICIkKiIgPiYyOyB9Cgpqc29uX3N0cigpIHsgIyBqc29uX3N0ciA8a2V5PiA8ZmlsZT4g4oCUIGZsYXQgImtleSI6InZhbHVlIgogIHNlZCAtbiAicy8uKlwiJDFcIltbOnNwYWNlOl1dKjpbWzpzcGFjZTpdXSpcIlxcKFteXCJdKlxcKVwiLiovXFwxL3AiICIkMiIgfCBoZWFkIC1uMQp9CgpbIC1mICIkQ09ORklHX0ZJTEUiIF0gfHwgZGllICJub3QgY29uZmlndXJlZCDigJQgcnVuOiBjdXJsIC1mc1NMIDxiYXNlX3VybD4vYXBpL3NraWxscy9pbnN0YWxsLnNoIHwgc2giCgpCQVNFX1VSTD0iJChqc29uX3N0ciBiYXNlX3VybCAiJENPTkZJR19GSUxFIikiCkJBQ0tFTkQ9IiQoanNvbl9zdHIgYmFja2VuZCAiJENPTkZJR19GSUxFIikiCkFQSV9LRVk9IiQoanNvbl9zdHIgYXBpX2tleSAiJENPTkZJR19GSUxFIikiCkxJTktfTU9ERT0iJChqc29uX3N0ciBsaW5rX21vZGUgIiRDT05GSUdfRklMRSIpIgpTQ09QRT0iJChqc29uX3N0ciBzY29wZSAiJENPTkZJR19GSUxFIikiCkNMSUVOVFM9IiQoc2VkIC1uICdzLy4qImNsaWVudHMiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlxbXChbXl1dKlwpXF0uKi9cMS9wJyAiJENPTkZJR19GSUxFIiB8IHRyICcsJyAnICcgfCB0ciAtZCAnIicgfCB0ciAtcyAnICcpIgoKWyAtbiAiJENMSUVOVFMiIF0gfHwgQ0xJRU5UUz0iYWdlbnRzIgpbIC1uICIkTElOS19NT0RFIiBdIHx8IExJTktfTU9ERT0iY29weSIKWyAtbiAiJFNDT1BFIiBdIHx8IFNDT1BFPSJwcm9qZWN0IgoKaWYgWyAteiAiJEJBQ0tFTkQiIF07IHRoZW4KICBbIC1uICIkQkFTRV9VUkwiIF0gfHwgZGllICJjb25maWcgaGFzIG5vIGJhY2tlbmQgYW5kIG5vIGJhc2VfdXJsIgogIEJBQ0tFTkQ9IiQoY3VybCAtZnNTTCAiJEJBU0VfVVJML2FwaS9jb25maWciIHwgc2VkIC1uICdzLy4qImJhY2tlbmQiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKiJcKFteIl0qXCkiLiovXDEvcCcgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJEJBQ0tFTkQiIF0gfHwgZGllICJjb3VsZCBub3QgcmVzb2x2ZSBiYWNrZW5kIGZyb20gJEJBU0VfVVJML2FwaS9jb25maWciCmZpCkJBQ0tFTkQ9IiQocHJpbnRmICclcycgIiRCQUNLRU5EIiB8IHNlZCAnczovKiQ6OicpIgoKUk9PVD0iJFBXRCIKWyAiJFNDT1BFIiA9ICJob21lIiBdICYmIFJPT1Q9IiRIT01FIgoKIyBkaXJfZm9yIENMSUVOVF9JRCAtPiByZWxhdGl2ZSBza2lsbCBkaXIgKGdlbmVyYXRlZCBmcm9tIHRoZSBtYW5pZmVzdCBpbiB0aGUKIyByZWFsIGJ1aWxkOyBhIHJlcHJlc2VudGF0aXZlIHN1YnNldCBoZXJlIGZvciBsb2NhbCB0ZXN0aW5nKS4KZGlyX2ZvcigpIHsKICBjYXNlICIkMSIgaW4KX19ESVJfRk9SX0NBU0VTX18KICAgICopIHJldHVybiAxIDs7CiAgZXNhYwp9CgojIE9uZSAiLi4vIiBwZXIgcGF0aCBjb21wb25lbnQgb2YgJDEgKGEgY2xpZW50IGRpciByZWxhdGl2ZSB0byBST09UKSwgc28KIyBzeW1saW5rcyBhcmUgcmVsYXRpdmUgYW5kIHN1cnZpdmUgYmVpbmcgY29tbWl0dGVkIGFuZCBjaGVja2VkIG91dCBlbHNld2hlcmUuCnJlbF90b19yb290KCkgewogIF91cD0iIjsgX29sZGlmcz0kSUZTOyBJRlM9LwogIGZvciBfc2VnIGluICQxOyBkbyBbIC1uICIkX3NlZyIgXSAmJiBfdXA9Ii4uLyRfdXAiOyBkb25lCiAgSUZTPSRfb2xkaWZzOyBwcmludGYgJyVzJyAiJF91cCIKfQoKYXBpKCkgeyAjIGFwaSA8TUVUSE9EPiA8cGF0aD4gW2V4dHJhIGN1cmwgYXJncy4uLl0gLT4gYm9keSBvbiBzdGRvdXQKICBtPSIkMSI7IHA9IiQyIjsgc2hpZnQgMgogIGN1cmwgLWZzUyAtWCAiJG0iICIkQkFDS0VORCRwIiAtSCAieC1hcGkta2V5OiBCZWFyZXIgJEFQSV9LRVkiICIkQCIKfQoKbWV0YV92ZXJzaW9uKCkgeyAjIG1ldGFfdmVyc2lvbiA8bmFtZT4gLT4gY3VycmVudF92ZXJzaW9uIGZyb20gcmVnaXN0cnkKICBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyQxIiAyPi9kZXYvbnVsbCBcCiAgICB8IHNlZCAtbiAncy8uKiJjdXJyZW50X3ZlcnNpb24iW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlwoWzAtOV1bMC05XSpcKS4qL1wxL3AnIHwgaGVhZCAtbjEKfQoKX21hbmFnZWQoKSB7ICMgX21hbmFnZWQgPGRpcj4gLT4gMCBpZiBzYWZlIHRvIG92ZXJ3cml0ZSAob3VycyBvciBzeW1saW5rIG9yIGFic2VudCkKICBkPSIkMSIKICBpZiBbIC1lICIkZCIgXSAmJiBbICEgLUwgIiRkIiBdICYmIFsgISAtZiAiJGQvLmltcC1za2lsbC5qc29uIiBdOyB0aGVuCiAgICBpbmZvICJza2lwICRkIChleGlzdHMsIG5vdCBtYW5hZ2VkIGJ5IGltcCkiOyByZXR1cm4gMQogIGZpCiAgcmV0dXJuIDAKfQoKX3B1dF9yZWFsKCkgeyAjIF9wdXRfcmVhbCA8ZGVzdD4gPHNyY2Rpcj4gPG1hcmtlci1qc29uPgogIF9tYW5hZ2VkICIkMSIgfHwgcmV0dXJuIDAKICBybSAtcmYgIiQxIjsgbWtkaXIgLXAgIiQxIjsgY3AgLVIgIiQyLy4iICIkMS8iCiAgcHJpbnRmICclc1xuJyAiJDMiID4gIiQxLy5pbXAtc2tpbGwuanNvbiIKfQoKcGxhY2Vfc2tpbGwoKSB7ICMgcGxhY2Vfc2tpbGwgPG5hbWU+IDxzcmNkaXI+IDx2ZXJzaW9uPgogIHBuYW1lPSIkMSI7IHBzcmM9IiQyIjsgcHZlcj0iJHszOi0xfSIKICBtYXJrZXI9J3sgIm5hbWUiOiAiJyIkcG5hbWUiJyIsICJ2ZXJzaW9uIjogJyIkcHZlciInLCAic291cmNlIjogIiciJEJBQ0tFTkQiJyIgfScKICBjYW5vbj0iJFJPT1QvLmFnZW50cy9za2lsbHMvJHBuYW1lIgogIFsgIiRMSU5LX01PREUiID0gInN5bWxpbmsiIF0gJiYgX3B1dF9yZWFsICIkY2Fub24iICIkcHNyYyIgIiRtYXJrZXIiCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgeyBpbmZvICJ1bmtub3duIGNsaWVudDogJGlkIjsgY29udGludWU7IH0KICAgIHBhcmVudD0iJFJPT1QvJGQiOyBkZXN0PSIkcGFyZW50LyRwbmFtZSIKICAgIGlmIFsgIiRMSU5LX01PREUiID0gInN5bWxpbmsiIF0gJiYgWyAiJGlkIiAhPSAiYWdlbnRzIiBdOyB0aGVuCiAgICAgIF9tYW5hZ2VkICIkZGVzdCIgfHwgY29udGludWUKICAgICAgbWtkaXIgLXAgIiRwYXJlbnQiOyBybSAtcmYgIiRkZXN0IgogICAgICByZWw9IiQocmVsX3RvX3Jvb3QgIiRkIikuYWdlbnRzL3NraWxscy8kcG5hbWUiCiAgICAgIGlmIGxuIC1zICIkcmVsIiAiJGRlc3QiIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAibGlua2VkICRkZXN0IC0+ICRyZWwiCiAgICAgIGVsc2UKICAgICAgICBpbmZvICJzeW1saW5rIHVuc3VwcG9ydGVkIGF0ICRkZXN0OyBjb3B5aW5nIgogICAgICAgIF9wdXRfcmVhbCAiJGRlc3QiICIkcHNyYyIgIiRtYXJrZXIiCiAgICAgIGZpCiAgICBlbGlmIFsgIiRMSU5LX01PREUiID0gImNvcHkiIF07IHRoZW4KICAgICAgX3B1dF9yZWFsICIkZGVzdCIgIiRwc3JjIiAiJG1hcmtlciIKICAgIGZpCiAgICAjIHN5bWxpbmsgKyBhZ2VudHM6IGFscmVhZHkgcGxhY2VkIGFzIHRoZSBjYW5vbmljYWwgc3RvcmUgYWJvdmUuCiAgZG9uZQp9CgppbnN0YWxsZWRfbmFtZXMoKSB7ICMgdW5pcXVlIHNraWxsIG5hbWVzIHRoYXQgY2Fycnkgb3VyIG1hcmtlciB1bmRlciBST09UCiAgeyBmaW5kICIkUk9PVC8uYWdlbnRzL3NraWxscyIgLW1heGRlcHRoIDIgLW5hbWUgLmltcC1za2lsbC5qc29uIDI+L2Rldi9udWxsCiAgICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICAgIGQ9IiQoZGlyX2ZvciAiJGlkIikiIHx8IGNvbnRpbnVlCiAgICAgIGZpbmQgIiRST09ULyRkIiAtbWF4ZGVwdGggMiAtbmFtZSAuaW1wLXNraWxsLmpzb24gMj4vZGV2L251bGwKICAgIGRvbmUKICB9IHwgd2hpbGUgcmVhZCAtciBtOyBkbyBqc29uX3N0ciBuYW1lICIkbSI7IGRvbmUgfCBzb3J0IC11Cn0KCm1hcmtlcl92ZXJzaW9uKCkgeyAjIG1hcmtlcl92ZXJzaW9uIDxuYW1lPgogIGZvciBiYXNlIGluICIkUk9PVC8uYWdlbnRzL3NraWxscyI7IGRvCiAgICBbIC1mICIkYmFzZS8kMS8uaW1wLXNraWxsLmpzb24iIF0gJiYgeyBqc29uX3N0ciB2ZXJzaW9uICIkYmFzZS8kMS8uaW1wLXNraWxsLmpzb24iOyByZXR1cm47IH0KICBkb25lCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgY29udGludWUKICAgIGY9IiRST09ULyRkLyQxLy5pbXAtc2tpbGwuanNvbiIKICAgIFsgLWYgIiRmIiBdICYmIHsganNvbl9zdHIgdmVyc2lvbiAiJGYiOyByZXR1cm47IH0KICBkb25lCn0KCmRvX2luc3RhbGwoKSB7ICMgZG9faW5zdGFsbCA8bmFtZT4KICBuYW1lPSIkMSIKICBUTVA9IiQobWt0ZW1wIC1kKSIKICBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyRuYW1lL2Rvd25sb2FkIiAtLW91dHB1dCAiJFRNUC9za2lsbC56aXAiIFwKICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJkb3dubG9hZCBmYWlsZWQgZm9yICckbmFtZScgKDQwMyA9IG5vIGFjY2VzcywgNDA0ID0gdW5rbm93bikiOyB9CiAgbWtkaXIgLXAgIiRUTVAveCIKICB1bnppcCAtcSAiJFRNUC9za2lsbC56aXAiIC1kICIkVE1QL3giIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJiYWQgYXJjaGl2ZSBmb3IgJyRuYW1lJyI7IH0KICBzcmM9IiQoZmluZCAiJFRNUC94IiAtbWluZGVwdGggMSAtbWF4ZGVwdGggMSAtdHlwZSBkIHwgaGVhZCAtbjEpIgogIFsgLW4gIiRzcmMiIF0gfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgInVuZXhwZWN0ZWQgYXJjaGl2ZSBsYXlvdXQgZm9yICckbmFtZSciOyB9CiAgdmVyPSIkKG1ldGFfdmVyc2lvbiAiJG5hbWUiKSIKICBwbGFjZV9za2lsbCAiJG5hbWUiICIkc3JjIiAiJHt2ZXI6LTF9IgogIHJtIC1yZiAiJFRNUCIKICBpbmZvICJpbnN0YWxsZWQgJG5hbWUgKHYke3ZlcjotMX0pIFskTElOS19NT0RFXSBpbnRvOiAkQ0xJRU5UUyIKfQoKdXNhZ2UoKSB7CiAgY2F0ID4mMiA8PEVPRgppbXAg4oCUIHNraWxsIGxpYnJhcnkgaGVscGVyCiAgaW1wIGxpc3QgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0IHNraWxscyB5b3UgY2FuIGFjY2VzcyAoSlNPTikKICBpbXAgZ2V0IDxuYW1lPiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNob3cgb25lIHNraWxsJ3MgbWV0YWRhdGEgKEpTT04pCiAgaW1wIGluc3RhbGwgPG5hbWU+ICAgICAgICAgICAgICAgICAgICAgICBpbnN0YWxsL3JlZnJlc2ggYSBza2lsbCBpbnRvIHlvdXIgYWdlbnQgY2xpZW50cwogIGltcCB1cGRhdGUgWzxuYW1lPl0gICAgICAgICAgICAgICAgICAgICAgdXBkYXRlIGluc3RhbGxlZCBza2lsbHMgKGFsbCwgb3Igb25lKSB0byBsYXRlc3QKICBpbXAgcHVibGlzaCA8bmFtZT4gPGRpcj4gPHB1YmxpY3xwcml2YXRlPiBwdWJsaXNoIGEgbG9jYWwgc2tpbGwgZm9sZGVyIGFzIDxuYW1lPgogIGltcCBjb25maWcgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2hvdyByZXNvbHZlZCBiYWNrZW5kIC8gc2NvcGUgLyBjbGllbnRzIChubyBzZWNyZXRzKQpFT0YKfQoKY21kPSIkezE6LWhlbHB9IgpbICQjIC1ndCAwIF0gJiYgc2hpZnQgfHwgdHJ1ZQoKY2FzZSAiJGNtZCIgaW4KICBsaXN0KSBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5IiA7OwogIGdldCkgWyAkIyAtZ2UgMSBdIHx8IGRpZSAidXNhZ2U6IGltcCBnZXQgPG5hbWU+IjsgYXBpIEdFVCAiL3NraWxscy9yZWdpc3RyeS8kMSIgOzsKICBpbnN0YWxsKSBbICQjIC1nZSAxIF0gfHwgZGllICJ1c2FnZTogaW1wIGluc3RhbGwgPG5hbWU+IjsgZG9faW5zdGFsbCAiJDEiIDs7CiAgdXBkYXRlKQogICAgaWYgWyAkIyAtZ2UgMSBdOyB0aGVuIG5hbWVzPSIkMSI7IGVsc2UgbmFtZXM9IiQoaW5zdGFsbGVkX25hbWVzKSI7IGZpCiAgICBbIC1uICIkbmFtZXMiIF0gfHwgeyBpbmZvICJubyBpbXAtbWFuYWdlZCBza2lsbHMgZm91bmQgdW5kZXIgJFJPT1QiOyBleGl0IDA7IH0KICAgIGZvciBuIGluICRuYW1lczsgZG8KICAgICAgY3VyPSIkKG1hcmtlcl92ZXJzaW9uICIkbiIpIgogICAgICBsYXRlc3Q9IiQobWV0YV92ZXJzaW9uICIkbiIpIgogICAgICBbIC1uICIkbGF0ZXN0IiBdIHx8IHsgaW5mbyAic2tpcCAkbiAobm90IGluIHJlZ2lzdHJ5KSI7IGNvbnRpbnVlOyB9CiAgICAgIGlmIFsgLXogIiRjdXIiIF0gfHwgWyAiJGxhdGVzdCIgLWd0ICIkY3VyIiBdIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAidXBkYXRpbmcgJG46IHYke2N1cjotP30gLT4gdiRsYXRlc3QiOyBkb19pbnN0YWxsICIkbiIKICAgICAgZWxzZQogICAgICAgIGluZm8gIiRuIHVwIHRvIGRhdGUgKHYkY3VyKSIKICAgICAgZmkKICAgIGRvbmUKICAgIDs7CiAgcHVibGlzaCkKICAgIFsgJCMgLWdlIDMgXSB8fCBkaWUgInVzYWdlOiBpbXAgcHVibGlzaCA8bmFtZT4gPGRpcj4gPHB1YmxpY3xwcml2YXRlPiIKICAgIG5hbWU9IiQxIjsgZm9sZGVyPSIkMiI7IHZpc2liaWxpdHk9IiQzIgogICAgY2FzZSAiJHZpc2liaWxpdHkiIGluCiAgICAgIHB1YmxpY3xwcml2YXRlKSA7OwogICAgICAqKSBkaWUgInZpc2liaWxpdHkgbXVzdCBiZSAncHVibGljJyBvciAncHJpdmF0ZScgKGFzayB0aGUgdXNlciB3aGljaCB0aGV5IHdhbnQpIiA7OwogICAgZXNhYwogICAgWyAtZCAiJGZvbGRlciIgXSB8fCBkaWUgIm5vIHN1Y2ggZm9sZGVyOiAkZm9sZGVyIgogICAgWyAtZiAiJGZvbGRlci9TS0lMTC5tZCIgXSB8fCBkaWUgIiRmb2xkZXIgaGFzIG5vIFNLSUxMLm1kIGF0IGl0cyByb290IgogICAgY29tbWFuZCAtdiB6aXAgPi9kZXYvbnVsbCAyPiYxIHx8IGRpZSAidGhlICd6aXAnIGNvbW1hbmQgaXMgcmVxdWlyZWQgdG8gcHVibGlzaCIKICAgIFRNUD0iJChta3RlbXAgLWQpIgogICAgKCBjZCAiJChkaXJuYW1lICIkZm9sZGVyIikiIFwKICAgICAgJiYgemlwIC1xIC1yIC1YICIkVE1QL3NraWxsLnppcCIgIiQoYmFzZW5hbWUgIiRmb2xkZXIiKSIgXAogICAgICAgICAgIC14ICcqLy5pbXAtc2tpbGwuanNvbicgJyovLmdpdC8qJyAnKi5EU19TdG9yZScgJyovX19NQUNPU1gvKicgKSBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJjb3VsZCBub3QgemlwICRmb2xkZXIiOyB9CiAgICBhcGkgUE9TVCAiL3NraWxscy9yZWdpc3RyeS8kbmFtZT92aXNpYmlsaXR5PSR2aXNpYmlsaXR5IiAtSCAiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi96aXAiIFwKICAgICAgLS1kYXRhLWJpbmFyeSBAIiRUTVAvc2tpbGwuemlwIiBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJwdWJsaXNoIGZhaWxlZCAoNDAzID0gbm8gd3JpdGUgYWNjZXNzLCA0MDkgPSBuYW1lIHRha2VuKSI7IH0KICAgIHJtIC1yZiAiJFRNUCIKICAgIGluZm8gInB1Ymxpc2hlZCAkbmFtZSAoJHZpc2liaWxpdHkpIgogICAgOzsKICBjb25maWcpCiAgICBwcmludGYgJ2JhY2tlbmQ9JXNcbnNjb3BlPSVzXG5saW5rX21vZGU9JXNcbmNsaWVudHM9JXNcbmFwaV9rZXk9JXNcbicgXAogICAgICAiJEJBQ0tFTkQiICIkU0NPUEUiICIkTElOS19NT0RFIiAiJENMSUVOVFMiIFwKICAgICAgIiQoWyAtbiAiJEFQSV9LRVkiIF0gJiYgZWNobyBzZXQgfHwgZWNobyBNSVNTSU5HKSIKICAgIDs7CiAgaGVscHwtLWhlbHB8LWgpIHVzYWdlIDs7CiAgKikgaW5mbyAidW5rbm93biBjb21tYW5kOiAkY21kIjsgdXNhZ2U7IGV4aXQgMiA7Owplc2FjCg==";
13823
+
13824
+ // src/skills/bootstrap/imp-skills.ts
13825
+ var BOOTSTRAP_CLIENTS_JSON = JSON.stringify(CLIENT_MANIFEST, null, 2);
13826
+ var DIR_FOR_CASES = CLIENT_MANIFEST.map(
13827
+ (c) => ` ${c.id}) printf '%s' '${c.dir}' ;;`
13828
+ ).join("\n");
13829
+ var BOOTSTRAP_IMP_SH = Buffer.from(IMP_SH_B64, "base64").toString("utf8").replace("__DIR_FOR_CASES__", DIR_FOR_CASES);
13830
+ var BOOTSTRAP_SKILL_MD = `---
13831
+ name: imp-skills
13832
+ 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.
13833
+ ---
13834
+
13835
+ # IMP Skills
13836
+
13837
+ Bridge to the IMP central skill library. **All operations go through the
13838
+ bundled helper script \u2014 do not hand-write curl or copy files yourself.** The
13839
+ script reads the API token from config (keeping it out of this conversation) and
13840
+ handles the multi-client copy/symlink fan-out deterministically.
13841
+
13842
+ ## The helper
13843
+
13844
+ Run the script next to this file, \`scripts/imp\`, with \`sh\` and the absolute
13845
+ path of this skill's directory:
13846
+
13847
+ \`\`\`
13848
+ sh "<this-skill-dir>/scripts/imp" <command>
13849
+ \`\`\`
13850
+
13851
+ Commands:
13852
+ - \`list\` \u2014 skills you can access (JSON on stdout)
13853
+ - \`get <name>\` \u2014 one skill's metadata (JSON)
13854
+ - \`install <name>\` \u2014 install/refresh a skill into the user's agent clients
13855
+ - \`update [<name>]\` \u2014 update every installed skill, or just \`<name>\`, to latest
13856
+ - \`publish <name> <folder> <public|private>\` \u2014 publish a local skill folder as \`<name>\`
13857
+ - \`config\` \u2014 show resolved backend / scope / clients (prints no secrets)
13858
+
13859
+ The token, backend URL, target clients, copy-vs-symlink mode, and scope all come
13860
+ from \`~/.config/imp/skills.json\` (written by the installer). Never print the
13861
+ \`api_key\` or read it into your reply \u2014 the script uses it internally.
13862
+
13863
+ ## Requests \u2192 commands
13864
+
13865
+ - "list / search skills" \u2192 \`imp list\`, then filter the JSON for the user.
13866
+ - "install skill X" / "add the X skill" \u2192 \`imp install X\`.
13867
+ - "update / get the latest version [of X]" \u2192 \`imp update [X]\`.
13868
+ - "publish / upload this skill as X" \u2192 confirm the target name with the user; for
13869
+ an existing skill run \`imp get X\` first and confirm a new version is intended.
13870
+ For a NEW skill you MUST ask the user whether it should be \`public\` (visible
13871
+ to everyone on the instance) or \`private\` (only them) \u2014 never pick a
13872
+ visibility yourself; then \`imp publish X <folder> <public|private>\`.
13873
+
13874
+ ## Not configured yet?
13875
+
13876
+ If \`imp config\` reports it's not configured (or \`~/.config/imp/skills.json\`
13877
+ is missing), tell the user to run the installer \u2014 it sets everything up
13878
+ interactively (base URL, API key, target clients, copy/symlink):
13879
+
13880
+ \`\`\`
13881
+ curl -fsSL <base_url>/api/skills/install.sh | sh
13882
+ \`\`\`
13883
+
13884
+ \`<base_url>\` is their instance's frontend URL (e.g. https://ai.open.de). They
13885
+ can create an API key at \`<base_url>/token\`.
13886
+
13887
+ ## Errors
13888
+
13889
+ - install: \`403\` = no access to that skill; \`404\` = unknown name.
13890
+ - publish: \`400\` = missing/invalid visibility (new skills need public|private);
13891
+ \`403\` = you can see it but lack write access; \`409\` = the name is taken by a
13892
+ skill you can't access.
13893
+ `;
13894
+
13766
13895
  // src/exulu/routes.ts
13767
13896
  var REQUEST_SIZE_LIMIT = "50mb";
13768
13897
  var getExuluVersionNumber = async () => {
@@ -13836,6 +13965,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
13836
13965
  }
13837
13966
  next();
13838
13967
  });
13968
+ const rawZip = express2.raw({ type: ["application/zip", "application/octet-stream", "application/x-zip-compressed", "application/x-zip"], limit: "50mb" });
13839
13969
  console.log(`
13840
13970
  \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
13841
13971
  \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
@@ -15488,7 +15618,9 @@ ${style.markdown}` : params.prompt;
15488
15618
  const budget_duration = String(body?.budget_duration ?? "");
15489
15619
  if (!Number.isFinite(max_budget) || max_budget <= 0) return null;
15490
15620
  if (!BUDGET_ALLOWED_DURATIONS.has(budget_duration)) return null;
15491
- return { max_budget, budget_duration };
15621
+ const reset = parseResetAt(body?.budget_reset_at);
15622
+ if (!reset.valid) return null;
15623
+ return { max_budget, budget_duration, budget_reset_at: reset.value };
15492
15624
  };
15493
15625
  const parseBudgetSettingsBody = (body) => {
15494
15626
  if (!body || typeof body !== "object") return null;
@@ -15558,7 +15690,7 @@ ${style.markdown}` : params.prompt;
15558
15690
  }
15559
15691
  const body = parseBudgetBody(req.body);
15560
15692
  if (!body) {
15561
- res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
15693
+ res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
15562
15694
  return;
15563
15695
  }
15564
15696
  const entityIds = Array.isArray(req.body?.entityIds) ? req.body.entityIds : [];
@@ -15570,7 +15702,7 @@ ${style.markdown}` : params.prompt;
15570
15702
  for (const id of entityIds) {
15571
15703
  const tag = budgetTagFor(entityType, id);
15572
15704
  try {
15573
- await upsertBudget(tag, body.max_budget, body.budget_duration);
15705
+ await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
15574
15706
  results.push({ entityId: String(id), ok: true });
15575
15707
  } catch (err) {
15576
15708
  results.push({
@@ -15595,12 +15727,12 @@ ${style.markdown}` : params.prompt;
15595
15727
  }
15596
15728
  const body = parseBudgetBody(req.body);
15597
15729
  if (!body) {
15598
- res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
15730
+ res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
15599
15731
  return;
15600
15732
  }
15601
15733
  const tag = budgetTagFor(entityType, req.params.entityId ?? "");
15602
15734
  try {
15603
- await upsertBudget(tag, body.max_budget, body.budget_duration);
15735
+ await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
15604
15736
  const info = await tagInfo([tag]);
15605
15737
  res.status(200).json({ budget: info[tag] ?? null });
15606
15738
  } catch (err) {
@@ -16063,6 +16195,206 @@ ${style.markdown}` : params.prompt;
16063
16195
  }
16064
16196
  return root;
16065
16197
  }
16198
+ app.get("/skills/agent/bootstrap", async (_req, res) => {
16199
+ try {
16200
+ const zip = new JSZip3();
16201
+ zip.file("imp-skills/SKILL.md", BOOTSTRAP_SKILL_MD);
16202
+ zip.file("imp-skills/references/clients.json", BOOTSTRAP_CLIENTS_JSON);
16203
+ zip.file("imp-skills/scripts/imp", BOOTSTRAP_IMP_SH, {
16204
+ unixPermissions: 493
16205
+ });
16206
+ const buffer = await zip.generateAsync({
16207
+ type: "nodebuffer",
16208
+ platform: "UNIX"
16209
+ });
16210
+ res.setHeader("Content-Type", "application/zip");
16211
+ res.setHeader("Content-Disposition", 'attachment; filename="imp-skills.zip"');
16212
+ res.send(buffer);
16213
+ } catch (err) {
16214
+ console.error("[SKILLS] Failed to build bootstrap zip", err);
16215
+ res.status(500).json({ detail: "Failed to build bootstrap skill." });
16216
+ }
16217
+ });
16218
+ app.get("/skills/registry", async (req, res) => {
16219
+ const authResult = await requestValidators.authenticate(req);
16220
+ if (!authResult.user?.id) {
16221
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16222
+ return;
16223
+ }
16224
+ const { db } = await postgresClient();
16225
+ const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
16226
+ const all = await db("skills").select("*");
16227
+ const readable = await filterReadableSkills(db, all, authResult.user);
16228
+ const skills = readable.filter((s) => {
16229
+ if (!tag) return true;
16230
+ const tags = Array.isArray(s.tags) ? s.tags : [];
16231
+ return tags.includes(tag);
16232
+ }).map((s) => ({
16233
+ name: s.name,
16234
+ description: s.description ?? "",
16235
+ tags: Array.isArray(s.tags) ? s.tags : [],
16236
+ current_version: s.current_version ?? 1,
16237
+ updated_at: s.updatedAt ?? s.updated_at ?? null
16238
+ }));
16239
+ res.json({ skills });
16240
+ });
16241
+ app.get("/skills/registry/:name/download", async (req, res) => {
16242
+ const authResult = await requestValidators.authenticate(req);
16243
+ if (!authResult.user?.id) {
16244
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16245
+ return;
16246
+ }
16247
+ const { db } = await postgresClient();
16248
+ const skill = await resolveSkillByName(db, req.params.name);
16249
+ if (!skill) {
16250
+ res.status(404).json({ detail: "Skill not found." });
16251
+ return;
16252
+ }
16253
+ if (!await canAccessSkill(db, skill, "read", authResult.user)) {
16254
+ res.status(403).json({ detail: "You don't have access to this skill." });
16255
+ return;
16256
+ }
16257
+ const vQuery = req.query.version;
16258
+ const version = !vQuery || vQuery === "latest" ? skill.current_version ?? 1 : Number(vQuery);
16259
+ if (!Number.isFinite(version) || version < 1) {
16260
+ res.status(400).json({ detail: "Invalid version." });
16261
+ return;
16262
+ }
16263
+ const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
16264
+ const versionPrefix = `skills/${skill.id}/v${version}/`;
16265
+ const files = await listS3ObjectsByPrefix(versionPrefix, config);
16266
+ if (files.length === 0) {
16267
+ res.status(404).json({ detail: `Version v${version} has no files.` });
16268
+ return;
16269
+ }
16270
+ const zip = new JSZip3();
16271
+ for (const file of files) {
16272
+ const idx = file.key.indexOf(versionPrefix);
16273
+ const rel = idx >= 0 ? file.key.slice(idx + versionPrefix.length) : file.key;
16274
+ if (!rel) continue;
16275
+ const bytes = await getS3ObjectBytes(file.key, config);
16276
+ zip.file(`${safeName}/${rel}`, bytes);
16277
+ }
16278
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
16279
+ res.setHeader("Content-Type", "application/zip");
16280
+ res.setHeader("Content-Disposition", `attachment; filename="${safeName}.skill"`);
16281
+ res.send(buffer);
16282
+ });
16283
+ app.post("/skills/registry/:name", rawZip, async (req, res) => {
16284
+ const authResult = await requestValidators.authenticate(req);
16285
+ if (!authResult.user?.id) {
16286
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16287
+ return;
16288
+ }
16289
+ const name = req.params.name;
16290
+ const bytes = req.body;
16291
+ if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
16292
+ res.status(400).json({ detail: "Empty body. Send the skill as a zip/.skill payload." });
16293
+ return;
16294
+ }
16295
+ const { db } = await postgresClient();
16296
+ const existing = await resolveSkillByName(db, name);
16297
+ if (existing) {
16298
+ const canWrite = await canAccessSkill(db, existing, "write", authResult.user);
16299
+ if (canWrite) {
16300
+ const nextVersion = (existing.current_version ?? 1) + 1;
16301
+ try {
16302
+ await extractBundleToVersion({ bytes, skillId: existing.id, version: nextVersion, config });
16303
+ } catch (err) {
16304
+ if (err instanceof BundleValidationError) {
16305
+ res.status(400).json({ detail: err.message });
16306
+ return;
16307
+ }
16308
+ console.error("[SKILLS] publish (new version) failed", err);
16309
+ res.status(500).json({ detail: "Failed to publish new version." });
16310
+ return;
16311
+ }
16312
+ const history = Array.isArray(existing.history) ? existing.history : [];
16313
+ await db("skills").where({ id: existing.id }).update({
16314
+ current_version: nextVersion,
16315
+ history: JSON.stringify([
16316
+ ...history,
16317
+ { version: nextVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
16318
+ ])
16319
+ });
16320
+ res.json({ name, version: nextVersion, created: false });
16321
+ return;
16322
+ } else {
16323
+ const canRead = await canAccessSkill(db, existing, "read", authResult.user);
16324
+ if (!canRead) {
16325
+ res.status(409).json({ detail: "That name is unavailable." });
16326
+ return;
16327
+ }
16328
+ res.status(403).json({ detail: "You don't have write access to this skill." });
16329
+ return;
16330
+ }
16331
+ }
16332
+ const visibility = req.query.visibility;
16333
+ if (visibility !== "public" && visibility !== "private") {
16334
+ res.status(400).json({
16335
+ detail: "Missing or invalid 'visibility'. New skills require ?visibility=public or ?visibility=private \u2014 ask the user which they want."
16336
+ });
16337
+ return;
16338
+ }
16339
+ const meta = await parseSkillFrontmatter(bytes);
16340
+ const skillId = randomUUID3();
16341
+ try {
16342
+ await extractBundleToVersion({ bytes, skillId, version: 1, config });
16343
+ } catch (err) {
16344
+ if (err instanceof BundleValidationError) {
16345
+ res.status(400).json({ detail: err.message });
16346
+ return;
16347
+ }
16348
+ console.error("[SKILLS] publish (create) failed", err);
16349
+ res.status(500).json({ detail: "Failed to publish skill." });
16350
+ return;
16351
+ }
16352
+ try {
16353
+ await db("skills").insert({
16354
+ id: skillId,
16355
+ name,
16356
+ description: meta.description ?? "",
16357
+ s3folder: `skills/${skillId}`,
16358
+ tags: JSON.stringify([]),
16359
+ usage_count: 0,
16360
+ favorite_count: 0,
16361
+ current_version: 1,
16362
+ history: JSON.stringify([
16363
+ { version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
16364
+ ]),
16365
+ rights_mode: visibility,
16366
+ created_by: authResult.user.id
16367
+ });
16368
+ } catch (err) {
16369
+ res.status(409).json({ detail: "That name is unavailable." });
16370
+ return;
16371
+ }
16372
+ res.json({ name, version: 1, created: true });
16373
+ });
16374
+ app.get("/skills/registry/:name", async (req, res) => {
16375
+ const authResult = await requestValidators.authenticate(req);
16376
+ if (!authResult.user?.id) {
16377
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
16378
+ return;
16379
+ }
16380
+ const { db } = await postgresClient();
16381
+ const skill = await resolveSkillByName(db, req.params.name);
16382
+ if (!skill) {
16383
+ res.status(404).json({ detail: "Skill not found." });
16384
+ return;
16385
+ }
16386
+ if (!await canAccessSkill(db, skill, "read", authResult.user)) {
16387
+ res.status(403).json({ detail: "You don't have access to this skill." });
16388
+ return;
16389
+ }
16390
+ res.json({
16391
+ name: skill.name,
16392
+ description: skill.description ?? "",
16393
+ tags: Array.isArray(skill.tags) ? skill.tags : [],
16394
+ current_version: skill.current_version ?? 1,
16395
+ history: Array.isArray(skill.history) ? skill.history : []
16396
+ });
16397
+ });
16066
16398
  app.post("/skills/:skillId/init", async (req, res) => {
16067
16399
  const authResult = await requestValidators.authenticate(req);
16068
16400
  if (!authResult.user?.id) {
@@ -16110,8 +16442,8 @@ ${style.markdown}` : params.prompt;
16110
16442
  }
16111
16443
  const { skillId } = req.params;
16112
16444
  const { extension, contentType } = req.body ?? {};
16113
- if (extension !== ".zip" && extension !== ".md") {
16114
- res.status(400).json({ detail: 'extension must be ".zip" or ".md".' });
16445
+ if (extension !== ".zip" && extension !== ".md" && extension !== ".skill") {
16446
+ res.status(400).json({ detail: 'extension must be ".zip", ".md", or ".skill".' });
16115
16447
  return;
16116
16448
  }
16117
16449
  if (!contentType || typeof contentType !== "string") {
@@ -16250,19 +16582,22 @@ ${style.markdown}` : params.prompt;
16250
16582
  }
16251
16583
  const versionPrefix = `skills/${skillId}/v${version}/`;
16252
16584
  const files = await listS3ObjectsByPrefix(versionPrefix, config);
16253
- const zip = new JSZip2();
16585
+ const asSkill = req.query.format === "skill";
16586
+ const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
16587
+ const zip = new JSZip3();
16254
16588
  let fileCount = 0;
16255
16589
  for (const file of files) {
16256
16590
  const prefixIndex = file.key.indexOf(versionPrefix);
16257
16591
  const relativePath = prefixIndex >= 0 ? file.key.slice(prefixIndex + versionPrefix.length) : file.key;
16258
16592
  if (!relativePath) continue;
16259
16593
  const bytes = await getS3ObjectBytes(file.key, config);
16260
- zip.file(relativePath, bytes);
16594
+ const archivePath = asSkill ? `${safeName}/${relativePath}` : relativePath;
16595
+ zip.file(archivePath, bytes);
16261
16596
  fileCount += 1;
16262
16597
  }
16263
16598
  const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
16264
16599
  zip.file(
16265
- "version.txt",
16600
+ asSkill ? `${safeName}/version.txt` : "version.txt",
16266
16601
  [
16267
16602
  `Skill: ${skill.name ?? skillId}`,
16268
16603
  `Skill id: ${skillId}`,
@@ -16273,13 +16608,9 @@ ${style.markdown}` : params.prompt;
16273
16608
  ].join("\n")
16274
16609
  );
16275
16610
  const buffer = await zip.generateAsync({ type: "nodebuffer" });
16276
- const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
16277
- const filename = `${safeName}-v${version}.zip`;
16611
+ const filename = asSkill ? `${safeName}.skill` : `${safeName}-v${version}.zip`;
16278
16612
  res.setHeader("Content-Type", "application/zip");
16279
- res.setHeader(
16280
- "Content-Disposition",
16281
- `attachment; filename="${filename}"`
16282
- );
16613
+ res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
16283
16614
  res.send(buffer);
16284
16615
  });
16285
16616
  app.post("/skills/:skillId/sign", async (req, res) => {
@@ -16579,6 +16910,12 @@ ${style.markdown}` : params.prompt;
16579
16910
  const fullSessionPrefix = `${generalPrefix}${userSessionPrefix}`;
16580
16911
  return { userSessionPrefix, fullSessionPrefix };
16581
16912
  }
16913
+ const loadSessionFilesAuth = async (req, res, sessionId, rights) => {
16914
+ const authed = await loadAuthedSession(req, res, sessionId, rights);
16915
+ if (!authed) return null;
16916
+ const ownerId = authed.session.user ?? authed.user.id;
16917
+ return { ...authed, ownerId, ...buildSessionPrefixes(ownerId, sessionId) };
16918
+ };
16582
16919
  function sanitizeFilename(name) {
16583
16920
  const trimmed = name.trim();
16584
16921
  if (!trimmed) return "";
@@ -16587,11 +16924,6 @@ ${style.markdown}` : params.prompt;
16587
16924
  return trimmed.replace(/[\\/]/g, "_");
16588
16925
  }
16589
16926
  app.get("/sessions/:sessionId/files", async (req, res) => {
16590
- const authResult = await requestValidators.authenticate(req);
16591
- if (!authResult.user?.id) {
16592
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16593
- return;
16594
- }
16595
16927
  const sessionId = req.params.sessionId;
16596
16928
  if (!sessionId) {
16597
16929
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16601,10 +16933,9 @@ ${style.markdown}` : params.prompt;
16601
16933
  res.status(500).json({ detail: "File uploads are not configured." });
16602
16934
  return;
16603
16935
  }
16604
- const { userSessionPrefix } = buildSessionPrefixes(
16605
- authResult.user.id,
16606
- sessionId
16607
- );
16936
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
16937
+ if (!authed) return;
16938
+ const { userSessionPrefix } = authed;
16608
16939
  let objects;
16609
16940
  try {
16610
16941
  objects = await listS3ObjectsByPrefix(userSessionPrefix, config);
@@ -16634,11 +16965,6 @@ ${style.markdown}` : params.prompt;
16634
16965
  app.post(
16635
16966
  "/sessions/:sessionId/files/upload-sign",
16636
16967
  async (req, res) => {
16637
- const authResult = await requestValidators.authenticate(req);
16638
- if (!authResult.user?.id) {
16639
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16640
- return;
16641
- }
16642
16968
  const sessionId = req.params.sessionId;
16643
16969
  if (!sessionId) {
16644
16970
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16648,6 +16974,8 @@ ${style.markdown}` : params.prompt;
16648
16974
  res.status(500).json({ detail: "File uploads are not configured." });
16649
16975
  return;
16650
16976
  }
16977
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
16978
+ if (!authed) return;
16651
16979
  const { filename, contentType } = req.body ?? {};
16652
16980
  if (!filename || typeof filename !== "string") {
16653
16981
  res.status(400).json({ detail: "Missing filename in request body." });
@@ -16662,11 +16990,7 @@ ${style.markdown}` : params.prompt;
16662
16990
  res.status(400).json({ detail: "Missing contentType in request body." });
16663
16991
  return;
16664
16992
  }
16665
- const { userSessionPrefix, fullSessionPrefix } = buildSessionPrefixes(
16666
- authResult.user.id,
16667
- sessionId
16668
- );
16669
- const fullKey = `${fullSessionPrefix}${safeName}`;
16993
+ const fullKey = `${authed.fullSessionPrefix}${safeName}`;
16670
16994
  const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
16671
16995
  res.json({ uploadUrl, key: fullKey });
16672
16996
  }
@@ -16674,11 +16998,6 @@ ${style.markdown}` : params.prompt;
16674
16998
  app.post(
16675
16999
  "/sessions/:sessionId/files/sync-to-sandbox",
16676
17000
  async (req, res) => {
16677
- const authResult = await requestValidators.authenticate(req);
16678
- if (!authResult.user?.id) {
16679
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16680
- return;
16681
- }
16682
17001
  const sessionId = req.params.sessionId;
16683
17002
  if (!sessionId) {
16684
17003
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16689,18 +17008,16 @@ ${style.markdown}` : params.prompt;
16689
17008
  res.status(400).json({ detail: "Missing key in request body." });
16690
17009
  return;
16691
17010
  }
16692
- const { fullSessionPrefix } = buildSessionPrefixes(
16693
- authResult.user.id,
16694
- sessionId
16695
- );
16696
- if (!key.startsWith(fullSessionPrefix)) {
17011
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
17012
+ if (!authed) return;
17013
+ if (!key.startsWith(authed.fullSessionPrefix)) {
16697
17014
  res.status(403).json({ detail: "Key does not belong to this session." });
16698
17015
  return;
16699
17016
  }
16700
17017
  try {
16701
17018
  const result = await downloadKeyIntoSandbox({
16702
17019
  sessionId,
16703
- userId: authResult.user.id,
17020
+ userId: authed.ownerId,
16704
17021
  fullS3Key: key,
16705
17022
  config
16706
17023
  });
@@ -16714,11 +17031,6 @@ ${style.markdown}` : params.prompt;
16714
17031
  app.delete(
16715
17032
  "/sessions/:sessionId/files",
16716
17033
  async (req, res) => {
16717
- const authResult = await requestValidators.authenticate(req);
16718
- if (!authResult.user?.id) {
16719
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16720
- return;
16721
- }
16722
17034
  const sessionId = req.params.sessionId;
16723
17035
  if (!sessionId) {
16724
17036
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16729,11 +17041,9 @@ ${style.markdown}` : params.prompt;
16729
17041
  res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
16730
17042
  return;
16731
17043
  }
16732
- const { fullSessionPrefix } = buildSessionPrefixes(
16733
- authResult.user.id,
16734
- sessionId
16735
- );
16736
- if (!key.startsWith(fullSessionPrefix)) {
17044
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
17045
+ if (!authed) return;
17046
+ if (!key.startsWith(authed.fullSessionPrefix)) {
16737
17047
  res.status(403).json({ detail: "Key does not belong to this session." });
16738
17048
  return;
16739
17049
  }
@@ -16752,11 +17062,6 @@ ${style.markdown}` : params.prompt;
16752
17062
  if (!req.headers.authorization && typeof req.query.auth === "string") {
16753
17063
  req.headers.authorization = `Bearer ${req.query.auth}`;
16754
17064
  }
16755
- const authResult = await requestValidators.authenticate(req);
16756
- if (!authResult.user?.id) {
16757
- res.status(authResult.code ?? 401).json({ detail: authResult.message });
16758
- return;
16759
- }
16760
17065
  const sessionId = req.params.sessionId;
16761
17066
  if (!sessionId) {
16762
17067
  res.status(400).json({ detail: "Missing sessionId in path." });
@@ -16767,11 +17072,9 @@ ${style.markdown}` : params.prompt;
16767
17072
  res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
16768
17073
  return;
16769
17074
  }
16770
- const { fullSessionPrefix } = buildSessionPrefixes(
16771
- authResult.user.id,
16772
- sessionId
16773
- );
16774
- if (!key.startsWith(fullSessionPrefix)) {
17075
+ const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
17076
+ if (!authed) return;
17077
+ if (!key.startsWith(authed.fullSessionPrefix)) {
16775
17078
  res.status(403).json({ detail: "Key does not belong to this session." });
16776
17079
  return;
16777
17080
  }
@@ -16875,7 +17178,7 @@ ${style.markdown}` : params.prompt;
16875
17178
  ` - tracking.json tracking events linked to the user`,
16876
17179
  ``
16877
17180
  ].join("\n");
16878
- const zip = new JSZip2();
17181
+ const zip = new JSZip3();
16879
17182
  zip.file("README.txt", readme);
16880
17183
  zip.file("user_data.json", JSON.stringify(userExport, null, 2));
16881
17184
  zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
@@ -20937,15 +21240,15 @@ var execute = async ({ contexts }) => {
20937
21240
  };
20938
21241
 
20939
21242
  // src/exulu/litellm/db-init.ts
20940
- import { existsSync as existsSync6, readdirSync } from "fs";
21243
+ import { existsSync as existsSync5, readdirSync } from "fs";
20941
21244
  import { resolve as resolve3 } from "path";
20942
21245
  import { spawnSync } from "child_process";
20943
21246
  import { Client } from "pg";
20944
21247
 
20945
21248
  // src/exulu/litellm/db-setup-check.ts
20946
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
21249
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
20947
21250
  var readLiteLLMDatabaseUrl = (configPath) => {
20948
- if (!existsSync5(configPath)) return void 0;
21251
+ if (!existsSync4(configPath)) return void 0;
20949
21252
  const text = readFileSync4(configPath, "utf8");
20950
21253
  const match = text.match(
20951
21254
  /^\s*database_url:\s*["']?([^"'\n#]+?)["']?\s*(#.*)?$/m
@@ -21148,7 +21451,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21148
21451
  const venvBin = resolve3(packageRoot, "ee/python/.venv/bin");
21149
21452
  const prismaCli = resolve3(venvBin, "prisma");
21150
21453
  const venvLibDir = resolve3(packageRoot, "ee/python/.venv/lib");
21151
- const pythonVersionDir = existsSync6(venvLibDir) ? readdirSync(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
21454
+ const pythonVersionDir = existsSync5(venvLibDir) ? readdirSync(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
21152
21455
  if (!pythonVersionDir) {
21153
21456
  warn2([
21154
21457
  `Could not find a python3.* directory under ${venvLibDir}.`,
@@ -21163,7 +21466,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21163
21466
  "site-packages/litellm/proxy"
21164
21467
  );
21165
21468
  const schemaPath = resolve3(litellmProxyDir, "schema.prisma");
21166
- if (!existsSync6(prismaCli)) {
21469
+ if (!existsSync5(prismaCli)) {
21167
21470
  warn2([
21168
21471
  `Prisma CLI not found at ${prismaCli}.`,
21169
21472
  `Run \`npm run python:setup\` to create the venv and install prisma.`,
@@ -21171,7 +21474,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
21171
21474
  ]);
21172
21475
  return;
21173
21476
  }
21174
- if (!existsSync6(schemaPath)) {
21477
+ if (!existsSync5(schemaPath)) {
21175
21478
  warn2([
21176
21479
  `LiteLLM Prisma schema not found at ${schemaPath}.`,
21177
21480
  `Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
@@ -21754,20 +22057,20 @@ import WordExtractor from "word-extractor";
21754
22057
  import { parseOfficeAsync as parseOfficeAsync2 } from "officeparser";
21755
22058
 
21756
22059
  // src/utils/python-executor.ts
21757
- import { exec as exec2 } from "child_process";
21758
- import { promisify as promisify2 } from "util";
21759
- import { resolve as resolve4, join as join2, dirname } from "path";
21760
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
22060
+ import { exec } from "child_process";
22061
+ import { promisify } from "util";
22062
+ import { resolve as resolve4, join, dirname } from "path";
22063
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
21761
22064
  import { fileURLToPath } from "url";
21762
- var execAsync2 = promisify2(exec2);
22065
+ var execAsync = promisify(exec);
21763
22066
  function getPackageRoot2() {
21764
22067
  const currentFile = fileURLToPath(import.meta.url);
21765
22068
  let currentDir = dirname(currentFile);
21766
22069
  let attempts = 0;
21767
22070
  const maxAttempts = 10;
21768
22071
  while (attempts < maxAttempts) {
21769
- const packageJsonPath = join2(currentDir, "package.json");
21770
- if (existsSync7(packageJsonPath)) {
22072
+ const packageJsonPath = join(currentDir, "package.json");
22073
+ if (existsSync6(packageJsonPath)) {
21771
22074
  try {
21772
22075
  const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
21773
22076
  if (packageJson.name === "@exulu/backend") {
@@ -21808,7 +22111,7 @@ function getVenvPath(packageRoot) {
21808
22111
  }
21809
22112
  function getPythonExecutable(packageRoot) {
21810
22113
  const venvPath = getVenvPath(packageRoot);
21811
- return join2(venvPath, "bin", "python");
22114
+ return join(venvPath, "bin", "python");
21812
22115
  }
21813
22116
  async function validatePythonEnvironmentForExecution(packageRoot) {
21814
22117
  const validation = await validatePythonEnvironment(packageRoot);
@@ -21831,7 +22134,7 @@ async function executePythonScript(config) {
21831
22134
  await validatePythonEnvironmentForExecution(packageRoot);
21832
22135
  }
21833
22136
  const resolvedScriptPath = resolve4(packageRoot, scriptPath);
21834
- if (!existsSync7(resolvedScriptPath)) {
22137
+ if (!existsSync6(resolvedScriptPath)) {
21835
22138
  throw new PythonExecutionError(
21836
22139
  `Python script not found: ${resolvedScriptPath}`,
21837
22140
  "",
@@ -21845,7 +22148,7 @@ async function executePythonScript(config) {
21845
22148
  });
21846
22149
  const command = `${pythonExecutable} "${resolvedScriptPath}" ${quotedArgs.join(" ")}`;
21847
22150
  try {
21848
- const { stdout, stderr } = await execAsync2(command, {
22151
+ const { stdout, stderr } = await execAsync(command, {
21849
22152
  cwd,
21850
22153
  timeout,
21851
22154
  env: {