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