@exulu/backend 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
|
-
|
|
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) {
|
|
@@ -18422,21 +18451,7 @@ function isOsJunkPath(path2) {
|
|
|
18422
18451
|
const basename = path2.split("/").pop() ?? "";
|
|
18423
18452
|
return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
|
|
18424
18453
|
}
|
|
18425
|
-
async function
|
|
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
|
-
}
|
|
18454
|
+
async function extractZipToPrefix(bytes, prefix, config) {
|
|
18440
18455
|
let zip;
|
|
18441
18456
|
try {
|
|
18442
18457
|
zip = await import_jszip.default.loadAsync(bytes);
|
|
@@ -18500,7 +18515,7 @@ async function extractBundleToS3(opts) {
|
|
|
18500
18515
|
}
|
|
18501
18516
|
let filesCount = 0;
|
|
18502
18517
|
for (const { relPath, content } of prepared) {
|
|
18503
|
-
const s3Key =
|
|
18518
|
+
const s3Key = `${prefix}${relPath}`;
|
|
18504
18519
|
await uploadFile(
|
|
18505
18520
|
content,
|
|
18506
18521
|
s3Key,
|
|
@@ -18509,12 +18524,77 @@ async function extractBundleToS3(opts) {
|
|
|
18509
18524
|
void 0,
|
|
18510
18525
|
void 0,
|
|
18511
18526
|
true
|
|
18512
|
-
// global=true —
|
|
18527
|
+
// global=true — skill files are shared across users
|
|
18513
18528
|
);
|
|
18514
18529
|
filesCount += 1;
|
|
18515
18530
|
}
|
|
18516
18531
|
return { filesCount };
|
|
18517
18532
|
}
|
|
18533
|
+
async function extractBundleToS3(opts) {
|
|
18534
|
+
const { bytes, skillId, isZip, config } = opts;
|
|
18535
|
+
if (!isZip) {
|
|
18536
|
+
await uploadFile(
|
|
18537
|
+
bytes,
|
|
18538
|
+
`skills/${skillId}/v1/SKILL.md`,
|
|
18539
|
+
config,
|
|
18540
|
+
{ contentType: "text/markdown" },
|
|
18541
|
+
void 0,
|
|
18542
|
+
void 0,
|
|
18543
|
+
true
|
|
18544
|
+
// global=true so the key isn't user-prefixed (skill files are shared)
|
|
18545
|
+
);
|
|
18546
|
+
return { filesCount: 1 };
|
|
18547
|
+
}
|
|
18548
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v1/`, config);
|
|
18549
|
+
}
|
|
18550
|
+
async function extractBundleToVersion(opts) {
|
|
18551
|
+
const { bytes, skillId, version, config } = opts;
|
|
18552
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v${version}/`, config);
|
|
18553
|
+
}
|
|
18554
|
+
|
|
18555
|
+
// src/skills/frontmatter.ts
|
|
18556
|
+
init_cjs_shims();
|
|
18557
|
+
var import_jszip2 = __toESM(require("jszip"), 1);
|
|
18558
|
+
function parseFrontmatter(md) {
|
|
18559
|
+
const match = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
18560
|
+
if (!match) return {};
|
|
18561
|
+
const block = match[1];
|
|
18562
|
+
const out = {};
|
|
18563
|
+
for (const line of block.split(/\r?\n/)) {
|
|
18564
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
18565
|
+
if (!m) continue;
|
|
18566
|
+
const key = m[1];
|
|
18567
|
+
let v = m[2].trim();
|
|
18568
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
18569
|
+
v = v.slice(1, -1);
|
|
18570
|
+
}
|
|
18571
|
+
out[key] = v;
|
|
18572
|
+
}
|
|
18573
|
+
return out;
|
|
18574
|
+
}
|
|
18575
|
+
async function parseSkillFrontmatter(zipBytes) {
|
|
18576
|
+
let zip;
|
|
18577
|
+
try {
|
|
18578
|
+
zip = await import_jszip2.default.loadAsync(zipBytes);
|
|
18579
|
+
} catch {
|
|
18580
|
+
return {};
|
|
18581
|
+
}
|
|
18582
|
+
const paths = [];
|
|
18583
|
+
zip.forEach((p, entry) => {
|
|
18584
|
+
if (!entry.dir) paths.push(p);
|
|
18585
|
+
});
|
|
18586
|
+
const heads = new Set(paths.map((p) => p.split("/")[0]).filter(Boolean));
|
|
18587
|
+
let strip = (p) => p;
|
|
18588
|
+
if (heads.size === 1) {
|
|
18589
|
+
const head = [...heads][0] + "/";
|
|
18590
|
+
if (paths.every((p) => p.startsWith(head))) strip = (p) => p.slice(head.length);
|
|
18591
|
+
}
|
|
18592
|
+
const skillPath = paths.find((p) => strip(p) === "SKILL.md");
|
|
18593
|
+
if (!skillPath) return {};
|
|
18594
|
+
const md = await zip.file(skillPath).async("string");
|
|
18595
|
+
const fm = parseFrontmatter(md);
|
|
18596
|
+
return { name: fm.name, description: fm.description };
|
|
18597
|
+
}
|
|
18518
18598
|
|
|
18519
18599
|
// src/sessions/pdf-preview-cache.ts
|
|
18520
18600
|
init_cjs_shims();
|
|
@@ -18598,7 +18678,7 @@ var import_fs3 = __toESM(require("fs"), 1);
|
|
|
18598
18678
|
var import_node_crypto9 = require("crypto");
|
|
18599
18679
|
var import_api2 = require("@opentelemetry/api");
|
|
18600
18680
|
init_check_record_access();
|
|
18601
|
-
var
|
|
18681
|
+
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
18602
18682
|
var import_ai15 = require("ai");
|
|
18603
18683
|
var import_cookie_parser = __toESM(require("cookie-parser"), 1);
|
|
18604
18684
|
init_statistics2();
|
|
@@ -21113,6 +21193,155 @@ var contentHeadersFor = (key, contentType, filename) => {
|
|
|
21113
21193
|
};
|
|
21114
21194
|
var getSharedArtifactByName = (db2, name) => db2("shared_artifacts").where({ name }).first();
|
|
21115
21195
|
|
|
21196
|
+
// src/skills/skill-access.ts
|
|
21197
|
+
init_cjs_shims();
|
|
21198
|
+
init_check_record_access();
|
|
21199
|
+
async function resolveSkillByName(db2, name) {
|
|
21200
|
+
const row = await db2("skills").where({ name }).first();
|
|
21201
|
+
return row ?? null;
|
|
21202
|
+
}
|
|
21203
|
+
async function canAccessSkill(db2, skill, action, user) {
|
|
21204
|
+
const rbac = await RBACResolver(db2, "skill", skill.id, skill.rights_mode || "private");
|
|
21205
|
+
return checkRecordAccess({ ...skill, RBAC: rbac }, action, user);
|
|
21206
|
+
}
|
|
21207
|
+
async function filterReadableSkills(db2, skills, user) {
|
|
21208
|
+
const out = [];
|
|
21209
|
+
for (const s of skills) {
|
|
21210
|
+
if (await canAccessSkill(db2, s, "read", user)) out.push(s);
|
|
21211
|
+
}
|
|
21212
|
+
return out;
|
|
21213
|
+
}
|
|
21214
|
+
|
|
21215
|
+
// src/skills/bootstrap/exulu-skills.ts
|
|
21216
|
+
init_cjs_shims();
|
|
21217
|
+
|
|
21218
|
+
// src/skills/bootstrap/clients.ts
|
|
21219
|
+
init_cjs_shims();
|
|
21220
|
+
var CLIENT_MANIFEST = [
|
|
21221
|
+
{ id: "agents", dir: ".agents/skills" },
|
|
21222
|
+
// cross-agent standard (symlink canonical store)
|
|
21223
|
+
{ id: "claude", dir: ".claude/skills" },
|
|
21224
|
+
{ id: "windsurf", dir: ".windsurf/skills" },
|
|
21225
|
+
{ id: "continue", dir: ".continue/skills" },
|
|
21226
|
+
{ id: "roo", dir: ".roo/skills" },
|
|
21227
|
+
{ id: "kilocode", dir: ".kilocode/skills" },
|
|
21228
|
+
{ id: "crush", dir: ".crush/skills" },
|
|
21229
|
+
{ id: "goose", dir: ".goose/skills" },
|
|
21230
|
+
{ id: "qwen", dir: ".qwen/skills" },
|
|
21231
|
+
{ id: "iflow", dir: ".iflow/skills" },
|
|
21232
|
+
{ id: "junie", dir: ".junie/skills" },
|
|
21233
|
+
{ id: "kiro", dir: ".kiro/skills" },
|
|
21234
|
+
{ id: "trae", dir: ".trae/skills" },
|
|
21235
|
+
{ id: "augment", dir: ".augment/skills" },
|
|
21236
|
+
{ id: "factory", dir: ".factory/skills" },
|
|
21237
|
+
{ id: "devin", dir: ".devin/skills" },
|
|
21238
|
+
{ id: "openhands", dir: ".openhands/skills" },
|
|
21239
|
+
{ id: "pi", dir: ".pi/skills" },
|
|
21240
|
+
{ id: "cortex", dir: ".cortex/skills" },
|
|
21241
|
+
{ id: "zencoder", dir: ".zencoder/skills" },
|
|
21242
|
+
{ id: "codebuddy", dir: ".codebuddy/skills" },
|
|
21243
|
+
{ id: "codestudio", dir: ".codestudio/skills" },
|
|
21244
|
+
{ id: "commandcode", dir: ".commandcode/skills" },
|
|
21245
|
+
{ id: "codemaker", dir: ".codemaker/skills" },
|
|
21246
|
+
{ id: "codeartsdoer", dir: ".codeartsdoer/skills" },
|
|
21247
|
+
{ id: "lingma", dir: ".lingma/skills" },
|
|
21248
|
+
{ id: "qoder", dir: ".qoder/skills" },
|
|
21249
|
+
{ id: "rovodev", dir: ".rovodev/skills" },
|
|
21250
|
+
{ id: "moxby", dir: ".moxby/skills" },
|
|
21251
|
+
{ id: "mux", dir: ".mux/skills" },
|
|
21252
|
+
{ id: "neovate", dir: ".neovate/skills" },
|
|
21253
|
+
{ id: "ona", dir: ".ona/skills" },
|
|
21254
|
+
{ id: "pochi", dir: ".pochi/skills" },
|
|
21255
|
+
{ id: "reasonix", dir: ".reasonix/skills" },
|
|
21256
|
+
{ id: "terramind", dir: ".terramind/skills" },
|
|
21257
|
+
{ id: "tinycloud", dir: ".tinycloud/skills" },
|
|
21258
|
+
{ id: "vibe", dir: ".vibe/skills" },
|
|
21259
|
+
{ id: "adal", dir: ".adal/skills" },
|
|
21260
|
+
{ id: "aider-desk", dir: ".aider-desk/skills" },
|
|
21261
|
+
{ id: "autohand", dir: ".autohand/skills" },
|
|
21262
|
+
{ id: "bob", dir: ".bob/skills" },
|
|
21263
|
+
{ id: "hermes", dir: ".hermes/skills" },
|
|
21264
|
+
{ id: "inferencesh", dir: ".inferencesh/skills" },
|
|
21265
|
+
{ id: "jazz", dir: ".jazz/skills" },
|
|
21266
|
+
{ id: "kode", dir: ".kode/skills" },
|
|
21267
|
+
{ id: "mcpjam", dir: ".mcpjam/skills" },
|
|
21268
|
+
{ id: "forge", dir: ".forge/skills" },
|
|
21269
|
+
{ id: "tabnine", dir: ".tabnine/agent/skills" }
|
|
21270
|
+
// exception: nested under agent/
|
|
21271
|
+
];
|
|
21272
|
+
|
|
21273
|
+
// src/skills/bootstrap/exulu-sh.generated.ts
|
|
21274
|
+
init_cjs_shims();
|
|
21275
|
+
var EXULU_SH_B64 = "IyEvYmluL3NoCiMgZXh1bHUg4oCUIGhlbHBlciBmb3IgdGhlIEV4dWx1IGNlbnRyYWwgc2tpbGwgbGlicmFyeS4gVGhlIGFnZW50IGludm9rZXMgdGhpcwojIChuZXZlciByYXcgY3VybCk6IHRoZSB0b2tlbiBpcyByZWFkIGZyb20gdGhlIGNvbmZpZyBmaWxlIGhlcmUgYW5kIHNlbnQgdmlhCiMgYHgtYXBpLWtleTogQmVhcmVyYCwgc28gaXQgbmV2ZXIgZW50ZXJzIHRoZSBtb2RlbCBjb250ZXh0LiBDbGllbnQgZmFuLW91dAojIChjb3B5L3N5bWxpbmsgYWNyb3NzIGFnZW50IGNsaWVudHMpIGlzIGRldGVybWluaXN0aWMuCnNldCAtZXUKCkNPTkZJR19ESVI9IiRIT01FLy5jb25maWcvZXh1bHUiCkNPTkZJR19GSUxFPSIkQ09ORklHX0RJUi9za2lsbHMuanNvbiIKCmRpZSgpIHsgcHJpbnRmICdleHVsdTogJXNcbicgIiQqIiA+JjI7IGV4aXQgMTsgfQppbmZvKCkgeyBwcmludGYgJyVzXG4nICIkKiIgPiYyOyB9Cgpqc29uX3N0cigpIHsgIyBqc29uX3N0ciA8a2V5PiA8ZmlsZT4g4oCUIGZsYXQgImtleSI6InZhbHVlIgogIHNlZCAtbiAicy8uKlwiJDFcIltbOnNwYWNlOl1dKjpbWzpzcGFjZTpdXSpcIlxcKFteXCJdKlxcKVwiLiovXFwxL3AiICIkMiIgfCBoZWFkIC1uMQp9CgpbIC1mICIkQ09ORklHX0ZJTEUiIF0gfHwgZGllICJub3QgY29uZmlndXJlZCDigJQgcnVuOiBjdXJsIC1mc1NMIDxiYXNlX3VybD4vYXBpL3NraWxscy9pbnN0YWxsLnNoIHwgc2giCgpCQVNFX1VSTD0iJChqc29uX3N0ciBiYXNlX3VybCAiJENPTkZJR19GSUxFIikiCkJBQ0tFTkQ9IiQoanNvbl9zdHIgYmFja2VuZCAiJENPTkZJR19GSUxFIikiCkFQSV9LRVk9IiQoanNvbl9zdHIgYXBpX2tleSAiJENPTkZJR19GSUxFIikiCkxJTktfTU9ERT0iJChqc29uX3N0ciBsaW5rX21vZGUgIiRDT05GSUdfRklMRSIpIgpTQ09QRT0iJChqc29uX3N0ciBzY29wZSAiJENPTkZJR19GSUxFIikiCkNMSUVOVFM9IiQoc2VkIC1uICdzLy4qImNsaWVudHMiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlxbXChbXl1dKlwpXF0uKi9cMS9wJyAiJENPTkZJR19GSUxFIiB8IHRyICcsJyAnICcgfCB0ciAtZCAnIicgfCB0ciAtcyAnICcpIgoKWyAtbiAiJENMSUVOVFMiIF0gfHwgQ0xJRU5UUz0iYWdlbnRzIgpbIC1uICIkTElOS19NT0RFIiBdIHx8IExJTktfTU9ERT0iY29weSIKWyAtbiAiJFNDT1BFIiBdIHx8IFNDT1BFPSJwcm9qZWN0IgoKaWYgWyAteiAiJEJBQ0tFTkQiIF07IHRoZW4KICBbIC1uICIkQkFTRV9VUkwiIF0gfHwgZGllICJjb25maWcgaGFzIG5vIGJhY2tlbmQgYW5kIG5vIGJhc2VfdXJsIgogIEJBQ0tFTkQ9IiQoY3VybCAtZnNTTCAiJEJBU0VfVVJML2FwaS9jb25maWciIHwgc2VkIC1uICdzLy4qImJhY2tlbmQiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKiJcKFteIl0qXCkiLiovXDEvcCcgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJEJBQ0tFTkQiIF0gfHwgZGllICJjb3VsZCBub3QgcmVzb2x2ZSBiYWNrZW5kIGZyb20gJEJBU0VfVVJML2FwaS9jb25maWciCmZpCkJBQ0tFTkQ9IiQocHJpbnRmICclcycgIiRCQUNLRU5EIiB8IHNlZCAnczovKiQ6OicpIgoKUk9PVD0iJFBXRCIKWyAiJFNDT1BFIiA9ICJob21lIiBdICYmIFJPT1Q9IiRIT01FIgoKIyBkaXJfZm9yIENMSUVOVF9JRCAtPiByZWxhdGl2ZSBza2lsbCBkaXIgKGdlbmVyYXRlZCBmcm9tIHRoZSBtYW5pZmVzdCBpbiB0aGUKIyByZWFsIGJ1aWxkOyBhIHJlcHJlc2VudGF0aXZlIHN1YnNldCBoZXJlIGZvciBsb2NhbCB0ZXN0aW5nKS4KZGlyX2ZvcigpIHsKICBjYXNlICIkMSIgaW4KX19ESVJfRk9SX0NBU0VTX18KICAgICopIHJldHVybiAxIDs7CiAgZXNhYwp9CgphcGkoKSB7ICMgYXBpIDxNRVRIT0Q+IDxwYXRoPiBbZXh0cmEgY3VybCBhcmdzLi4uXSAtPiBib2R5IG9uIHN0ZG91dAogIG09IiQxIjsgcD0iJDIiOyBzaGlmdCAyCiAgY3VybCAtZnNTIC1YICIkbSIgIiRCQUNLRU5EJHAiIC1IICJ4LWFwaS1rZXk6IEJlYXJlciAkQVBJX0tFWSIgIiRAIgp9CgptZXRhX3ZlcnNpb24oKSB7ICMgbWV0YV92ZXJzaW9uIDxuYW1lPiAtPiBjdXJyZW50X3ZlcnNpb24gZnJvbSByZWdpc3RyeQogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJDEiIDI+L2Rldi9udWxsIFwKICAgIHwgc2VkIC1uICdzLy4qImN1cnJlbnRfdmVyc2lvbiJbWzpzcGFjZTpdXSo6W1s6c3BhY2U6XV0qXChbMC05XVswLTldKlwpLiovXDEvcCcgfCBoZWFkIC1uMQp9CgpfbWFuYWdlZCgpIHsgIyBfbWFuYWdlZCA8ZGlyPiAtPiAwIGlmIHNhZmUgdG8gb3ZlcndyaXRlIChvdXJzIG9yIHN5bWxpbmsgb3IgYWJzZW50KQogIGQ9IiQxIgogIGlmIFsgLWUgIiRkIiBdICYmIFsgISAtTCAiJGQiIF0gJiYgWyAhIC1mICIkZC8uZXh1bHUtc2tpbGwuanNvbiIgXTsgdGhlbgogICAgaW5mbyAic2tpcCAkZCAoZXhpc3RzLCBub3QgbWFuYWdlZCBieSBleHVsdSkiOyByZXR1cm4gMQogIGZpCiAgcmV0dXJuIDAKfQoKX3B1dF9yZWFsKCkgeyAjIF9wdXRfcmVhbCA8ZGVzdD4gPHNyY2Rpcj4gPG1hcmtlci1qc29uPgogIF9tYW5hZ2VkICIkMSIgfHwgcmV0dXJuIDAKICBybSAtcmYgIiQxIjsgbWtkaXIgLXAgIiQxIjsgY3AgLVIgIiQyLy4iICIkMS8iCiAgcHJpbnRmICclc1xuJyAiJDMiID4gIiQxLy5leHVsdS1za2lsbC5qc29uIgp9CgpwbGFjZV9za2lsbCgpIHsgIyBwbGFjZV9za2lsbCA8bmFtZT4gPHNyY2Rpcj4gPHZlcnNpb24+CiAgcG5hbWU9IiQxIjsgcHNyYz0iJDIiOyBwdmVyPSIkezM6LTF9IgogIG1hcmtlcj0neyAibmFtZSI6ICInIiRwbmFtZSInIiwgInZlcnNpb24iOiAnIiRwdmVyIicsICJzb3VyY2UiOiAiJyIkQkFDS0VORCInIiB9JwogIGNhbm9uPSIkUk9PVC8uYWdlbnRzL3NraWxscy8kcG5hbWUiCiAgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBfcHV0X3JlYWwgIiRjYW5vbiIgIiRwc3JjIiAiJG1hcmtlciIKICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICBkPSIkKGRpcl9mb3IgIiRpZCIpIiB8fCB7IGluZm8gInVua25vd24gY2xpZW50OiAkaWQiOyBjb250aW51ZTsgfQogICAgcGFyZW50PSIkUk9PVC8kZCI7IGRlc3Q9IiRwYXJlbnQvJHBuYW1lIgogICAgaWYgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBbICIkaWQiICE9ICJhZ2VudHMiIF07IHRoZW4KICAgICAgX21hbmFnZWQgIiRkZXN0IiB8fCBjb250aW51ZQogICAgICBta2RpciAtcCAiJHBhcmVudCI7IHJtIC1yZiAiJGRlc3QiCiAgICAgIGlmIGxuIC1zICIkY2Fub24iICIkZGVzdCIgMj4vZGV2L251bGw7IHRoZW4KICAgICAgICBpbmZvICJsaW5rZWQgJGRlc3QgLT4gJGNhbm9uIgogICAgICBlbHNlCiAgICAgICAgaW5mbyAic3ltbGluayB1bnN1cHBvcnRlZCBhdCAkZGVzdDsgY29weWluZyIKICAgICAgICBfcHV0X3JlYWwgIiRkZXN0IiAiJHBzcmMiICIkbWFya2VyIgogICAgICBmaQogICAgZWxpZiBbICIkTElOS19NT0RFIiA9ICJjb3B5IiBdOyB0aGVuCiAgICAgIF9wdXRfcmVhbCAiJGRlc3QiICIkcHNyYyIgIiRtYXJrZXIiCiAgICBmaQogICAgIyBzeW1saW5rICsgYWdlbnRzOiBhbHJlYWR5IHBsYWNlZCBhcyB0aGUgY2Fub25pY2FsIHN0b3JlIGFib3ZlLgogIGRvbmUKfQoKaW5zdGFsbGVkX25hbWVzKCkgeyAjIHVuaXF1ZSBza2lsbCBuYW1lcyB0aGF0IGNhcnJ5IG91ciBtYXJrZXIgdW5kZXIgUk9PVAogIHsgZmluZCAiJFJPT1QvLmFnZW50cy9za2lsbHMiIC1tYXhkZXB0aCAyIC1uYW1lIC5leHVsdS1za2lsbC5qc29uIDI+L2Rldi9udWxsCiAgICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICAgIGQ9IiQoZGlyX2ZvciAiJGlkIikiIHx8IGNvbnRpbnVlCiAgICAgIGZpbmQgIiRST09ULyRkIiAtbWF4ZGVwdGggMiAtbmFtZSAuZXh1bHUtc2tpbGwuanNvbiAyPi9kZXYvbnVsbAogICAgZG9uZQogIH0gfCB3aGlsZSByZWFkIC1yIG07IGRvIGpzb25fc3RyIG5hbWUgIiRtIjsgZG9uZSB8IHNvcnQgLXUKfQoKbWFya2VyX3ZlcnNpb24oKSB7ICMgbWFya2VyX3ZlcnNpb24gPG5hbWU+CiAgZm9yIGJhc2UgaW4gIiRST09ULy5hZ2VudHMvc2tpbGxzIjsgZG8KICAgIFsgLWYgIiRiYXNlLyQxLy5leHVsdS1za2lsbC5qc29uIiBdICYmIHsganNvbl9zdHIgdmVyc2lvbiAiJGJhc2UvJDEvLmV4dWx1LXNraWxsLmpzb24iOyByZXR1cm47IH0KICBkb25lCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgY29udGludWUKICAgIGY9IiRST09ULyRkLyQxLy5leHVsdS1za2lsbC5qc29uIgogICAgWyAtZiAiJGYiIF0gJiYgeyBqc29uX3N0ciB2ZXJzaW9uICIkZiI7IHJldHVybjsgfQogIGRvbmUKfQoKZG9faW5zdGFsbCgpIHsgIyBkb19pbnN0YWxsIDxuYW1lPgogIG5hbWU9IiQxIgogIFRNUD0iJChta3RlbXAgLWQpIgogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJG5hbWUvZG93bmxvYWQiIC0tb3V0cHV0ICIkVE1QL3NraWxsLnppcCIgXAogICAgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImRvd25sb2FkIGZhaWxlZCBmb3IgJyRuYW1lJyAoNDAzID0gbm8gYWNjZXNzLCA0MDQgPSB1bmtub3duKSI7IH0KICBta2RpciAtcCAiJFRNUC94IgogIHVuemlwIC1xICIkVE1QL3NraWxsLnppcCIgLWQgIiRUTVAveCIgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImJhZCBhcmNoaXZlIGZvciAnJG5hbWUnIjsgfQogIHNyYz0iJChmaW5kICIkVE1QL3giIC1taW5kZXB0aCAxIC1tYXhkZXB0aCAxIC10eXBlIGQgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJHNyYyIgXSB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAidW5leHBlY3RlZCBhcmNoaXZlIGxheW91dCBmb3IgJyRuYW1lJyI7IH0KICB2ZXI9IiQobWV0YV92ZXJzaW9uICIkbmFtZSIpIgogIHBsYWNlX3NraWxsICIkbmFtZSIgIiRzcmMiICIke3ZlcjotMX0iCiAgcm0gLXJmICIkVE1QIgogIGluZm8gImluc3RhbGxlZCAkbmFtZSAodiR7dmVyOi0xfSkgWyRMSU5LX01PREVdIGludG86ICRDTElFTlRTIgp9Cgp1c2FnZSgpIHsKICBjYXQgPiYyIDw8RU9GCmV4dWx1IOKAlCBFeHVsdSBza2lsbCBsaWJyYXJ5IGhlbHBlcgogIGV4dWx1IGxpc3QgICAgICAgICAgICAgICAgIGxpc3Qgc2tpbGxzIHlvdSBjYW4gYWNjZXNzIChKU09OKQogIGV4dWx1IGdldCA8bmFtZT4gICAgICAgICAgIHNob3cgb25lIHNraWxsJ3MgbWV0YWRhdGEgKEpTT04pCiAgZXh1bHUgaW5zdGFsbCA8bmFtZT4gICAgICAgaW5zdGFsbC9yZWZyZXNoIGEgc2tpbGwgaW50byB5b3VyIGFnZW50IGNsaWVudHMKICBleHVsdSB1cGRhdGUgWzxuYW1lPl0gICAgICB1cGRhdGUgaW5zdGFsbGVkIHNraWxscyAoYWxsLCBvciBvbmUpIHRvIGxhdGVzdAogIGV4dWx1IHB1Ymxpc2ggPG5hbWU+IDxkaXI+IHB1Ymxpc2ggYSBsb2NhbCBza2lsbCBmb2xkZXIgYXMgPG5hbWU+CiAgZXh1bHUgY29uZmlnICAgICAgICAgICAgICAgc2hvdyByZXNvbHZlZCBiYWNrZW5kIC8gc2NvcGUgLyBjbGllbnRzIChubyBzZWNyZXRzKQpFT0YKfQoKY21kPSIkezE6LWhlbHB9IgpbICQjIC1ndCAwIF0gJiYgc2hpZnQgfHwgdHJ1ZQoKY2FzZSAiJGNtZCIgaW4KICBsaXN0KSBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5IiA7OwogIGdldCkgWyAkIyAtZ2UgMSBdIHx8IGRpZSAidXNhZ2U6IGV4dWx1IGdldCA8bmFtZT4iOyBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyQxIiA7OwogIGluc3RhbGwpIFsgJCMgLWdlIDEgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBpbnN0YWxsIDxuYW1lPiI7IGRvX2luc3RhbGwgIiQxIiA7OwogIHVwZGF0ZSkKICAgIGlmIFsgJCMgLWdlIDEgXTsgdGhlbiBuYW1lcz0iJDEiOyBlbHNlIG5hbWVzPSIkKGluc3RhbGxlZF9uYW1lcykiOyBmaQogICAgWyAtbiAiJG5hbWVzIiBdIHx8IHsgaW5mbyAibm8gZXh1bHUtbWFuYWdlZCBza2lsbHMgZm91bmQgdW5kZXIgJFJPT1QiOyBleGl0IDA7IH0KICAgIGZvciBuIGluICRuYW1lczsgZG8KICAgICAgY3VyPSIkKG1hcmtlcl92ZXJzaW9uICIkbiIpIgogICAgICBsYXRlc3Q9IiQobWV0YV92ZXJzaW9uICIkbiIpIgogICAgICBbIC1uICIkbGF0ZXN0IiBdIHx8IHsgaW5mbyAic2tpcCAkbiAobm90IGluIHJlZ2lzdHJ5KSI7IGNvbnRpbnVlOyB9CiAgICAgIGlmIFsgLXogIiRjdXIiIF0gfHwgWyAiJGxhdGVzdCIgLWd0ICIkY3VyIiBdIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAidXBkYXRpbmcgJG46IHYke2N1cjotP30gLT4gdiRsYXRlc3QiOyBkb19pbnN0YWxsICIkbiIKICAgICAgZWxzZQogICAgICAgIGluZm8gIiRuIHVwIHRvIGRhdGUgKHYkY3VyKSIKICAgICAgZmkKICAgIGRvbmUKICAgIDs7CiAgcHVibGlzaCkKICAgIFsgJCMgLWdlIDIgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBwdWJsaXNoIDxuYW1lPiA8ZGlyPiIKICAgIG5hbWU9IiQxIjsgZm9sZGVyPSIkMiIKICAgIFsgLWQgIiRmb2xkZXIiIF0gfHwgZGllICJubyBzdWNoIGZvbGRlcjogJGZvbGRlciIKICAgIFsgLWYgIiRmb2xkZXIvU0tJTEwubWQiIF0gfHwgZGllICIkZm9sZGVyIGhhcyBubyBTS0lMTC5tZCBhdCBpdHMgcm9vdCIKICAgIGNvbW1hbmQgLXYgemlwID4vZGV2L251bGwgMj4mMSB8fCBkaWUgInRoZSAnemlwJyBjb21tYW5kIGlzIHJlcXVpcmVkIHRvIHB1Ymxpc2giCiAgICBUTVA9IiQobWt0ZW1wIC1kKSIKICAgICggY2QgIiQoZGlybmFtZSAiJGZvbGRlciIpIiBcCiAgICAgICYmIHppcCAtcSAtciAtWCAiJFRNUC9za2lsbC56aXAiICIkKGJhc2VuYW1lICIkZm9sZGVyIikiIFwKICAgICAgICAgICAteCAnKi8uZXh1bHUtc2tpbGwuanNvbicgJyovLmdpdC8qJyAnKi5EU19TdG9yZScgJyovX19NQUNPU1gvKicgKSBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJjb3VsZCBub3QgemlwICRmb2xkZXIiOyB9CiAgICBhcGkgUE9TVCAiL3NraWxscy9yZWdpc3RyeS8kbmFtZSIgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vemlwIiBcCiAgICAgIC0tZGF0YS1iaW5hcnkgQCIkVE1QL3NraWxsLnppcCIgXAogICAgICB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAicHVibGlzaCBmYWlsZWQgKDQwMyA9IG5vIHdyaXRlIGFjY2VzcywgNDA5ID0gbmFtZSB0YWtlbikiOyB9CiAgICBybSAtcmYgIiRUTVAiCiAgICBpbmZvICJwdWJsaXNoZWQgJG5hbWUiCiAgICA7OwogIGNvbmZpZykKICAgIHByaW50ZiAnYmFja2VuZD0lc1xuc2NvcGU9JXNcbmxpbmtfbW9kZT0lc1xuY2xpZW50cz0lc1xuYXBpX2tleT0lc1xuJyBcCiAgICAgICIkQkFDS0VORCIgIiRTQ09QRSIgIiRMSU5LX01PREUiICIkQ0xJRU5UUyIgXAogICAgICAiJChbIC1uICIkQVBJX0tFWSIgXSAmJiBlY2hvIHNldCB8fCBlY2hvIE1JU1NJTkcpIgogICAgOzsKICBoZWxwfC0taGVscHwtaCkgdXNhZ2UgOzsKICAqKSBpbmZvICJ1bmtub3duIGNvbW1hbmQ6ICRjbWQiOyB1c2FnZTsgZXhpdCAyIDs7CmVzYWMK";
|
|
21276
|
+
|
|
21277
|
+
// src/skills/bootstrap/exulu-skills.ts
|
|
21278
|
+
var BOOTSTRAP_CLIENTS_JSON = JSON.stringify(CLIENT_MANIFEST, null, 2);
|
|
21279
|
+
var DIR_FOR_CASES = CLIENT_MANIFEST.map(
|
|
21280
|
+
(c) => ` ${c.id}) printf '%s' '${c.dir}' ;;`
|
|
21281
|
+
).join("\n");
|
|
21282
|
+
var BOOTSTRAP_EXULU_SH = Buffer.from(EXULU_SH_B64, "base64").toString("utf8").replace("__DIR_FOR_CASES__", DIR_FOR_CASES);
|
|
21283
|
+
var BOOTSTRAP_SKILL_MD = `---
|
|
21284
|
+
name: exulu-skills
|
|
21285
|
+
description: Install, update, and publish skills from this Exulu instance's central skill library. Use when the user asks to install a skill, get the latest version of a skill, list available skills, or publish a skill to Exulu.
|
|
21286
|
+
---
|
|
21287
|
+
|
|
21288
|
+
# Exulu Skills
|
|
21289
|
+
|
|
21290
|
+
Bridge to the Exulu central skill library. **All operations go through the
|
|
21291
|
+
bundled helper script \u2014 do not hand-write curl or copy files yourself.** The
|
|
21292
|
+
script reads the API token from config (keeping it out of this conversation) and
|
|
21293
|
+
handles the multi-client copy/symlink fan-out deterministically.
|
|
21294
|
+
|
|
21295
|
+
## The helper
|
|
21296
|
+
|
|
21297
|
+
Run the script next to this file, \`scripts/exulu\`, with \`sh\` and the absolute
|
|
21298
|
+
path of this skill's directory:
|
|
21299
|
+
|
|
21300
|
+
\`\`\`
|
|
21301
|
+
sh "<this-skill-dir>/scripts/exulu" <command>
|
|
21302
|
+
\`\`\`
|
|
21303
|
+
|
|
21304
|
+
Commands:
|
|
21305
|
+
- \`list\` \u2014 skills you can access (JSON on stdout)
|
|
21306
|
+
- \`get <name>\` \u2014 one skill's metadata (JSON)
|
|
21307
|
+
- \`install <name>\` \u2014 install/refresh a skill into the user's agent clients
|
|
21308
|
+
- \`update [<name>]\` \u2014 update every installed skill, or just \`<name>\`, to latest
|
|
21309
|
+
- \`publish <name> <folder>\` \u2014 publish a local skill folder as \`<name>\`
|
|
21310
|
+
- \`config\` \u2014 show resolved backend / scope / clients (prints no secrets)
|
|
21311
|
+
|
|
21312
|
+
The token, backend URL, target clients, copy-vs-symlink mode, and scope all come
|
|
21313
|
+
from \`~/.config/exulu/skills.json\` (written by the installer). Never print the
|
|
21314
|
+
\`api_key\` or read it into your reply \u2014 the script uses it internally.
|
|
21315
|
+
|
|
21316
|
+
## Requests \u2192 commands
|
|
21317
|
+
|
|
21318
|
+
- "list / search skills" \u2192 \`exulu list\`, then filter the JSON for the user.
|
|
21319
|
+
- "install skill X" / "add the X skill" \u2192 \`exulu install X\`.
|
|
21320
|
+
- "update / get the latest version [of X]" \u2192 \`exulu update [X]\`.
|
|
21321
|
+
- "publish / upload this skill as X" \u2192 confirm the target name with the user; for
|
|
21322
|
+
an existing skill run \`exulu get X\` first and confirm a new version is intended;
|
|
21323
|
+
then \`exulu publish X <folder>\`.
|
|
21324
|
+
|
|
21325
|
+
## Not configured yet?
|
|
21326
|
+
|
|
21327
|
+
If \`exulu config\` reports it's not configured (or \`~/.config/exulu/skills.json\`
|
|
21328
|
+
is missing), tell the user to run the installer \u2014 it sets everything up
|
|
21329
|
+
interactively (base URL, API key, target clients, copy/symlink):
|
|
21330
|
+
|
|
21331
|
+
\`\`\`
|
|
21332
|
+
curl -fsSL <base_url>/api/skills/install.sh | sh
|
|
21333
|
+
\`\`\`
|
|
21334
|
+
|
|
21335
|
+
\`<base_url>\` is their Exulu frontend URL (e.g. https://ai.open.de). They can
|
|
21336
|
+
create an API key at \`<base_url>/token\`.
|
|
21337
|
+
|
|
21338
|
+
## Errors
|
|
21339
|
+
|
|
21340
|
+
- install: \`403\` = no access to that skill; \`404\` = unknown name.
|
|
21341
|
+
- publish: \`403\` = you can see it but lack write access; \`409\` = the name is
|
|
21342
|
+
taken by a skill you can't access.
|
|
21343
|
+
`;
|
|
21344
|
+
|
|
21116
21345
|
// src/exulu/routes.ts
|
|
21117
21346
|
var REQUEST_SIZE_LIMIT = "50mb";
|
|
21118
21347
|
var getExuluVersionNumber = async () => {
|
|
@@ -21186,6 +21415,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
|
|
|
21186
21415
|
}
|
|
21187
21416
|
next();
|
|
21188
21417
|
});
|
|
21418
|
+
const rawZip = import_express5.default.raw({ type: ["application/zip", "application/octet-stream", "application/x-zip-compressed", "application/x-zip"], limit: "50mb" });
|
|
21189
21419
|
console.log(`
|
|
21190
21420
|
\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
21421
|
\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
|
|
@@ -22838,7 +23068,9 @@ ${style.markdown}` : params.prompt;
|
|
|
22838
23068
|
const budget_duration = String(body?.budget_duration ?? "");
|
|
22839
23069
|
if (!Number.isFinite(max_budget) || max_budget <= 0) return null;
|
|
22840
23070
|
if (!BUDGET_ALLOWED_DURATIONS.has(budget_duration)) return null;
|
|
22841
|
-
|
|
23071
|
+
const reset = parseResetAt(body?.budget_reset_at);
|
|
23072
|
+
if (!reset.valid) return null;
|
|
23073
|
+
return { max_budget, budget_duration, budget_reset_at: reset.value };
|
|
22842
23074
|
};
|
|
22843
23075
|
const parseBudgetSettingsBody = (body) => {
|
|
22844
23076
|
if (!body || typeof body !== "object") return null;
|
|
@@ -22908,7 +23140,7 @@ ${style.markdown}` : params.prompt;
|
|
|
22908
23140
|
}
|
|
22909
23141
|
const body = parseBudgetBody(req.body);
|
|
22910
23142
|
if (!body) {
|
|
22911
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
23143
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
22912
23144
|
return;
|
|
22913
23145
|
}
|
|
22914
23146
|
const entityIds = Array.isArray(req.body?.entityIds) ? req.body.entityIds : [];
|
|
@@ -22920,7 +23152,7 @@ ${style.markdown}` : params.prompt;
|
|
|
22920
23152
|
for (const id of entityIds) {
|
|
22921
23153
|
const tag = budgetTagFor(entityType, id);
|
|
22922
23154
|
try {
|
|
22923
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
23155
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
22924
23156
|
results.push({ entityId: String(id), ok: true });
|
|
22925
23157
|
} catch (err) {
|
|
22926
23158
|
results.push({
|
|
@@ -22945,12 +23177,12 @@ ${style.markdown}` : params.prompt;
|
|
|
22945
23177
|
}
|
|
22946
23178
|
const body = parseBudgetBody(req.body);
|
|
22947
23179
|
if (!body) {
|
|
22948
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
23180
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
22949
23181
|
return;
|
|
22950
23182
|
}
|
|
22951
23183
|
const tag = budgetTagFor(entityType, req.params.entityId ?? "");
|
|
22952
23184
|
try {
|
|
22953
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
23185
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
22954
23186
|
const info = await tagInfo([tag]);
|
|
22955
23187
|
res.status(200).json({ budget: info[tag] ?? null });
|
|
22956
23188
|
} catch (err) {
|
|
@@ -23413,6 +23645,199 @@ ${style.markdown}` : params.prompt;
|
|
|
23413
23645
|
}
|
|
23414
23646
|
return root;
|
|
23415
23647
|
}
|
|
23648
|
+
app.get("/skills/agent/bootstrap", async (_req, res) => {
|
|
23649
|
+
try {
|
|
23650
|
+
const zip = new import_jszip3.default();
|
|
23651
|
+
zip.file("exulu-skills/SKILL.md", BOOTSTRAP_SKILL_MD);
|
|
23652
|
+
zip.file("exulu-skills/references/clients.json", BOOTSTRAP_CLIENTS_JSON);
|
|
23653
|
+
zip.file("exulu-skills/scripts/exulu", BOOTSTRAP_EXULU_SH, {
|
|
23654
|
+
unixPermissions: 493
|
|
23655
|
+
});
|
|
23656
|
+
const buffer = await zip.generateAsync({
|
|
23657
|
+
type: "nodebuffer",
|
|
23658
|
+
platform: "UNIX"
|
|
23659
|
+
});
|
|
23660
|
+
res.setHeader("Content-Type", "application/zip");
|
|
23661
|
+
res.setHeader("Content-Disposition", 'attachment; filename="exulu-skills.zip"');
|
|
23662
|
+
res.send(buffer);
|
|
23663
|
+
} catch (err) {
|
|
23664
|
+
console.error("[SKILLS] Failed to build bootstrap zip", err);
|
|
23665
|
+
res.status(500).json({ detail: "Failed to build bootstrap skill." });
|
|
23666
|
+
}
|
|
23667
|
+
});
|
|
23668
|
+
app.get("/skills/registry", async (req, res) => {
|
|
23669
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23670
|
+
if (!authResult.user?.id) {
|
|
23671
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23672
|
+
return;
|
|
23673
|
+
}
|
|
23674
|
+
const { db: db2 } = await postgresClient();
|
|
23675
|
+
const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
|
|
23676
|
+
const all = await db2("skills").select("*");
|
|
23677
|
+
const readable = await filterReadableSkills(db2, all, authResult.user);
|
|
23678
|
+
const skills = readable.filter((s) => {
|
|
23679
|
+
if (!tag) return true;
|
|
23680
|
+
const tags = Array.isArray(s.tags) ? s.tags : [];
|
|
23681
|
+
return tags.includes(tag);
|
|
23682
|
+
}).map((s) => ({
|
|
23683
|
+
name: s.name,
|
|
23684
|
+
description: s.description ?? "",
|
|
23685
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
23686
|
+
current_version: s.current_version ?? 1,
|
|
23687
|
+
updated_at: s.updatedAt ?? s.updated_at ?? null
|
|
23688
|
+
}));
|
|
23689
|
+
res.json({ skills });
|
|
23690
|
+
});
|
|
23691
|
+
app.get("/skills/registry/:name/download", async (req, res) => {
|
|
23692
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23693
|
+
if (!authResult.user?.id) {
|
|
23694
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23695
|
+
return;
|
|
23696
|
+
}
|
|
23697
|
+
const { db: db2 } = await postgresClient();
|
|
23698
|
+
const skill = await resolveSkillByName(db2, req.params.name);
|
|
23699
|
+
if (!skill) {
|
|
23700
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
23701
|
+
return;
|
|
23702
|
+
}
|
|
23703
|
+
if (!await canAccessSkill(db2, skill, "read", authResult.user)) {
|
|
23704
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
23705
|
+
return;
|
|
23706
|
+
}
|
|
23707
|
+
const vQuery = req.query.version;
|
|
23708
|
+
const version = !vQuery || vQuery === "latest" ? skill.current_version ?? 1 : Number(vQuery);
|
|
23709
|
+
if (!Number.isFinite(version) || version < 1) {
|
|
23710
|
+
res.status(400).json({ detail: "Invalid version." });
|
|
23711
|
+
return;
|
|
23712
|
+
}
|
|
23713
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
23714
|
+
const versionPrefix = `skills/${skill.id}/v${version}/`;
|
|
23715
|
+
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
23716
|
+
if (files.length === 0) {
|
|
23717
|
+
res.status(404).json({ detail: `Version v${version} has no files.` });
|
|
23718
|
+
return;
|
|
23719
|
+
}
|
|
23720
|
+
const zip = new import_jszip3.default();
|
|
23721
|
+
for (const file of files) {
|
|
23722
|
+
const idx = file.key.indexOf(versionPrefix);
|
|
23723
|
+
const rel = idx >= 0 ? file.key.slice(idx + versionPrefix.length) : file.key;
|
|
23724
|
+
if (!rel) continue;
|
|
23725
|
+
const bytes = await getS3ObjectBytes(file.key, config);
|
|
23726
|
+
zip.file(`${safeName}/${rel}`, bytes);
|
|
23727
|
+
}
|
|
23728
|
+
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
23729
|
+
res.setHeader("Content-Type", "application/zip");
|
|
23730
|
+
res.setHeader("Content-Disposition", `attachment; filename="${safeName}.skill"`);
|
|
23731
|
+
res.send(buffer);
|
|
23732
|
+
});
|
|
23733
|
+
app.post("/skills/registry/:name", rawZip, async (req, res) => {
|
|
23734
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23735
|
+
if (!authResult.user?.id) {
|
|
23736
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23737
|
+
return;
|
|
23738
|
+
}
|
|
23739
|
+
const name = req.params.name;
|
|
23740
|
+
const bytes = req.body;
|
|
23741
|
+
if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
|
|
23742
|
+
res.status(400).json({ detail: "Empty body. Send the skill as a zip/.skill payload." });
|
|
23743
|
+
return;
|
|
23744
|
+
}
|
|
23745
|
+
const { db: db2 } = await postgresClient();
|
|
23746
|
+
const existing = await resolveSkillByName(db2, name);
|
|
23747
|
+
if (existing) {
|
|
23748
|
+
const canWrite = await canAccessSkill(db2, existing, "write", authResult.user);
|
|
23749
|
+
if (canWrite) {
|
|
23750
|
+
const nextVersion = (existing.current_version ?? 1) + 1;
|
|
23751
|
+
try {
|
|
23752
|
+
await extractBundleToVersion({ bytes, skillId: existing.id, version: nextVersion, config });
|
|
23753
|
+
} catch (err) {
|
|
23754
|
+
if (err instanceof BundleValidationError) {
|
|
23755
|
+
res.status(400).json({ detail: err.message });
|
|
23756
|
+
return;
|
|
23757
|
+
}
|
|
23758
|
+
console.error("[SKILLS] publish (new version) failed", err);
|
|
23759
|
+
res.status(500).json({ detail: "Failed to publish new version." });
|
|
23760
|
+
return;
|
|
23761
|
+
}
|
|
23762
|
+
const history = Array.isArray(existing.history) ? existing.history : [];
|
|
23763
|
+
await db2("skills").where({ id: existing.id }).update({
|
|
23764
|
+
current_version: nextVersion,
|
|
23765
|
+
history: JSON.stringify([
|
|
23766
|
+
...history,
|
|
23767
|
+
{ version: nextVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
23768
|
+
])
|
|
23769
|
+
});
|
|
23770
|
+
res.json({ name, version: nextVersion, created: false });
|
|
23771
|
+
return;
|
|
23772
|
+
} else {
|
|
23773
|
+
const canRead = await canAccessSkill(db2, existing, "read", authResult.user);
|
|
23774
|
+
if (!canRead) {
|
|
23775
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
23776
|
+
return;
|
|
23777
|
+
}
|
|
23778
|
+
res.status(403).json({ detail: "You don't have write access to this skill." });
|
|
23779
|
+
return;
|
|
23780
|
+
}
|
|
23781
|
+
}
|
|
23782
|
+
const meta = await parseSkillFrontmatter(bytes);
|
|
23783
|
+
const skillId = (0, import_node_crypto9.randomUUID)();
|
|
23784
|
+
try {
|
|
23785
|
+
await extractBundleToVersion({ bytes, skillId, version: 1, config });
|
|
23786
|
+
} catch (err) {
|
|
23787
|
+
if (err instanceof BundleValidationError) {
|
|
23788
|
+
res.status(400).json({ detail: err.message });
|
|
23789
|
+
return;
|
|
23790
|
+
}
|
|
23791
|
+
console.error("[SKILLS] publish (create) failed", err);
|
|
23792
|
+
res.status(500).json({ detail: "Failed to publish skill." });
|
|
23793
|
+
return;
|
|
23794
|
+
}
|
|
23795
|
+
try {
|
|
23796
|
+
await db2("skills").insert({
|
|
23797
|
+
id: skillId,
|
|
23798
|
+
name,
|
|
23799
|
+
description: meta.description ?? "",
|
|
23800
|
+
s3folder: `skills/${skillId}`,
|
|
23801
|
+
tags: JSON.stringify([]),
|
|
23802
|
+
usage_count: 0,
|
|
23803
|
+
favorite_count: 0,
|
|
23804
|
+
current_version: 1,
|
|
23805
|
+
history: JSON.stringify([
|
|
23806
|
+
{ version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
23807
|
+
]),
|
|
23808
|
+
rights_mode: "private",
|
|
23809
|
+
created_by: authResult.user.id
|
|
23810
|
+
});
|
|
23811
|
+
} catch (err) {
|
|
23812
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
23813
|
+
return;
|
|
23814
|
+
}
|
|
23815
|
+
res.json({ name, version: 1, created: true });
|
|
23816
|
+
});
|
|
23817
|
+
app.get("/skills/registry/:name", async (req, res) => {
|
|
23818
|
+
const authResult = await requestValidators.authenticate(req);
|
|
23819
|
+
if (!authResult.user?.id) {
|
|
23820
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
23821
|
+
return;
|
|
23822
|
+
}
|
|
23823
|
+
const { db: db2 } = await postgresClient();
|
|
23824
|
+
const skill = await resolveSkillByName(db2, req.params.name);
|
|
23825
|
+
if (!skill) {
|
|
23826
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
23827
|
+
return;
|
|
23828
|
+
}
|
|
23829
|
+
if (!await canAccessSkill(db2, skill, "read", authResult.user)) {
|
|
23830
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
23831
|
+
return;
|
|
23832
|
+
}
|
|
23833
|
+
res.json({
|
|
23834
|
+
name: skill.name,
|
|
23835
|
+
description: skill.description ?? "",
|
|
23836
|
+
tags: Array.isArray(skill.tags) ? skill.tags : [],
|
|
23837
|
+
current_version: skill.current_version ?? 1,
|
|
23838
|
+
history: Array.isArray(skill.history) ? skill.history : []
|
|
23839
|
+
});
|
|
23840
|
+
});
|
|
23416
23841
|
app.post("/skills/:skillId/init", async (req, res) => {
|
|
23417
23842
|
const authResult = await requestValidators.authenticate(req);
|
|
23418
23843
|
if (!authResult.user?.id) {
|
|
@@ -23460,8 +23885,8 @@ ${style.markdown}` : params.prompt;
|
|
|
23460
23885
|
}
|
|
23461
23886
|
const { skillId } = req.params;
|
|
23462
23887
|
const { extension, contentType } = req.body ?? {};
|
|
23463
|
-
if (extension !== ".zip" && extension !== ".md") {
|
|
23464
|
-
res.status(400).json({ detail: 'extension must be ".zip" or ".
|
|
23888
|
+
if (extension !== ".zip" && extension !== ".md" && extension !== ".skill") {
|
|
23889
|
+
res.status(400).json({ detail: 'extension must be ".zip", ".md", or ".skill".' });
|
|
23465
23890
|
return;
|
|
23466
23891
|
}
|
|
23467
23892
|
if (!contentType || typeof contentType !== "string") {
|
|
@@ -23600,19 +24025,22 @@ ${style.markdown}` : params.prompt;
|
|
|
23600
24025
|
}
|
|
23601
24026
|
const versionPrefix = `skills/${skillId}/v${version}/`;
|
|
23602
24027
|
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
23603
|
-
const
|
|
24028
|
+
const asSkill = req.query.format === "skill";
|
|
24029
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
24030
|
+
const zip = new import_jszip3.default();
|
|
23604
24031
|
let fileCount = 0;
|
|
23605
24032
|
for (const file of files) {
|
|
23606
24033
|
const prefixIndex = file.key.indexOf(versionPrefix);
|
|
23607
24034
|
const relativePath = prefixIndex >= 0 ? file.key.slice(prefixIndex + versionPrefix.length) : file.key;
|
|
23608
24035
|
if (!relativePath) continue;
|
|
23609
24036
|
const bytes = await getS3ObjectBytes(file.key, config);
|
|
23610
|
-
|
|
24037
|
+
const archivePath = asSkill ? `${safeName}/${relativePath}` : relativePath;
|
|
24038
|
+
zip.file(archivePath, bytes);
|
|
23611
24039
|
fileCount += 1;
|
|
23612
24040
|
}
|
|
23613
24041
|
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23614
24042
|
zip.file(
|
|
23615
|
-
"version.txt",
|
|
24043
|
+
asSkill ? `${safeName}/version.txt` : "version.txt",
|
|
23616
24044
|
[
|
|
23617
24045
|
`Skill: ${skill.name ?? skillId}`,
|
|
23618
24046
|
`Skill id: ${skillId}`,
|
|
@@ -23623,13 +24051,9 @@ ${style.markdown}` : params.prompt;
|
|
|
23623
24051
|
].join("\n")
|
|
23624
24052
|
);
|
|
23625
24053
|
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
23626
|
-
const
|
|
23627
|
-
const filename = `${safeName}-v${version}.zip`;
|
|
24054
|
+
const filename = asSkill ? `${safeName}.skill` : `${safeName}-v${version}.zip`;
|
|
23628
24055
|
res.setHeader("Content-Type", "application/zip");
|
|
23629
|
-
res.setHeader(
|
|
23630
|
-
"Content-Disposition",
|
|
23631
|
-
`attachment; filename="${filename}"`
|
|
23632
|
-
);
|
|
24056
|
+
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
23633
24057
|
res.send(buffer);
|
|
23634
24058
|
});
|
|
23635
24059
|
app.post("/skills/:skillId/sign", async (req, res) => {
|
|
@@ -23929,6 +24353,12 @@ ${style.markdown}` : params.prompt;
|
|
|
23929
24353
|
const fullSessionPrefix = `${generalPrefix}${userSessionPrefix}`;
|
|
23930
24354
|
return { userSessionPrefix, fullSessionPrefix };
|
|
23931
24355
|
}
|
|
24356
|
+
const loadSessionFilesAuth = async (req, res, sessionId, rights) => {
|
|
24357
|
+
const authed = await loadAuthedSession(req, res, sessionId, rights);
|
|
24358
|
+
if (!authed) return null;
|
|
24359
|
+
const ownerId = authed.session.user ?? authed.user.id;
|
|
24360
|
+
return { ...authed, ownerId, ...buildSessionPrefixes(ownerId, sessionId) };
|
|
24361
|
+
};
|
|
23932
24362
|
function sanitizeFilename(name) {
|
|
23933
24363
|
const trimmed = name.trim();
|
|
23934
24364
|
if (!trimmed) return "";
|
|
@@ -23937,11 +24367,6 @@ ${style.markdown}` : params.prompt;
|
|
|
23937
24367
|
return trimmed.replace(/[\\/]/g, "_");
|
|
23938
24368
|
}
|
|
23939
24369
|
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
24370
|
const sessionId = req.params.sessionId;
|
|
23946
24371
|
if (!sessionId) {
|
|
23947
24372
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -23951,10 +24376,9 @@ ${style.markdown}` : params.prompt;
|
|
|
23951
24376
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
23952
24377
|
return;
|
|
23953
24378
|
}
|
|
23954
|
-
const
|
|
23955
|
-
|
|
23956
|
-
|
|
23957
|
-
);
|
|
24379
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
24380
|
+
if (!authed) return;
|
|
24381
|
+
const { userSessionPrefix } = authed;
|
|
23958
24382
|
let objects;
|
|
23959
24383
|
try {
|
|
23960
24384
|
objects = await listS3ObjectsByPrefix(userSessionPrefix, config);
|
|
@@ -23984,11 +24408,6 @@ ${style.markdown}` : params.prompt;
|
|
|
23984
24408
|
app.post(
|
|
23985
24409
|
"/sessions/:sessionId/files/upload-sign",
|
|
23986
24410
|
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
24411
|
const sessionId = req.params.sessionId;
|
|
23993
24412
|
if (!sessionId) {
|
|
23994
24413
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -23998,6 +24417,8 @@ ${style.markdown}` : params.prompt;
|
|
|
23998
24417
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
23999
24418
|
return;
|
|
24000
24419
|
}
|
|
24420
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
24421
|
+
if (!authed) return;
|
|
24001
24422
|
const { filename, contentType } = req.body ?? {};
|
|
24002
24423
|
if (!filename || typeof filename !== "string") {
|
|
24003
24424
|
res.status(400).json({ detail: "Missing filename in request body." });
|
|
@@ -24012,11 +24433,7 @@ ${style.markdown}` : params.prompt;
|
|
|
24012
24433
|
res.status(400).json({ detail: "Missing contentType in request body." });
|
|
24013
24434
|
return;
|
|
24014
24435
|
}
|
|
24015
|
-
const
|
|
24016
|
-
authResult.user.id,
|
|
24017
|
-
sessionId
|
|
24018
|
-
);
|
|
24019
|
-
const fullKey = `${fullSessionPrefix}${safeName}`;
|
|
24436
|
+
const fullKey = `${authed.fullSessionPrefix}${safeName}`;
|
|
24020
24437
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
24021
24438
|
res.json({ uploadUrl, key: fullKey });
|
|
24022
24439
|
}
|
|
@@ -24024,11 +24441,6 @@ ${style.markdown}` : params.prompt;
|
|
|
24024
24441
|
app.post(
|
|
24025
24442
|
"/sessions/:sessionId/files/sync-to-sandbox",
|
|
24026
24443
|
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
24444
|
const sessionId = req.params.sessionId;
|
|
24033
24445
|
if (!sessionId) {
|
|
24034
24446
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -24039,18 +24451,16 @@ ${style.markdown}` : params.prompt;
|
|
|
24039
24451
|
res.status(400).json({ detail: "Missing key in request body." });
|
|
24040
24452
|
return;
|
|
24041
24453
|
}
|
|
24042
|
-
const
|
|
24043
|
-
|
|
24044
|
-
|
|
24045
|
-
);
|
|
24046
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
24454
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
24455
|
+
if (!authed) return;
|
|
24456
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
24047
24457
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
24048
24458
|
return;
|
|
24049
24459
|
}
|
|
24050
24460
|
try {
|
|
24051
24461
|
const result = await downloadKeyIntoSandbox({
|
|
24052
24462
|
sessionId,
|
|
24053
|
-
userId:
|
|
24463
|
+
userId: authed.ownerId,
|
|
24054
24464
|
fullS3Key: key,
|
|
24055
24465
|
config
|
|
24056
24466
|
});
|
|
@@ -24064,11 +24474,6 @@ ${style.markdown}` : params.prompt;
|
|
|
24064
24474
|
app.delete(
|
|
24065
24475
|
"/sessions/:sessionId/files",
|
|
24066
24476
|
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
24477
|
const sessionId = req.params.sessionId;
|
|
24073
24478
|
if (!sessionId) {
|
|
24074
24479
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -24079,11 +24484,9 @@ ${style.markdown}` : params.prompt;
|
|
|
24079
24484
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
24080
24485
|
return;
|
|
24081
24486
|
}
|
|
24082
|
-
const
|
|
24083
|
-
|
|
24084
|
-
|
|
24085
|
-
);
|
|
24086
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
24487
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
24488
|
+
if (!authed) return;
|
|
24489
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
24087
24490
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
24088
24491
|
return;
|
|
24089
24492
|
}
|
|
@@ -24102,11 +24505,6 @@ ${style.markdown}` : params.prompt;
|
|
|
24102
24505
|
if (!req.headers.authorization && typeof req.query.auth === "string") {
|
|
24103
24506
|
req.headers.authorization = `Bearer ${req.query.auth}`;
|
|
24104
24507
|
}
|
|
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
24508
|
const sessionId = req.params.sessionId;
|
|
24111
24509
|
if (!sessionId) {
|
|
24112
24510
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -24117,11 +24515,9 @@ ${style.markdown}` : params.prompt;
|
|
|
24117
24515
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
24118
24516
|
return;
|
|
24119
24517
|
}
|
|
24120
|
-
const
|
|
24121
|
-
|
|
24122
|
-
|
|
24123
|
-
);
|
|
24124
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
24518
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
24519
|
+
if (!authed) return;
|
|
24520
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
24125
24521
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
24126
24522
|
return;
|
|
24127
24523
|
}
|
|
@@ -24225,7 +24621,7 @@ ${style.markdown}` : params.prompt;
|
|
|
24225
24621
|
` - tracking.json tracking events linked to the user`,
|
|
24226
24622
|
``
|
|
24227
24623
|
].join("\n");
|
|
24228
|
-
const zip = new
|
|
24624
|
+
const zip = new import_jszip3.default();
|
|
24229
24625
|
zip.file("README.txt", readme);
|
|
24230
24626
|
zip.file("user_data.json", JSON.stringify(userExport, null, 2));
|
|
24231
24627
|
zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
|