@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.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
mapStreamErrorMessage,
|
|
55
55
|
oauthRegistry,
|
|
56
56
|
oauthTokenStore,
|
|
57
|
+
parseResetAt,
|
|
57
58
|
postgresClient,
|
|
58
59
|
provisionDefaultUserBudget,
|
|
59
60
|
reportSystemDependencies,
|
|
@@ -73,7 +74,7 @@ import {
|
|
|
73
74
|
upsertBudget,
|
|
74
75
|
waitForLiteLLMReady,
|
|
75
76
|
withRetry
|
|
76
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-Y7JPNBFM.js";
|
|
77
78
|
import {
|
|
78
79
|
findLiteLLMModel
|
|
79
80
|
} from "./chunk-7CCMW3IW.js";
|
|
@@ -11134,21 +11135,7 @@ function isOsJunkPath(path2) {
|
|
|
11134
11135
|
const basename = path2.split("/").pop() ?? "";
|
|
11135
11136
|
return basename === ".DS_Store" || basename === "Thumbs.db" || basename === "desktop.ini";
|
|
11136
11137
|
}
|
|
11137
|
-
async function
|
|
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
|
-
}
|
|
11138
|
+
async function extractZipToPrefix(bytes, prefix, config) {
|
|
11152
11139
|
let zip;
|
|
11153
11140
|
try {
|
|
11154
11141
|
zip = await JSZip.loadAsync(bytes);
|
|
@@ -11212,7 +11199,7 @@ async function extractBundleToS3(opts) {
|
|
|
11212
11199
|
}
|
|
11213
11200
|
let filesCount = 0;
|
|
11214
11201
|
for (const { relPath, content } of prepared) {
|
|
11215
|
-
const s3Key =
|
|
11202
|
+
const s3Key = `${prefix}${relPath}`;
|
|
11216
11203
|
await uploadFile(
|
|
11217
11204
|
content,
|
|
11218
11205
|
s3Key,
|
|
@@ -11221,12 +11208,76 @@ async function extractBundleToS3(opts) {
|
|
|
11221
11208
|
void 0,
|
|
11222
11209
|
void 0,
|
|
11223
11210
|
true
|
|
11224
|
-
// global=true —
|
|
11211
|
+
// global=true — skill files are shared across users
|
|
11225
11212
|
);
|
|
11226
11213
|
filesCount += 1;
|
|
11227
11214
|
}
|
|
11228
11215
|
return { filesCount };
|
|
11229
11216
|
}
|
|
11217
|
+
async function extractBundleToS3(opts) {
|
|
11218
|
+
const { bytes, skillId, isZip, config } = opts;
|
|
11219
|
+
if (!isZip) {
|
|
11220
|
+
await uploadFile(
|
|
11221
|
+
bytes,
|
|
11222
|
+
`skills/${skillId}/v1/SKILL.md`,
|
|
11223
|
+
config,
|
|
11224
|
+
{ contentType: "text/markdown" },
|
|
11225
|
+
void 0,
|
|
11226
|
+
void 0,
|
|
11227
|
+
true
|
|
11228
|
+
// global=true so the key isn't user-prefixed (skill files are shared)
|
|
11229
|
+
);
|
|
11230
|
+
return { filesCount: 1 };
|
|
11231
|
+
}
|
|
11232
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v1/`, config);
|
|
11233
|
+
}
|
|
11234
|
+
async function extractBundleToVersion(opts) {
|
|
11235
|
+
const { bytes, skillId, version, config } = opts;
|
|
11236
|
+
return extractZipToPrefix(bytes, `skills/${skillId}/v${version}/`, config);
|
|
11237
|
+
}
|
|
11238
|
+
|
|
11239
|
+
// src/skills/frontmatter.ts
|
|
11240
|
+
import JSZip2 from "jszip";
|
|
11241
|
+
function parseFrontmatter(md) {
|
|
11242
|
+
const match = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
11243
|
+
if (!match) return {};
|
|
11244
|
+
const block = match[1];
|
|
11245
|
+
const out = {};
|
|
11246
|
+
for (const line of block.split(/\r?\n/)) {
|
|
11247
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
11248
|
+
if (!m) continue;
|
|
11249
|
+
const key = m[1];
|
|
11250
|
+
let v = m[2].trim();
|
|
11251
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
11252
|
+
v = v.slice(1, -1);
|
|
11253
|
+
}
|
|
11254
|
+
out[key] = v;
|
|
11255
|
+
}
|
|
11256
|
+
return out;
|
|
11257
|
+
}
|
|
11258
|
+
async function parseSkillFrontmatter(zipBytes) {
|
|
11259
|
+
let zip;
|
|
11260
|
+
try {
|
|
11261
|
+
zip = await JSZip2.loadAsync(zipBytes);
|
|
11262
|
+
} catch {
|
|
11263
|
+
return {};
|
|
11264
|
+
}
|
|
11265
|
+
const paths = [];
|
|
11266
|
+
zip.forEach((p, entry) => {
|
|
11267
|
+
if (!entry.dir) paths.push(p);
|
|
11268
|
+
});
|
|
11269
|
+
const heads = new Set(paths.map((p) => p.split("/")[0]).filter(Boolean));
|
|
11270
|
+
let strip = (p) => p;
|
|
11271
|
+
if (heads.size === 1) {
|
|
11272
|
+
const head = [...heads][0] + "/";
|
|
11273
|
+
if (paths.every((p) => p.startsWith(head))) strip = (p) => p.slice(head.length);
|
|
11274
|
+
}
|
|
11275
|
+
const skillPath = paths.find((p) => strip(p) === "SKILL.md");
|
|
11276
|
+
if (!skillPath) return {};
|
|
11277
|
+
const md = await zip.file(skillPath).async("string");
|
|
11278
|
+
const fm = parseFrontmatter(md);
|
|
11279
|
+
return { name: fm.name, description: fm.description };
|
|
11280
|
+
}
|
|
11230
11281
|
|
|
11231
11282
|
// src/sessions/pdf-preview-cache.ts
|
|
11232
11283
|
import { exec } from "child_process";
|
|
@@ -11306,7 +11357,7 @@ import OpenAI from "openai";
|
|
|
11306
11357
|
import fs2 from "fs";
|
|
11307
11358
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
11308
11359
|
import "@opentelemetry/api";
|
|
11309
|
-
import
|
|
11360
|
+
import JSZip3 from "jszip";
|
|
11310
11361
|
import { createIdGenerator } from "ai";
|
|
11311
11362
|
import cookieParser from "cookie-parser";
|
|
11312
11363
|
|
|
@@ -13763,6 +13814,148 @@ var contentHeadersFor = (key, contentType, filename) => {
|
|
|
13763
13814
|
};
|
|
13764
13815
|
var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
|
|
13765
13816
|
|
|
13817
|
+
// src/skills/skill-access.ts
|
|
13818
|
+
async function resolveSkillByName(db, name) {
|
|
13819
|
+
const row = await db("skills").where({ name }).first();
|
|
13820
|
+
return row ?? null;
|
|
13821
|
+
}
|
|
13822
|
+
async function canAccessSkill(db, skill, action, user) {
|
|
13823
|
+
const rbac = await RBACResolver(db, "skill", skill.id, skill.rights_mode || "private");
|
|
13824
|
+
return checkRecordAccess({ ...skill, RBAC: rbac }, action, user);
|
|
13825
|
+
}
|
|
13826
|
+
async function filterReadableSkills(db, skills, user) {
|
|
13827
|
+
const out = [];
|
|
13828
|
+
for (const s of skills) {
|
|
13829
|
+
if (await canAccessSkill(db, s, "read", user)) out.push(s);
|
|
13830
|
+
}
|
|
13831
|
+
return out;
|
|
13832
|
+
}
|
|
13833
|
+
|
|
13834
|
+
// src/skills/bootstrap/clients.ts
|
|
13835
|
+
var CLIENT_MANIFEST = [
|
|
13836
|
+
{ id: "agents", dir: ".agents/skills" },
|
|
13837
|
+
// cross-agent standard (symlink canonical store)
|
|
13838
|
+
{ id: "claude", dir: ".claude/skills" },
|
|
13839
|
+
{ id: "windsurf", dir: ".windsurf/skills" },
|
|
13840
|
+
{ id: "continue", dir: ".continue/skills" },
|
|
13841
|
+
{ id: "roo", dir: ".roo/skills" },
|
|
13842
|
+
{ id: "kilocode", dir: ".kilocode/skills" },
|
|
13843
|
+
{ id: "crush", dir: ".crush/skills" },
|
|
13844
|
+
{ id: "goose", dir: ".goose/skills" },
|
|
13845
|
+
{ id: "qwen", dir: ".qwen/skills" },
|
|
13846
|
+
{ id: "iflow", dir: ".iflow/skills" },
|
|
13847
|
+
{ id: "junie", dir: ".junie/skills" },
|
|
13848
|
+
{ id: "kiro", dir: ".kiro/skills" },
|
|
13849
|
+
{ id: "trae", dir: ".trae/skills" },
|
|
13850
|
+
{ id: "augment", dir: ".augment/skills" },
|
|
13851
|
+
{ id: "factory", dir: ".factory/skills" },
|
|
13852
|
+
{ id: "devin", dir: ".devin/skills" },
|
|
13853
|
+
{ id: "openhands", dir: ".openhands/skills" },
|
|
13854
|
+
{ id: "pi", dir: ".pi/skills" },
|
|
13855
|
+
{ id: "cortex", dir: ".cortex/skills" },
|
|
13856
|
+
{ id: "zencoder", dir: ".zencoder/skills" },
|
|
13857
|
+
{ id: "codebuddy", dir: ".codebuddy/skills" },
|
|
13858
|
+
{ id: "codestudio", dir: ".codestudio/skills" },
|
|
13859
|
+
{ id: "commandcode", dir: ".commandcode/skills" },
|
|
13860
|
+
{ id: "codemaker", dir: ".codemaker/skills" },
|
|
13861
|
+
{ id: "codeartsdoer", dir: ".codeartsdoer/skills" },
|
|
13862
|
+
{ id: "lingma", dir: ".lingma/skills" },
|
|
13863
|
+
{ id: "qoder", dir: ".qoder/skills" },
|
|
13864
|
+
{ id: "rovodev", dir: ".rovodev/skills" },
|
|
13865
|
+
{ id: "moxby", dir: ".moxby/skills" },
|
|
13866
|
+
{ id: "mux", dir: ".mux/skills" },
|
|
13867
|
+
{ id: "neovate", dir: ".neovate/skills" },
|
|
13868
|
+
{ id: "ona", dir: ".ona/skills" },
|
|
13869
|
+
{ id: "pochi", dir: ".pochi/skills" },
|
|
13870
|
+
{ id: "reasonix", dir: ".reasonix/skills" },
|
|
13871
|
+
{ id: "terramind", dir: ".terramind/skills" },
|
|
13872
|
+
{ id: "tinycloud", dir: ".tinycloud/skills" },
|
|
13873
|
+
{ id: "vibe", dir: ".vibe/skills" },
|
|
13874
|
+
{ id: "adal", dir: ".adal/skills" },
|
|
13875
|
+
{ id: "aider-desk", dir: ".aider-desk/skills" },
|
|
13876
|
+
{ id: "autohand", dir: ".autohand/skills" },
|
|
13877
|
+
{ id: "bob", dir: ".bob/skills" },
|
|
13878
|
+
{ id: "hermes", dir: ".hermes/skills" },
|
|
13879
|
+
{ id: "inferencesh", dir: ".inferencesh/skills" },
|
|
13880
|
+
{ id: "jazz", dir: ".jazz/skills" },
|
|
13881
|
+
{ id: "kode", dir: ".kode/skills" },
|
|
13882
|
+
{ id: "mcpjam", dir: ".mcpjam/skills" },
|
|
13883
|
+
{ id: "forge", dir: ".forge/skills" },
|
|
13884
|
+
{ id: "tabnine", dir: ".tabnine/agent/skills" }
|
|
13885
|
+
// exception: nested under agent/
|
|
13886
|
+
];
|
|
13887
|
+
|
|
13888
|
+
// src/skills/bootstrap/exulu-sh.generated.ts
|
|
13889
|
+
var EXULU_SH_B64 = "IyEvYmluL3NoCiMgZXh1bHUg4oCUIGhlbHBlciBmb3IgdGhlIEV4dWx1IGNlbnRyYWwgc2tpbGwgbGlicmFyeS4gVGhlIGFnZW50IGludm9rZXMgdGhpcwojIChuZXZlciByYXcgY3VybCk6IHRoZSB0b2tlbiBpcyByZWFkIGZyb20gdGhlIGNvbmZpZyBmaWxlIGhlcmUgYW5kIHNlbnQgdmlhCiMgYHgtYXBpLWtleTogQmVhcmVyYCwgc28gaXQgbmV2ZXIgZW50ZXJzIHRoZSBtb2RlbCBjb250ZXh0LiBDbGllbnQgZmFuLW91dAojIChjb3B5L3N5bWxpbmsgYWNyb3NzIGFnZW50IGNsaWVudHMpIGlzIGRldGVybWluaXN0aWMuCnNldCAtZXUKCkNPTkZJR19ESVI9IiRIT01FLy5jb25maWcvZXh1bHUiCkNPTkZJR19GSUxFPSIkQ09ORklHX0RJUi9za2lsbHMuanNvbiIKCmRpZSgpIHsgcHJpbnRmICdleHVsdTogJXNcbicgIiQqIiA+JjI7IGV4aXQgMTsgfQppbmZvKCkgeyBwcmludGYgJyVzXG4nICIkKiIgPiYyOyB9Cgpqc29uX3N0cigpIHsgIyBqc29uX3N0ciA8a2V5PiA8ZmlsZT4g4oCUIGZsYXQgImtleSI6InZhbHVlIgogIHNlZCAtbiAicy8uKlwiJDFcIltbOnNwYWNlOl1dKjpbWzpzcGFjZTpdXSpcIlxcKFteXCJdKlxcKVwiLiovXFwxL3AiICIkMiIgfCBoZWFkIC1uMQp9CgpbIC1mICIkQ09ORklHX0ZJTEUiIF0gfHwgZGllICJub3QgY29uZmlndXJlZCDigJQgcnVuOiBjdXJsIC1mc1NMIDxiYXNlX3VybD4vYXBpL3NraWxscy9pbnN0YWxsLnNoIHwgc2giCgpCQVNFX1VSTD0iJChqc29uX3N0ciBiYXNlX3VybCAiJENPTkZJR19GSUxFIikiCkJBQ0tFTkQ9IiQoanNvbl9zdHIgYmFja2VuZCAiJENPTkZJR19GSUxFIikiCkFQSV9LRVk9IiQoanNvbl9zdHIgYXBpX2tleSAiJENPTkZJR19GSUxFIikiCkxJTktfTU9ERT0iJChqc29uX3N0ciBsaW5rX21vZGUgIiRDT05GSUdfRklMRSIpIgpTQ09QRT0iJChqc29uX3N0ciBzY29wZSAiJENPTkZJR19GSUxFIikiCkNMSUVOVFM9IiQoc2VkIC1uICdzLy4qImNsaWVudHMiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKlxbXChbXl1dKlwpXF0uKi9cMS9wJyAiJENPTkZJR19GSUxFIiB8IHRyICcsJyAnICcgfCB0ciAtZCAnIicgfCB0ciAtcyAnICcpIgoKWyAtbiAiJENMSUVOVFMiIF0gfHwgQ0xJRU5UUz0iYWdlbnRzIgpbIC1uICIkTElOS19NT0RFIiBdIHx8IExJTktfTU9ERT0iY29weSIKWyAtbiAiJFNDT1BFIiBdIHx8IFNDT1BFPSJwcm9qZWN0IgoKaWYgWyAteiAiJEJBQ0tFTkQiIF07IHRoZW4KICBbIC1uICIkQkFTRV9VUkwiIF0gfHwgZGllICJjb25maWcgaGFzIG5vIGJhY2tlbmQgYW5kIG5vIGJhc2VfdXJsIgogIEJBQ0tFTkQ9IiQoY3VybCAtZnNTTCAiJEJBU0VfVVJML2FwaS9jb25maWciIHwgc2VkIC1uICdzLy4qImJhY2tlbmQiW1s6c3BhY2U6XV0qOltbOnNwYWNlOl1dKiJcKFteIl0qXCkiLiovXDEvcCcgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJEJBQ0tFTkQiIF0gfHwgZGllICJjb3VsZCBub3QgcmVzb2x2ZSBiYWNrZW5kIGZyb20gJEJBU0VfVVJML2FwaS9jb25maWciCmZpCkJBQ0tFTkQ9IiQocHJpbnRmICclcycgIiRCQUNLRU5EIiB8IHNlZCAnczovKiQ6OicpIgoKUk9PVD0iJFBXRCIKWyAiJFNDT1BFIiA9ICJob21lIiBdICYmIFJPT1Q9IiRIT01FIgoKIyBkaXJfZm9yIENMSUVOVF9JRCAtPiByZWxhdGl2ZSBza2lsbCBkaXIgKGdlbmVyYXRlZCBmcm9tIHRoZSBtYW5pZmVzdCBpbiB0aGUKIyByZWFsIGJ1aWxkOyBhIHJlcHJlc2VudGF0aXZlIHN1YnNldCBoZXJlIGZvciBsb2NhbCB0ZXN0aW5nKS4KZGlyX2ZvcigpIHsKICBjYXNlICIkMSIgaW4KX19ESVJfRk9SX0NBU0VTX18KICAgICopIHJldHVybiAxIDs7CiAgZXNhYwp9CgphcGkoKSB7ICMgYXBpIDxNRVRIT0Q+IDxwYXRoPiBbZXh0cmEgY3VybCBhcmdzLi4uXSAtPiBib2R5IG9uIHN0ZG91dAogIG09IiQxIjsgcD0iJDIiOyBzaGlmdCAyCiAgY3VybCAtZnNTIC1YICIkbSIgIiRCQUNLRU5EJHAiIC1IICJ4LWFwaS1rZXk6IEJlYXJlciAkQVBJX0tFWSIgIiRAIgp9CgptZXRhX3ZlcnNpb24oKSB7ICMgbWV0YV92ZXJzaW9uIDxuYW1lPiAtPiBjdXJyZW50X3ZlcnNpb24gZnJvbSByZWdpc3RyeQogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJDEiIDI+L2Rldi9udWxsIFwKICAgIHwgc2VkIC1uICdzLy4qImN1cnJlbnRfdmVyc2lvbiJbWzpzcGFjZTpdXSo6W1s6c3BhY2U6XV0qXChbMC05XVswLTldKlwpLiovXDEvcCcgfCBoZWFkIC1uMQp9CgpfbWFuYWdlZCgpIHsgIyBfbWFuYWdlZCA8ZGlyPiAtPiAwIGlmIHNhZmUgdG8gb3ZlcndyaXRlIChvdXJzIG9yIHN5bWxpbmsgb3IgYWJzZW50KQogIGQ9IiQxIgogIGlmIFsgLWUgIiRkIiBdICYmIFsgISAtTCAiJGQiIF0gJiYgWyAhIC1mICIkZC8uZXh1bHUtc2tpbGwuanNvbiIgXTsgdGhlbgogICAgaW5mbyAic2tpcCAkZCAoZXhpc3RzLCBub3QgbWFuYWdlZCBieSBleHVsdSkiOyByZXR1cm4gMQogIGZpCiAgcmV0dXJuIDAKfQoKX3B1dF9yZWFsKCkgeyAjIF9wdXRfcmVhbCA8ZGVzdD4gPHNyY2Rpcj4gPG1hcmtlci1qc29uPgogIF9tYW5hZ2VkICIkMSIgfHwgcmV0dXJuIDAKICBybSAtcmYgIiQxIjsgbWtkaXIgLXAgIiQxIjsgY3AgLVIgIiQyLy4iICIkMS8iCiAgcHJpbnRmICclc1xuJyAiJDMiID4gIiQxLy5leHVsdS1za2lsbC5qc29uIgp9CgpwbGFjZV9za2lsbCgpIHsgIyBwbGFjZV9za2lsbCA8bmFtZT4gPHNyY2Rpcj4gPHZlcnNpb24+CiAgcG5hbWU9IiQxIjsgcHNyYz0iJDIiOyBwdmVyPSIkezM6LTF9IgogIG1hcmtlcj0neyAibmFtZSI6ICInIiRwbmFtZSInIiwgInZlcnNpb24iOiAnIiRwdmVyIicsICJzb3VyY2UiOiAiJyIkQkFDS0VORCInIiB9JwogIGNhbm9uPSIkUk9PVC8uYWdlbnRzL3NraWxscy8kcG5hbWUiCiAgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBfcHV0X3JlYWwgIiRjYW5vbiIgIiRwc3JjIiAiJG1hcmtlciIKICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICBkPSIkKGRpcl9mb3IgIiRpZCIpIiB8fCB7IGluZm8gInVua25vd24gY2xpZW50OiAkaWQiOyBjb250aW51ZTsgfQogICAgcGFyZW50PSIkUk9PVC8kZCI7IGRlc3Q9IiRwYXJlbnQvJHBuYW1lIgogICAgaWYgWyAiJExJTktfTU9ERSIgPSAic3ltbGluayIgXSAmJiBbICIkaWQiICE9ICJhZ2VudHMiIF07IHRoZW4KICAgICAgX21hbmFnZWQgIiRkZXN0IiB8fCBjb250aW51ZQogICAgICBta2RpciAtcCAiJHBhcmVudCI7IHJtIC1yZiAiJGRlc3QiCiAgICAgIGlmIGxuIC1zICIkY2Fub24iICIkZGVzdCIgMj4vZGV2L251bGw7IHRoZW4KICAgICAgICBpbmZvICJsaW5rZWQgJGRlc3QgLT4gJGNhbm9uIgogICAgICBlbHNlCiAgICAgICAgaW5mbyAic3ltbGluayB1bnN1cHBvcnRlZCBhdCAkZGVzdDsgY29weWluZyIKICAgICAgICBfcHV0X3JlYWwgIiRkZXN0IiAiJHBzcmMiICIkbWFya2VyIgogICAgICBmaQogICAgZWxpZiBbICIkTElOS19NT0RFIiA9ICJjb3B5IiBdOyB0aGVuCiAgICAgIF9wdXRfcmVhbCAiJGRlc3QiICIkcHNyYyIgIiRtYXJrZXIiCiAgICBmaQogICAgIyBzeW1saW5rICsgYWdlbnRzOiBhbHJlYWR5IHBsYWNlZCBhcyB0aGUgY2Fub25pY2FsIHN0b3JlIGFib3ZlLgogIGRvbmUKfQoKaW5zdGFsbGVkX25hbWVzKCkgeyAjIHVuaXF1ZSBza2lsbCBuYW1lcyB0aGF0IGNhcnJ5IG91ciBtYXJrZXIgdW5kZXIgUk9PVAogIHsgZmluZCAiJFJPT1QvLmFnZW50cy9za2lsbHMiIC1tYXhkZXB0aCAyIC1uYW1lIC5leHVsdS1za2lsbC5qc29uIDI+L2Rldi9udWxsCiAgICBmb3IgaWQgaW4gJENMSUVOVFM7IGRvCiAgICAgIGQ9IiQoZGlyX2ZvciAiJGlkIikiIHx8IGNvbnRpbnVlCiAgICAgIGZpbmQgIiRST09ULyRkIiAtbWF4ZGVwdGggMiAtbmFtZSAuZXh1bHUtc2tpbGwuanNvbiAyPi9kZXYvbnVsbAogICAgZG9uZQogIH0gfCB3aGlsZSByZWFkIC1yIG07IGRvIGpzb25fc3RyIG5hbWUgIiRtIjsgZG9uZSB8IHNvcnQgLXUKfQoKbWFya2VyX3ZlcnNpb24oKSB7ICMgbWFya2VyX3ZlcnNpb24gPG5hbWU+CiAgZm9yIGJhc2UgaW4gIiRST09ULy5hZ2VudHMvc2tpbGxzIjsgZG8KICAgIFsgLWYgIiRiYXNlLyQxLy5leHVsdS1za2lsbC5qc29uIiBdICYmIHsganNvbl9zdHIgdmVyc2lvbiAiJGJhc2UvJDEvLmV4dWx1LXNraWxsLmpzb24iOyByZXR1cm47IH0KICBkb25lCiAgZm9yIGlkIGluICRDTElFTlRTOyBkbwogICAgZD0iJChkaXJfZm9yICIkaWQiKSIgfHwgY29udGludWUKICAgIGY9IiRST09ULyRkLyQxLy5leHVsdS1za2lsbC5qc29uIgogICAgWyAtZiAiJGYiIF0gJiYgeyBqc29uX3N0ciB2ZXJzaW9uICIkZiI7IHJldHVybjsgfQogIGRvbmUKfQoKZG9faW5zdGFsbCgpIHsgIyBkb19pbnN0YWxsIDxuYW1lPgogIG5hbWU9IiQxIgogIFRNUD0iJChta3RlbXAgLWQpIgogIGFwaSBHRVQgIi9za2lsbHMvcmVnaXN0cnkvJG5hbWUvZG93bmxvYWQiIC0tb3V0cHV0ICIkVE1QL3NraWxsLnppcCIgXAogICAgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImRvd25sb2FkIGZhaWxlZCBmb3IgJyRuYW1lJyAoNDAzID0gbm8gYWNjZXNzLCA0MDQgPSB1bmtub3duKSI7IH0KICBta2RpciAtcCAiJFRNUC94IgogIHVuemlwIC1xICIkVE1QL3NraWxsLnppcCIgLWQgIiRUTVAveCIgfHwgeyBybSAtcmYgIiRUTVAiOyBkaWUgImJhZCBhcmNoaXZlIGZvciAnJG5hbWUnIjsgfQogIHNyYz0iJChmaW5kICIkVE1QL3giIC1taW5kZXB0aCAxIC1tYXhkZXB0aCAxIC10eXBlIGQgfCBoZWFkIC1uMSkiCiAgWyAtbiAiJHNyYyIgXSB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAidW5leHBlY3RlZCBhcmNoaXZlIGxheW91dCBmb3IgJyRuYW1lJyI7IH0KICB2ZXI9IiQobWV0YV92ZXJzaW9uICIkbmFtZSIpIgogIHBsYWNlX3NraWxsICIkbmFtZSIgIiRzcmMiICIke3ZlcjotMX0iCiAgcm0gLXJmICIkVE1QIgogIGluZm8gImluc3RhbGxlZCAkbmFtZSAodiR7dmVyOi0xfSkgWyRMSU5LX01PREVdIGludG86ICRDTElFTlRTIgp9Cgp1c2FnZSgpIHsKICBjYXQgPiYyIDw8RU9GCmV4dWx1IOKAlCBFeHVsdSBza2lsbCBsaWJyYXJ5IGhlbHBlcgogIGV4dWx1IGxpc3QgICAgICAgICAgICAgICAgIGxpc3Qgc2tpbGxzIHlvdSBjYW4gYWNjZXNzIChKU09OKQogIGV4dWx1IGdldCA8bmFtZT4gICAgICAgICAgIHNob3cgb25lIHNraWxsJ3MgbWV0YWRhdGEgKEpTT04pCiAgZXh1bHUgaW5zdGFsbCA8bmFtZT4gICAgICAgaW5zdGFsbC9yZWZyZXNoIGEgc2tpbGwgaW50byB5b3VyIGFnZW50IGNsaWVudHMKICBleHVsdSB1cGRhdGUgWzxuYW1lPl0gICAgICB1cGRhdGUgaW5zdGFsbGVkIHNraWxscyAoYWxsLCBvciBvbmUpIHRvIGxhdGVzdAogIGV4dWx1IHB1Ymxpc2ggPG5hbWU+IDxkaXI+IHB1Ymxpc2ggYSBsb2NhbCBza2lsbCBmb2xkZXIgYXMgPG5hbWU+CiAgZXh1bHUgY29uZmlnICAgICAgICAgICAgICAgc2hvdyByZXNvbHZlZCBiYWNrZW5kIC8gc2NvcGUgLyBjbGllbnRzIChubyBzZWNyZXRzKQpFT0YKfQoKY21kPSIkezE6LWhlbHB9IgpbICQjIC1ndCAwIF0gJiYgc2hpZnQgfHwgdHJ1ZQoKY2FzZSAiJGNtZCIgaW4KICBsaXN0KSBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5IiA7OwogIGdldCkgWyAkIyAtZ2UgMSBdIHx8IGRpZSAidXNhZ2U6IGV4dWx1IGdldCA8bmFtZT4iOyBhcGkgR0VUICIvc2tpbGxzL3JlZ2lzdHJ5LyQxIiA7OwogIGluc3RhbGwpIFsgJCMgLWdlIDEgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBpbnN0YWxsIDxuYW1lPiI7IGRvX2luc3RhbGwgIiQxIiA7OwogIHVwZGF0ZSkKICAgIGlmIFsgJCMgLWdlIDEgXTsgdGhlbiBuYW1lcz0iJDEiOyBlbHNlIG5hbWVzPSIkKGluc3RhbGxlZF9uYW1lcykiOyBmaQogICAgWyAtbiAiJG5hbWVzIiBdIHx8IHsgaW5mbyAibm8gZXh1bHUtbWFuYWdlZCBza2lsbHMgZm91bmQgdW5kZXIgJFJPT1QiOyBleGl0IDA7IH0KICAgIGZvciBuIGluICRuYW1lczsgZG8KICAgICAgY3VyPSIkKG1hcmtlcl92ZXJzaW9uICIkbiIpIgogICAgICBsYXRlc3Q9IiQobWV0YV92ZXJzaW9uICIkbiIpIgogICAgICBbIC1uICIkbGF0ZXN0IiBdIHx8IHsgaW5mbyAic2tpcCAkbiAobm90IGluIHJlZ2lzdHJ5KSI7IGNvbnRpbnVlOyB9CiAgICAgIGlmIFsgLXogIiRjdXIiIF0gfHwgWyAiJGxhdGVzdCIgLWd0ICIkY3VyIiBdIDI+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgaW5mbyAidXBkYXRpbmcgJG46IHYke2N1cjotP30gLT4gdiRsYXRlc3QiOyBkb19pbnN0YWxsICIkbiIKICAgICAgZWxzZQogICAgICAgIGluZm8gIiRuIHVwIHRvIGRhdGUgKHYkY3VyKSIKICAgICAgZmkKICAgIGRvbmUKICAgIDs7CiAgcHVibGlzaCkKICAgIFsgJCMgLWdlIDIgXSB8fCBkaWUgInVzYWdlOiBleHVsdSBwdWJsaXNoIDxuYW1lPiA8ZGlyPiIKICAgIG5hbWU9IiQxIjsgZm9sZGVyPSIkMiIKICAgIFsgLWQgIiRmb2xkZXIiIF0gfHwgZGllICJubyBzdWNoIGZvbGRlcjogJGZvbGRlciIKICAgIFsgLWYgIiRmb2xkZXIvU0tJTEwubWQiIF0gfHwgZGllICIkZm9sZGVyIGhhcyBubyBTS0lMTC5tZCBhdCBpdHMgcm9vdCIKICAgIGNvbW1hbmQgLXYgemlwID4vZGV2L251bGwgMj4mMSB8fCBkaWUgInRoZSAnemlwJyBjb21tYW5kIGlzIHJlcXVpcmVkIHRvIHB1Ymxpc2giCiAgICBUTVA9IiQobWt0ZW1wIC1kKSIKICAgICggY2QgIiQoZGlybmFtZSAiJGZvbGRlciIpIiBcCiAgICAgICYmIHppcCAtcSAtciAtWCAiJFRNUC9za2lsbC56aXAiICIkKGJhc2VuYW1lICIkZm9sZGVyIikiIFwKICAgICAgICAgICAteCAnKi8uZXh1bHUtc2tpbGwuanNvbicgJyovLmdpdC8qJyAnKi5EU19TdG9yZScgJyovX19NQUNPU1gvKicgKSBcCiAgICAgIHx8IHsgcm0gLXJmICIkVE1QIjsgZGllICJjb3VsZCBub3QgemlwICRmb2xkZXIiOyB9CiAgICBhcGkgUE9TVCAiL3NraWxscy9yZWdpc3RyeS8kbmFtZSIgLUggIkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vemlwIiBcCiAgICAgIC0tZGF0YS1iaW5hcnkgQCIkVE1QL3NraWxsLnppcCIgXAogICAgICB8fCB7IHJtIC1yZiAiJFRNUCI7IGRpZSAicHVibGlzaCBmYWlsZWQgKDQwMyA9IG5vIHdyaXRlIGFjY2VzcywgNDA5ID0gbmFtZSB0YWtlbikiOyB9CiAgICBybSAtcmYgIiRUTVAiCiAgICBpbmZvICJwdWJsaXNoZWQgJG5hbWUiCiAgICA7OwogIGNvbmZpZykKICAgIHByaW50ZiAnYmFja2VuZD0lc1xuc2NvcGU9JXNcbmxpbmtfbW9kZT0lc1xuY2xpZW50cz0lc1xuYXBpX2tleT0lc1xuJyBcCiAgICAgICIkQkFDS0VORCIgIiRTQ09QRSIgIiRMSU5LX01PREUiICIkQ0xJRU5UUyIgXAogICAgICAiJChbIC1uICIkQVBJX0tFWSIgXSAmJiBlY2hvIHNldCB8fCBlY2hvIE1JU1NJTkcpIgogICAgOzsKICBoZWxwfC0taGVscHwtaCkgdXNhZ2UgOzsKICAqKSBpbmZvICJ1bmtub3duIGNvbW1hbmQ6ICRjbWQiOyB1c2FnZTsgZXhpdCAyIDs7CmVzYWMK";
|
|
13890
|
+
|
|
13891
|
+
// src/skills/bootstrap/exulu-skills.ts
|
|
13892
|
+
var BOOTSTRAP_CLIENTS_JSON = JSON.stringify(CLIENT_MANIFEST, null, 2);
|
|
13893
|
+
var DIR_FOR_CASES = CLIENT_MANIFEST.map(
|
|
13894
|
+
(c) => ` ${c.id}) printf '%s' '${c.dir}' ;;`
|
|
13895
|
+
).join("\n");
|
|
13896
|
+
var BOOTSTRAP_EXULU_SH = Buffer.from(EXULU_SH_B64, "base64").toString("utf8").replace("__DIR_FOR_CASES__", DIR_FOR_CASES);
|
|
13897
|
+
var BOOTSTRAP_SKILL_MD = `---
|
|
13898
|
+
name: exulu-skills
|
|
13899
|
+
description: Install, update, and publish skills from this Exulu instance's central skill library. Use when the user asks to install a skill, get the latest version of a skill, list available skills, or publish a skill to Exulu.
|
|
13900
|
+
---
|
|
13901
|
+
|
|
13902
|
+
# Exulu Skills
|
|
13903
|
+
|
|
13904
|
+
Bridge to the Exulu central skill library. **All operations go through the
|
|
13905
|
+
bundled helper script \u2014 do not hand-write curl or copy files yourself.** The
|
|
13906
|
+
script reads the API token from config (keeping it out of this conversation) and
|
|
13907
|
+
handles the multi-client copy/symlink fan-out deterministically.
|
|
13908
|
+
|
|
13909
|
+
## The helper
|
|
13910
|
+
|
|
13911
|
+
Run the script next to this file, \`scripts/exulu\`, with \`sh\` and the absolute
|
|
13912
|
+
path of this skill's directory:
|
|
13913
|
+
|
|
13914
|
+
\`\`\`
|
|
13915
|
+
sh "<this-skill-dir>/scripts/exulu" <command>
|
|
13916
|
+
\`\`\`
|
|
13917
|
+
|
|
13918
|
+
Commands:
|
|
13919
|
+
- \`list\` \u2014 skills you can access (JSON on stdout)
|
|
13920
|
+
- \`get <name>\` \u2014 one skill's metadata (JSON)
|
|
13921
|
+
- \`install <name>\` \u2014 install/refresh a skill into the user's agent clients
|
|
13922
|
+
- \`update [<name>]\` \u2014 update every installed skill, or just \`<name>\`, to latest
|
|
13923
|
+
- \`publish <name> <folder>\` \u2014 publish a local skill folder as \`<name>\`
|
|
13924
|
+
- \`config\` \u2014 show resolved backend / scope / clients (prints no secrets)
|
|
13925
|
+
|
|
13926
|
+
The token, backend URL, target clients, copy-vs-symlink mode, and scope all come
|
|
13927
|
+
from \`~/.config/exulu/skills.json\` (written by the installer). Never print the
|
|
13928
|
+
\`api_key\` or read it into your reply \u2014 the script uses it internally.
|
|
13929
|
+
|
|
13930
|
+
## Requests \u2192 commands
|
|
13931
|
+
|
|
13932
|
+
- "list / search skills" \u2192 \`exulu list\`, then filter the JSON for the user.
|
|
13933
|
+
- "install skill X" / "add the X skill" \u2192 \`exulu install X\`.
|
|
13934
|
+
- "update / get the latest version [of X]" \u2192 \`exulu update [X]\`.
|
|
13935
|
+
- "publish / upload this skill as X" \u2192 confirm the target name with the user; for
|
|
13936
|
+
an existing skill run \`exulu get X\` first and confirm a new version is intended;
|
|
13937
|
+
then \`exulu publish X <folder>\`.
|
|
13938
|
+
|
|
13939
|
+
## Not configured yet?
|
|
13940
|
+
|
|
13941
|
+
If \`exulu config\` reports it's not configured (or \`~/.config/exulu/skills.json\`
|
|
13942
|
+
is missing), tell the user to run the installer \u2014 it sets everything up
|
|
13943
|
+
interactively (base URL, API key, target clients, copy/symlink):
|
|
13944
|
+
|
|
13945
|
+
\`\`\`
|
|
13946
|
+
curl -fsSL <base_url>/api/skills/install.sh | sh
|
|
13947
|
+
\`\`\`
|
|
13948
|
+
|
|
13949
|
+
\`<base_url>\` is their Exulu frontend URL (e.g. https://ai.open.de). They can
|
|
13950
|
+
create an API key at \`<base_url>/token\`.
|
|
13951
|
+
|
|
13952
|
+
## Errors
|
|
13953
|
+
|
|
13954
|
+
- install: \`403\` = no access to that skill; \`404\` = unknown name.
|
|
13955
|
+
- publish: \`403\` = you can see it but lack write access; \`409\` = the name is
|
|
13956
|
+
taken by a skill you can't access.
|
|
13957
|
+
`;
|
|
13958
|
+
|
|
13766
13959
|
// src/exulu/routes.ts
|
|
13767
13960
|
var REQUEST_SIZE_LIMIT = "50mb";
|
|
13768
13961
|
var getExuluVersionNumber = async () => {
|
|
@@ -13836,6 +14029,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
|
|
|
13836
14029
|
}
|
|
13837
14030
|
next();
|
|
13838
14031
|
});
|
|
14032
|
+
const rawZip = express2.raw({ type: ["application/zip", "application/octet-stream", "application/x-zip-compressed", "application/x-zip"], limit: "50mb" });
|
|
13839
14033
|
console.log(`
|
|
13840
14034
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
|
|
13841
14035
|
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
@@ -15488,7 +15682,9 @@ ${style.markdown}` : params.prompt;
|
|
|
15488
15682
|
const budget_duration = String(body?.budget_duration ?? "");
|
|
15489
15683
|
if (!Number.isFinite(max_budget) || max_budget <= 0) return null;
|
|
15490
15684
|
if (!BUDGET_ALLOWED_DURATIONS.has(budget_duration)) return null;
|
|
15491
|
-
|
|
15685
|
+
const reset = parseResetAt(body?.budget_reset_at);
|
|
15686
|
+
if (!reset.valid) return null;
|
|
15687
|
+
return { max_budget, budget_duration, budget_reset_at: reset.value };
|
|
15492
15688
|
};
|
|
15493
15689
|
const parseBudgetSettingsBody = (body) => {
|
|
15494
15690
|
if (!body || typeof body !== "object") return null;
|
|
@@ -15558,7 +15754,7 @@ ${style.markdown}` : params.prompt;
|
|
|
15558
15754
|
}
|
|
15559
15755
|
const body = parseBudgetBody(req.body);
|
|
15560
15756
|
if (!body) {
|
|
15561
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
15757
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
15562
15758
|
return;
|
|
15563
15759
|
}
|
|
15564
15760
|
const entityIds = Array.isArray(req.body?.entityIds) ? req.body.entityIds : [];
|
|
@@ -15570,7 +15766,7 @@ ${style.markdown}` : params.prompt;
|
|
|
15570
15766
|
for (const id of entityIds) {
|
|
15571
15767
|
const tag = budgetTagFor(entityType, id);
|
|
15572
15768
|
try {
|
|
15573
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
15769
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
15574
15770
|
results.push({ entityId: String(id), ok: true });
|
|
15575
15771
|
} catch (err) {
|
|
15576
15772
|
results.push({
|
|
@@ -15595,12 +15791,12 @@ ${style.markdown}` : params.prompt;
|
|
|
15595
15791
|
}
|
|
15596
15792
|
const body = parseBudgetBody(req.body);
|
|
15597
15793
|
if (!body) {
|
|
15598
|
-
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration)." });
|
|
15794
|
+
res.status(400).json({ detail: "Invalid budget (max_budget, budget_duration, budget_reset_at)." });
|
|
15599
15795
|
return;
|
|
15600
15796
|
}
|
|
15601
15797
|
const tag = budgetTagFor(entityType, req.params.entityId ?? "");
|
|
15602
15798
|
try {
|
|
15603
|
-
await upsertBudget(tag, body.max_budget, body.budget_duration);
|
|
15799
|
+
await upsertBudget(tag, body.max_budget, body.budget_duration, body.budget_reset_at);
|
|
15604
15800
|
const info = await tagInfo([tag]);
|
|
15605
15801
|
res.status(200).json({ budget: info[tag] ?? null });
|
|
15606
15802
|
} catch (err) {
|
|
@@ -16063,6 +16259,199 @@ ${style.markdown}` : params.prompt;
|
|
|
16063
16259
|
}
|
|
16064
16260
|
return root;
|
|
16065
16261
|
}
|
|
16262
|
+
app.get("/skills/agent/bootstrap", async (_req, res) => {
|
|
16263
|
+
try {
|
|
16264
|
+
const zip = new JSZip3();
|
|
16265
|
+
zip.file("exulu-skills/SKILL.md", BOOTSTRAP_SKILL_MD);
|
|
16266
|
+
zip.file("exulu-skills/references/clients.json", BOOTSTRAP_CLIENTS_JSON);
|
|
16267
|
+
zip.file("exulu-skills/scripts/exulu", BOOTSTRAP_EXULU_SH, {
|
|
16268
|
+
unixPermissions: 493
|
|
16269
|
+
});
|
|
16270
|
+
const buffer = await zip.generateAsync({
|
|
16271
|
+
type: "nodebuffer",
|
|
16272
|
+
platform: "UNIX"
|
|
16273
|
+
});
|
|
16274
|
+
res.setHeader("Content-Type", "application/zip");
|
|
16275
|
+
res.setHeader("Content-Disposition", 'attachment; filename="exulu-skills.zip"');
|
|
16276
|
+
res.send(buffer);
|
|
16277
|
+
} catch (err) {
|
|
16278
|
+
console.error("[SKILLS] Failed to build bootstrap zip", err);
|
|
16279
|
+
res.status(500).json({ detail: "Failed to build bootstrap skill." });
|
|
16280
|
+
}
|
|
16281
|
+
});
|
|
16282
|
+
app.get("/skills/registry", async (req, res) => {
|
|
16283
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16284
|
+
if (!authResult.user?.id) {
|
|
16285
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16286
|
+
return;
|
|
16287
|
+
}
|
|
16288
|
+
const { db } = await postgresClient();
|
|
16289
|
+
const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
|
|
16290
|
+
const all = await db("skills").select("*");
|
|
16291
|
+
const readable = await filterReadableSkills(db, all, authResult.user);
|
|
16292
|
+
const skills = readable.filter((s) => {
|
|
16293
|
+
if (!tag) return true;
|
|
16294
|
+
const tags = Array.isArray(s.tags) ? s.tags : [];
|
|
16295
|
+
return tags.includes(tag);
|
|
16296
|
+
}).map((s) => ({
|
|
16297
|
+
name: s.name,
|
|
16298
|
+
description: s.description ?? "",
|
|
16299
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
16300
|
+
current_version: s.current_version ?? 1,
|
|
16301
|
+
updated_at: s.updatedAt ?? s.updated_at ?? null
|
|
16302
|
+
}));
|
|
16303
|
+
res.json({ skills });
|
|
16304
|
+
});
|
|
16305
|
+
app.get("/skills/registry/:name/download", async (req, res) => {
|
|
16306
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16307
|
+
if (!authResult.user?.id) {
|
|
16308
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16309
|
+
return;
|
|
16310
|
+
}
|
|
16311
|
+
const { db } = await postgresClient();
|
|
16312
|
+
const skill = await resolveSkillByName(db, req.params.name);
|
|
16313
|
+
if (!skill) {
|
|
16314
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
16315
|
+
return;
|
|
16316
|
+
}
|
|
16317
|
+
if (!await canAccessSkill(db, skill, "read", authResult.user)) {
|
|
16318
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
16319
|
+
return;
|
|
16320
|
+
}
|
|
16321
|
+
const vQuery = req.query.version;
|
|
16322
|
+
const version = !vQuery || vQuery === "latest" ? skill.current_version ?? 1 : Number(vQuery);
|
|
16323
|
+
if (!Number.isFinite(version) || version < 1) {
|
|
16324
|
+
res.status(400).json({ detail: "Invalid version." });
|
|
16325
|
+
return;
|
|
16326
|
+
}
|
|
16327
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
16328
|
+
const versionPrefix = `skills/${skill.id}/v${version}/`;
|
|
16329
|
+
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
16330
|
+
if (files.length === 0) {
|
|
16331
|
+
res.status(404).json({ detail: `Version v${version} has no files.` });
|
|
16332
|
+
return;
|
|
16333
|
+
}
|
|
16334
|
+
const zip = new JSZip3();
|
|
16335
|
+
for (const file of files) {
|
|
16336
|
+
const idx = file.key.indexOf(versionPrefix);
|
|
16337
|
+
const rel = idx >= 0 ? file.key.slice(idx + versionPrefix.length) : file.key;
|
|
16338
|
+
if (!rel) continue;
|
|
16339
|
+
const bytes = await getS3ObjectBytes(file.key, config);
|
|
16340
|
+
zip.file(`${safeName}/${rel}`, bytes);
|
|
16341
|
+
}
|
|
16342
|
+
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
16343
|
+
res.setHeader("Content-Type", "application/zip");
|
|
16344
|
+
res.setHeader("Content-Disposition", `attachment; filename="${safeName}.skill"`);
|
|
16345
|
+
res.send(buffer);
|
|
16346
|
+
});
|
|
16347
|
+
app.post("/skills/registry/:name", rawZip, async (req, res) => {
|
|
16348
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16349
|
+
if (!authResult.user?.id) {
|
|
16350
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16351
|
+
return;
|
|
16352
|
+
}
|
|
16353
|
+
const name = req.params.name;
|
|
16354
|
+
const bytes = req.body;
|
|
16355
|
+
if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
|
|
16356
|
+
res.status(400).json({ detail: "Empty body. Send the skill as a zip/.skill payload." });
|
|
16357
|
+
return;
|
|
16358
|
+
}
|
|
16359
|
+
const { db } = await postgresClient();
|
|
16360
|
+
const existing = await resolveSkillByName(db, name);
|
|
16361
|
+
if (existing) {
|
|
16362
|
+
const canWrite = await canAccessSkill(db, existing, "write", authResult.user);
|
|
16363
|
+
if (canWrite) {
|
|
16364
|
+
const nextVersion = (existing.current_version ?? 1) + 1;
|
|
16365
|
+
try {
|
|
16366
|
+
await extractBundleToVersion({ bytes, skillId: existing.id, version: nextVersion, config });
|
|
16367
|
+
} catch (err) {
|
|
16368
|
+
if (err instanceof BundleValidationError) {
|
|
16369
|
+
res.status(400).json({ detail: err.message });
|
|
16370
|
+
return;
|
|
16371
|
+
}
|
|
16372
|
+
console.error("[SKILLS] publish (new version) failed", err);
|
|
16373
|
+
res.status(500).json({ detail: "Failed to publish new version." });
|
|
16374
|
+
return;
|
|
16375
|
+
}
|
|
16376
|
+
const history = Array.isArray(existing.history) ? existing.history : [];
|
|
16377
|
+
await db("skills").where({ id: existing.id }).update({
|
|
16378
|
+
current_version: nextVersion,
|
|
16379
|
+
history: JSON.stringify([
|
|
16380
|
+
...history,
|
|
16381
|
+
{ version: nextVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
16382
|
+
])
|
|
16383
|
+
});
|
|
16384
|
+
res.json({ name, version: nextVersion, created: false });
|
|
16385
|
+
return;
|
|
16386
|
+
} else {
|
|
16387
|
+
const canRead = await canAccessSkill(db, existing, "read", authResult.user);
|
|
16388
|
+
if (!canRead) {
|
|
16389
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
16390
|
+
return;
|
|
16391
|
+
}
|
|
16392
|
+
res.status(403).json({ detail: "You don't have write access to this skill." });
|
|
16393
|
+
return;
|
|
16394
|
+
}
|
|
16395
|
+
}
|
|
16396
|
+
const meta = await parseSkillFrontmatter(bytes);
|
|
16397
|
+
const skillId = randomUUID3();
|
|
16398
|
+
try {
|
|
16399
|
+
await extractBundleToVersion({ bytes, skillId, version: 1, config });
|
|
16400
|
+
} catch (err) {
|
|
16401
|
+
if (err instanceof BundleValidationError) {
|
|
16402
|
+
res.status(400).json({ detail: err.message });
|
|
16403
|
+
return;
|
|
16404
|
+
}
|
|
16405
|
+
console.error("[SKILLS] publish (create) failed", err);
|
|
16406
|
+
res.status(500).json({ detail: "Failed to publish skill." });
|
|
16407
|
+
return;
|
|
16408
|
+
}
|
|
16409
|
+
try {
|
|
16410
|
+
await db("skills").insert({
|
|
16411
|
+
id: skillId,
|
|
16412
|
+
name,
|
|
16413
|
+
description: meta.description ?? "",
|
|
16414
|
+
s3folder: `skills/${skillId}`,
|
|
16415
|
+
tags: JSON.stringify([]),
|
|
16416
|
+
usage_count: 0,
|
|
16417
|
+
favorite_count: 0,
|
|
16418
|
+
current_version: 1,
|
|
16419
|
+
history: JSON.stringify([
|
|
16420
|
+
{ version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Published from agent" }
|
|
16421
|
+
]),
|
|
16422
|
+
rights_mode: "private",
|
|
16423
|
+
created_by: authResult.user.id
|
|
16424
|
+
});
|
|
16425
|
+
} catch (err) {
|
|
16426
|
+
res.status(409).json({ detail: "That name is unavailable." });
|
|
16427
|
+
return;
|
|
16428
|
+
}
|
|
16429
|
+
res.json({ name, version: 1, created: true });
|
|
16430
|
+
});
|
|
16431
|
+
app.get("/skills/registry/:name", async (req, res) => {
|
|
16432
|
+
const authResult = await requestValidators.authenticate(req);
|
|
16433
|
+
if (!authResult.user?.id) {
|
|
16434
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
16435
|
+
return;
|
|
16436
|
+
}
|
|
16437
|
+
const { db } = await postgresClient();
|
|
16438
|
+
const skill = await resolveSkillByName(db, req.params.name);
|
|
16439
|
+
if (!skill) {
|
|
16440
|
+
res.status(404).json({ detail: "Skill not found." });
|
|
16441
|
+
return;
|
|
16442
|
+
}
|
|
16443
|
+
if (!await canAccessSkill(db, skill, "read", authResult.user)) {
|
|
16444
|
+
res.status(403).json({ detail: "You don't have access to this skill." });
|
|
16445
|
+
return;
|
|
16446
|
+
}
|
|
16447
|
+
res.json({
|
|
16448
|
+
name: skill.name,
|
|
16449
|
+
description: skill.description ?? "",
|
|
16450
|
+
tags: Array.isArray(skill.tags) ? skill.tags : [],
|
|
16451
|
+
current_version: skill.current_version ?? 1,
|
|
16452
|
+
history: Array.isArray(skill.history) ? skill.history : []
|
|
16453
|
+
});
|
|
16454
|
+
});
|
|
16066
16455
|
app.post("/skills/:skillId/init", async (req, res) => {
|
|
16067
16456
|
const authResult = await requestValidators.authenticate(req);
|
|
16068
16457
|
if (!authResult.user?.id) {
|
|
@@ -16110,8 +16499,8 @@ ${style.markdown}` : params.prompt;
|
|
|
16110
16499
|
}
|
|
16111
16500
|
const { skillId } = req.params;
|
|
16112
16501
|
const { extension, contentType } = req.body ?? {};
|
|
16113
|
-
if (extension !== ".zip" && extension !== ".md") {
|
|
16114
|
-
res.status(400).json({ detail: 'extension must be ".zip" or ".
|
|
16502
|
+
if (extension !== ".zip" && extension !== ".md" && extension !== ".skill") {
|
|
16503
|
+
res.status(400).json({ detail: 'extension must be ".zip", ".md", or ".skill".' });
|
|
16115
16504
|
return;
|
|
16116
16505
|
}
|
|
16117
16506
|
if (!contentType || typeof contentType !== "string") {
|
|
@@ -16250,19 +16639,22 @@ ${style.markdown}` : params.prompt;
|
|
|
16250
16639
|
}
|
|
16251
16640
|
const versionPrefix = `skills/${skillId}/v${version}/`;
|
|
16252
16641
|
const files = await listS3ObjectsByPrefix(versionPrefix, config);
|
|
16253
|
-
const
|
|
16642
|
+
const asSkill = req.query.format === "skill";
|
|
16643
|
+
const safeName = String(skill.name ?? "skill").replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "") || "skill";
|
|
16644
|
+
const zip = new JSZip3();
|
|
16254
16645
|
let fileCount = 0;
|
|
16255
16646
|
for (const file of files) {
|
|
16256
16647
|
const prefixIndex = file.key.indexOf(versionPrefix);
|
|
16257
16648
|
const relativePath = prefixIndex >= 0 ? file.key.slice(prefixIndex + versionPrefix.length) : file.key;
|
|
16258
16649
|
if (!relativePath) continue;
|
|
16259
16650
|
const bytes = await getS3ObjectBytes(file.key, config);
|
|
16260
|
-
|
|
16651
|
+
const archivePath = asSkill ? `${safeName}/${relativePath}` : relativePath;
|
|
16652
|
+
zip.file(archivePath, bytes);
|
|
16261
16653
|
fileCount += 1;
|
|
16262
16654
|
}
|
|
16263
16655
|
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16264
16656
|
zip.file(
|
|
16265
|
-
"version.txt",
|
|
16657
|
+
asSkill ? `${safeName}/version.txt` : "version.txt",
|
|
16266
16658
|
[
|
|
16267
16659
|
`Skill: ${skill.name ?? skillId}`,
|
|
16268
16660
|
`Skill id: ${skillId}`,
|
|
@@ -16273,13 +16665,9 @@ ${style.markdown}` : params.prompt;
|
|
|
16273
16665
|
].join("\n")
|
|
16274
16666
|
);
|
|
16275
16667
|
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
16276
|
-
const
|
|
16277
|
-
const filename = `${safeName}-v${version}.zip`;
|
|
16668
|
+
const filename = asSkill ? `${safeName}.skill` : `${safeName}-v${version}.zip`;
|
|
16278
16669
|
res.setHeader("Content-Type", "application/zip");
|
|
16279
|
-
res.setHeader(
|
|
16280
|
-
"Content-Disposition",
|
|
16281
|
-
`attachment; filename="${filename}"`
|
|
16282
|
-
);
|
|
16670
|
+
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
16283
16671
|
res.send(buffer);
|
|
16284
16672
|
});
|
|
16285
16673
|
app.post("/skills/:skillId/sign", async (req, res) => {
|
|
@@ -16579,6 +16967,12 @@ ${style.markdown}` : params.prompt;
|
|
|
16579
16967
|
const fullSessionPrefix = `${generalPrefix}${userSessionPrefix}`;
|
|
16580
16968
|
return { userSessionPrefix, fullSessionPrefix };
|
|
16581
16969
|
}
|
|
16970
|
+
const loadSessionFilesAuth = async (req, res, sessionId, rights) => {
|
|
16971
|
+
const authed = await loadAuthedSession(req, res, sessionId, rights);
|
|
16972
|
+
if (!authed) return null;
|
|
16973
|
+
const ownerId = authed.session.user ?? authed.user.id;
|
|
16974
|
+
return { ...authed, ownerId, ...buildSessionPrefixes(ownerId, sessionId) };
|
|
16975
|
+
};
|
|
16582
16976
|
function sanitizeFilename(name) {
|
|
16583
16977
|
const trimmed = name.trim();
|
|
16584
16978
|
if (!trimmed) return "";
|
|
@@ -16587,11 +16981,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16587
16981
|
return trimmed.replace(/[\\/]/g, "_");
|
|
16588
16982
|
}
|
|
16589
16983
|
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
16984
|
const sessionId = req.params.sessionId;
|
|
16596
16985
|
if (!sessionId) {
|
|
16597
16986
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16601,10 +16990,9 @@ ${style.markdown}` : params.prompt;
|
|
|
16601
16990
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
16602
16991
|
return;
|
|
16603
16992
|
}
|
|
16604
|
-
const
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
);
|
|
16993
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
16994
|
+
if (!authed) return;
|
|
16995
|
+
const { userSessionPrefix } = authed;
|
|
16608
16996
|
let objects;
|
|
16609
16997
|
try {
|
|
16610
16998
|
objects = await listS3ObjectsByPrefix(userSessionPrefix, config);
|
|
@@ -16634,11 +17022,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16634
17022
|
app.post(
|
|
16635
17023
|
"/sessions/:sessionId/files/upload-sign",
|
|
16636
17024
|
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
17025
|
const sessionId = req.params.sessionId;
|
|
16643
17026
|
if (!sessionId) {
|
|
16644
17027
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16648,6 +17031,8 @@ ${style.markdown}` : params.prompt;
|
|
|
16648
17031
|
res.status(500).json({ detail: "File uploads are not configured." });
|
|
16649
17032
|
return;
|
|
16650
17033
|
}
|
|
17034
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
17035
|
+
if (!authed) return;
|
|
16651
17036
|
const { filename, contentType } = req.body ?? {};
|
|
16652
17037
|
if (!filename || typeof filename !== "string") {
|
|
16653
17038
|
res.status(400).json({ detail: "Missing filename in request body." });
|
|
@@ -16662,11 +17047,7 @@ ${style.markdown}` : params.prompt;
|
|
|
16662
17047
|
res.status(400).json({ detail: "Missing contentType in request body." });
|
|
16663
17048
|
return;
|
|
16664
17049
|
}
|
|
16665
|
-
const
|
|
16666
|
-
authResult.user.id,
|
|
16667
|
-
sessionId
|
|
16668
|
-
);
|
|
16669
|
-
const fullKey = `${fullSessionPrefix}${safeName}`;
|
|
17050
|
+
const fullKey = `${authed.fullSessionPrefix}${safeName}`;
|
|
16670
17051
|
const uploadUrl = await getS3SignedUploadUrl(fullKey, contentType, config);
|
|
16671
17052
|
res.json({ uploadUrl, key: fullKey });
|
|
16672
17053
|
}
|
|
@@ -16674,11 +17055,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16674
17055
|
app.post(
|
|
16675
17056
|
"/sessions/:sessionId/files/sync-to-sandbox",
|
|
16676
17057
|
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
17058
|
const sessionId = req.params.sessionId;
|
|
16683
17059
|
if (!sessionId) {
|
|
16684
17060
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16689,18 +17065,16 @@ ${style.markdown}` : params.prompt;
|
|
|
16689
17065
|
res.status(400).json({ detail: "Missing key in request body." });
|
|
16690
17066
|
return;
|
|
16691
17067
|
}
|
|
16692
|
-
const
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
);
|
|
16696
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
17068
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
17069
|
+
if (!authed) return;
|
|
17070
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
16697
17071
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
16698
17072
|
return;
|
|
16699
17073
|
}
|
|
16700
17074
|
try {
|
|
16701
17075
|
const result = await downloadKeyIntoSandbox({
|
|
16702
17076
|
sessionId,
|
|
16703
|
-
userId:
|
|
17077
|
+
userId: authed.ownerId,
|
|
16704
17078
|
fullS3Key: key,
|
|
16705
17079
|
config
|
|
16706
17080
|
});
|
|
@@ -16714,11 +17088,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16714
17088
|
app.delete(
|
|
16715
17089
|
"/sessions/:sessionId/files",
|
|
16716
17090
|
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
17091
|
const sessionId = req.params.sessionId;
|
|
16723
17092
|
if (!sessionId) {
|
|
16724
17093
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16729,11 +17098,9 @@ ${style.markdown}` : params.prompt;
|
|
|
16729
17098
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
16730
17099
|
return;
|
|
16731
17100
|
}
|
|
16732
|
-
const
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
);
|
|
16736
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
17101
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "write");
|
|
17102
|
+
if (!authed) return;
|
|
17103
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
16737
17104
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
16738
17105
|
return;
|
|
16739
17106
|
}
|
|
@@ -16752,11 +17119,6 @@ ${style.markdown}` : params.prompt;
|
|
|
16752
17119
|
if (!req.headers.authorization && typeof req.query.auth === "string") {
|
|
16753
17120
|
req.headers.authorization = `Bearer ${req.query.auth}`;
|
|
16754
17121
|
}
|
|
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
17122
|
const sessionId = req.params.sessionId;
|
|
16761
17123
|
if (!sessionId) {
|
|
16762
17124
|
res.status(400).json({ detail: "Missing sessionId in path." });
|
|
@@ -16767,11 +17129,9 @@ ${style.markdown}` : params.prompt;
|
|
|
16767
17129
|
res.status(400).json({ detail: "Missing or invalid 'key' query parameter." });
|
|
16768
17130
|
return;
|
|
16769
17131
|
}
|
|
16770
|
-
const
|
|
16771
|
-
|
|
16772
|
-
|
|
16773
|
-
);
|
|
16774
|
-
if (!key.startsWith(fullSessionPrefix)) {
|
|
17132
|
+
const authed = await loadSessionFilesAuth(req, res, sessionId, "read");
|
|
17133
|
+
if (!authed) return;
|
|
17134
|
+
if (!key.startsWith(authed.fullSessionPrefix)) {
|
|
16775
17135
|
res.status(403).json({ detail: "Key does not belong to this session." });
|
|
16776
17136
|
return;
|
|
16777
17137
|
}
|
|
@@ -16875,7 +17235,7 @@ ${style.markdown}` : params.prompt;
|
|
|
16875
17235
|
` - tracking.json tracking events linked to the user`,
|
|
16876
17236
|
``
|
|
16877
17237
|
].join("\n");
|
|
16878
|
-
const zip = new
|
|
17238
|
+
const zip = new JSZip3();
|
|
16879
17239
|
zip.file("README.txt", readme);
|
|
16880
17240
|
zip.file("user_data.json", JSON.stringify(userExport, null, 2));
|
|
16881
17241
|
zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
|