@lumerahq/cli 0.31.0-dev.0 → 0.31.0-dev.1
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/{chunk-AEZLQMFF.js → chunk-B6Y5CECC.js} +2 -2
- package/dist/{chunk-ODANFLL7.js → chunk-HS63O4MH.js} +93 -11
- package/dist/{chunk-2K4ZYGCE.js → chunk-IVII7OZZ.js} +1 -1
- package/dist/{chunk-E7YZ6QS6.js → chunk-ZJ7M7JD3.js} +29 -0
- package/dist/{deps-ZDRNDMUF.js → deps-SIZ77NYE.js} +6 -4
- package/dist/{dev-S5JVAXDD.js → dev-7ZQRZJXX.js} +3 -3
- package/dist/{functions-DZCNHDRJ.js → functions-WPHGKE54.js} +1 -1
- package/dist/index.js +14 -14
- package/dist/{init-QVR42DUW.js → init-WE722M4X.js} +1 -1
- package/dist/{project-3HITJUM4.js → project-BDRAEZWE.js} +1 -1
- package/dist/{register-G3YK77HE.js → register-ZXNNU3NJ.js} +1 -1
- package/dist/{resources-I6JSKGOX.js → resources-GM2OWDKP.js} +339 -69
- package/dist/{run-MSAMUYQH.js → run-7KJPBOIY.js} +1 -1
- package/package.json +1 -1
- package/templates/default/package.json +8 -1
- package/templates/default/pnpm-lock.yaml +172 -183
- package/templates/default/pyproject.toml +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deploy
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-IVII7OZZ.js";
|
|
4
4
|
import {
|
|
5
5
|
syncDeps
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-HS63O4MH.js";
|
|
7
7
|
import {
|
|
8
8
|
loadEnv
|
|
9
9
|
} from "./chunk-2CR762KB.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
createApiClient,
|
|
13
13
|
isApiErrorStatus,
|
|
14
14
|
validateCollectionNameLength
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-ZJ7M7JD3.js";
|
|
16
16
|
import {
|
|
17
17
|
findProjectRoot,
|
|
18
18
|
getApiUrl,
|
|
@@ -31,8 +31,9 @@ import "./chunk-PNKVD2UK.js";
|
|
|
31
31
|
import pc2 from "picocolors";
|
|
32
32
|
import prompts from "prompts";
|
|
33
33
|
import { execFileSync, execSync } from "child_process";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
34
|
+
import { createHash } from "crypto";
|
|
35
|
+
import { existsSync as existsSync2, lstatSync, mkdirSync, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
|
|
36
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
36
37
|
|
|
37
38
|
// src/lib/lint/index.ts
|
|
38
39
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
@@ -2496,6 +2497,7 @@ async function applyApp(args) {
|
|
|
2496
2497
|
const accessLevel = getFlagValue(args, "access-level");
|
|
2497
2498
|
const validAccessLevels = [
|
|
2498
2499
|
"org_members_with_permission",
|
|
2500
|
+
"specific_people",
|
|
2499
2501
|
"org_members",
|
|
2500
2502
|
"invited",
|
|
2501
2503
|
"signed_in",
|
|
@@ -2642,6 +2644,130 @@ var SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
2642
2644
|
var SKILL_NAME_MAX_LENGTH = 100;
|
|
2643
2645
|
var SKILL_SLUG_MAX_LENGTH = 100;
|
|
2644
2646
|
var SKILL_SUMMARY_MAX_LENGTH = 500;
|
|
2647
|
+
var SKILL_RESOURCE_MAX_COUNT = 20;
|
|
2648
|
+
var SKILL_RESOURCE_MAX_BYTES = 10 * 1024 * 1024;
|
|
2649
|
+
var SKILL_RESOURCES_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
|
|
2650
|
+
var SKILL_RESOURCE_MAX_PATH_BYTES = 1024;
|
|
2651
|
+
var SKILL_RESOURCE_MAX_PATH_PART_BYTES = 255;
|
|
2652
|
+
var SKILL_RESOURCE_CONTENT_TYPES = {
|
|
2653
|
+
bmp: "image/bmp",
|
|
2654
|
+
gif: "image/gif",
|
|
2655
|
+
jpeg: "image/jpeg",
|
|
2656
|
+
jpg: "image/jpeg",
|
|
2657
|
+
pdf: "application/pdf",
|
|
2658
|
+
png: "image/png",
|
|
2659
|
+
webp: "image/webp"
|
|
2660
|
+
};
|
|
2661
|
+
function skillResourceContentType(path) {
|
|
2662
|
+
return SKILL_RESOURCE_CONTENT_TYPES[path.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
|
|
2663
|
+
}
|
|
2664
|
+
function skillResourcePathError(value) {
|
|
2665
|
+
if (!value || Buffer.byteLength(value) > SKILL_RESOURCE_MAX_PATH_BYTES) {
|
|
2666
|
+
return `path must be at most ${SKILL_RESOURCE_MAX_PATH_BYTES} bytes`;
|
|
2667
|
+
}
|
|
2668
|
+
if (value.startsWith("/") || value.endsWith("/") || value.includes("\\")) {
|
|
2669
|
+
return "path must be relative and slash-separated";
|
|
2670
|
+
}
|
|
2671
|
+
if (/\p{Cc}/u.test(value)) return "path cannot contain control characters";
|
|
2672
|
+
for (const part of value.split("/")) {
|
|
2673
|
+
if (!part || part === "." || part === "..") return "path cannot contain empty, dot, or parent segments";
|
|
2674
|
+
if (part !== part.trim()) return "path segments cannot start or end with whitespace";
|
|
2675
|
+
if (Buffer.byteLength(part) > SKILL_RESOURCE_MAX_PATH_PART_BYTES) {
|
|
2676
|
+
return `path segments must be at most ${SKILL_RESOURCE_MAX_PATH_PART_BYTES} bytes`;
|
|
2677
|
+
}
|
|
2678
|
+
if (part.toLowerCase() === "skill.md") return "SKILL.md is reserved";
|
|
2679
|
+
}
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2682
|
+
function skillResourcePathsCollide(left, right) {
|
|
2683
|
+
const leftParts = left.split("/");
|
|
2684
|
+
const rightParts = right.split("/");
|
|
2685
|
+
const limit = Math.min(leftParts.length, rightParts.length);
|
|
2686
|
+
for (let i = 0; i < limit; i++) {
|
|
2687
|
+
if (leftParts[i] === rightParts[i]) continue;
|
|
2688
|
+
return leftParts[i]?.toLowerCase() === rightParts[i]?.toLowerCase();
|
|
2689
|
+
}
|
|
2690
|
+
return true;
|
|
2691
|
+
}
|
|
2692
|
+
function localSkillResources(skillDir) {
|
|
2693
|
+
const resources = [];
|
|
2694
|
+
const errors = [];
|
|
2695
|
+
let totalBytes = 0;
|
|
2696
|
+
let fileCount = 0;
|
|
2697
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2698
|
+
const scan = (directory, parentParts) => {
|
|
2699
|
+
const names = readdirSync2(directory, { encoding: "buffer" }).map((rawName) => {
|
|
2700
|
+
try {
|
|
2701
|
+
return decoder.decode(rawName);
|
|
2702
|
+
} catch {
|
|
2703
|
+
errors.push(`${parentParts.join("/") || "."}: file names must be valid UTF-8`);
|
|
2704
|
+
return null;
|
|
2705
|
+
}
|
|
2706
|
+
}).filter((name) => name !== null).sort();
|
|
2707
|
+
for (const name of names) {
|
|
2708
|
+
if (parentParts.length === 0 && name === "SKILL.md") continue;
|
|
2709
|
+
const parts = [...parentParts, name];
|
|
2710
|
+
const resourcePath = parts.join("/");
|
|
2711
|
+
const filePath = join2(directory, name);
|
|
2712
|
+
const stat = lstatSync(filePath);
|
|
2713
|
+
if (stat.isSymbolicLink()) {
|
|
2714
|
+
errors.push(`${resourcePath}: symlinks are not supported`);
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
if (stat.isDirectory()) {
|
|
2718
|
+
scan(filePath, parts);
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
if (!stat.isFile()) {
|
|
2722
|
+
errors.push(`${resourcePath}: only regular files are supported`);
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
fileCount++;
|
|
2726
|
+
totalBytes += stat.size;
|
|
2727
|
+
const pathError = skillResourcePathError(resourcePath);
|
|
2728
|
+
if (pathError) {
|
|
2729
|
+
errors.push(`${resourcePath}: ${pathError}`);
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
if (stat.size > SKILL_RESOURCE_MAX_BYTES) {
|
|
2733
|
+
errors.push(`${resourcePath}: file exceeds the 10 MiB limit`);
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
const conflict = resources.find((resource) => skillResourcePathsCollide(resource.path, resourcePath));
|
|
2737
|
+
if (conflict) {
|
|
2738
|
+
errors.push(`${resourcePath}: path conflicts ignoring letter case with ${conflict.path}`);
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const sha256 = createHash("sha256").update(readFileSync2(filePath)).digest("hex");
|
|
2742
|
+
resources.push({ path: resourcePath, filePath, size: stat.size, sha256 });
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
scan(skillDir, []);
|
|
2746
|
+
if (fileCount > SKILL_RESOURCE_MAX_COUNT) errors.push(`resources: a skill can have at most ${SKILL_RESOURCE_MAX_COUNT} files`);
|
|
2747
|
+
if (totalBytes > SKILL_RESOURCES_MAX_TOTAL_BYTES) errors.push("resources: files can total at most 100 MiB");
|
|
2748
|
+
return { resources: resources.sort((a, b) => a.path.localeCompare(b.path)), errors };
|
|
2749
|
+
}
|
|
2750
|
+
function compareSkillResources(local, remote) {
|
|
2751
|
+
const localByPath = new Map(local.map((resource) => [resource.path, resource]));
|
|
2752
|
+
const remoteByPath = new Map(remote.map((resource) => [resource.path, resource]));
|
|
2753
|
+
const added = [...localByPath.keys()].filter((path) => !remoteByPath.has(path)).sort();
|
|
2754
|
+
const replaced = [...localByPath].filter(([path, resource]) => {
|
|
2755
|
+
const current = remoteByPath.get(path);
|
|
2756
|
+
return current && (current.size !== resource.size || current.sha256 !== resource.sha256);
|
|
2757
|
+
}).map(([path]) => path).sort();
|
|
2758
|
+
const deleted = [...remoteByPath.keys()].filter((path) => !localByPath.has(path)).sort();
|
|
2759
|
+
return { added, replaced, deleted };
|
|
2760
|
+
}
|
|
2761
|
+
function skillResourceChangesEmpty(changes) {
|
|
2762
|
+
return changes.added.length === 0 && changes.replaced.length === 0 && changes.deleted.length === 0;
|
|
2763
|
+
}
|
|
2764
|
+
function skillResourceChangesSummary(changes) {
|
|
2765
|
+
return [
|
|
2766
|
+
...changes.added.map((path) => `+${path}`),
|
|
2767
|
+
...changes.replaced.map((path) => `~${path}`),
|
|
2768
|
+
...changes.deleted.map((path) => `-${path}`)
|
|
2769
|
+
].join(", ");
|
|
2770
|
+
}
|
|
2645
2771
|
function loadLocalSkills(platformDir, filterName) {
|
|
2646
2772
|
const skillsDir = join2(platformDir, "skills");
|
|
2647
2773
|
if (!existsSync2(skillsDir)) return [];
|
|
@@ -2654,11 +2780,19 @@ function loadLocalSkills(platformDir, filterName) {
|
|
|
2654
2780
|
errors.push(`${entry.name}: directory name must be a lowercase slug`);
|
|
2655
2781
|
continue;
|
|
2656
2782
|
}
|
|
2657
|
-
const
|
|
2783
|
+
const skillDir = join2(skillsDir, entry.name);
|
|
2784
|
+
const path = join2(skillDir, "SKILL.md");
|
|
2658
2785
|
if (!existsSync2(path)) {
|
|
2659
2786
|
errors.push(`${entry.name}: missing SKILL.md`);
|
|
2660
2787
|
continue;
|
|
2661
2788
|
}
|
|
2789
|
+
const skillMarkdownStat = lstatSync(path);
|
|
2790
|
+
if (skillMarkdownStat.isSymbolicLink() || !skillMarkdownStat.isFile()) {
|
|
2791
|
+
errors.push(`${entry.name}: SKILL.md must be a regular file`);
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
const { resources, errors: resourceErrors } = localSkillResources(skillDir);
|
|
2795
|
+
errors.push(...resourceErrors.map((error3) => `${entry.name}: ${error3}`));
|
|
2662
2796
|
const content = readFileSync2(path, "utf-8");
|
|
2663
2797
|
const separator = /\r?\n---\r?\n/.exec(content);
|
|
2664
2798
|
const parts = separator ? [content.slice(0, separator.index), content.slice(separator.index + separator[0].length)] : [content];
|
|
@@ -2684,7 +2818,7 @@ function loadLocalSkills(platformDir, filterName) {
|
|
|
2684
2818
|
if (summary.length > SKILL_SUMMARY_MAX_LENGTH) errors.push(`${entry.name}: summary must be at most ${SKILL_SUMMARY_MAX_LENGTH} characters`);
|
|
2685
2819
|
if (!instructions) errors.push(`${entry.name}: SKILL.md must include instructions after ---`);
|
|
2686
2820
|
if (name && name.length <= SKILL_NAME_MAX_LENGTH && summary && summary.length <= SKILL_SUMMARY_MAX_LENGTH && instructions) {
|
|
2687
|
-
skills.push({ slug: entry.name, name, summary, instructions });
|
|
2821
|
+
skills.push({ slug: entry.name, name, summary, instructions, resources });
|
|
2688
2822
|
}
|
|
2689
2823
|
}
|
|
2690
2824
|
if (errors.length > 0) {
|
|
@@ -2728,6 +2862,13 @@ function remoteSkillsByLocalSlug(remoteSkills, appName) {
|
|
|
2728
2862
|
}
|
|
2729
2863
|
return byLocalSlug;
|
|
2730
2864
|
}
|
|
2865
|
+
function resolveProjectSkillLocalSlug(remoteSkills, selector) {
|
|
2866
|
+
if (!selector) return void 0;
|
|
2867
|
+
for (const [localSlug, skill] of remoteSkills) {
|
|
2868
|
+
if (selector === localSlug || selector === skill.slug || selector === skill.name) return localSlug;
|
|
2869
|
+
}
|
|
2870
|
+
return selector;
|
|
2871
|
+
}
|
|
2731
2872
|
function projectSkillRefCandidates(refs, appName) {
|
|
2732
2873
|
return [...new Set(refs.flatMap((ref) => appName && SKILL_SLUG_RE.test(ref) ? [scopedProjectSkillSlug(appName, ref), ref] : [ref]))];
|
|
2733
2874
|
}
|
|
@@ -2794,13 +2935,24 @@ async function planSkills(api, localSkills, projectId, appName) {
|
|
|
2794
2935
|
for (const skill of localSkills) {
|
|
2795
2936
|
const remote = remoteBySlug.get(skill.slug);
|
|
2796
2937
|
if (!remote) {
|
|
2797
|
-
|
|
2938
|
+
const resourceSummary = skillResourceChangesSummary(compareSkillResources(skill.resources, []));
|
|
2939
|
+
changes.push({
|
|
2940
|
+
type: "create",
|
|
2941
|
+
resource: "skill",
|
|
2942
|
+
id: skill.slug,
|
|
2943
|
+
name: skill.name,
|
|
2944
|
+
details: resourceSummary ? `resources (${resourceSummary})` : void 0
|
|
2945
|
+
});
|
|
2798
2946
|
continue;
|
|
2799
2947
|
}
|
|
2800
2948
|
const changed = [];
|
|
2801
2949
|
if (remote.name !== skill.name) changed.push("name");
|
|
2802
2950
|
if ((remote.summary || "").trim() !== skill.summary.trim()) changed.push("summary");
|
|
2803
2951
|
if ((remote.instructions || "").trim() !== skill.instructions.trim()) changed.push("instructions");
|
|
2952
|
+
const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
|
|
2953
|
+
if (!skillResourceChangesEmpty(resourceChanges)) {
|
|
2954
|
+
changed.push(`resources (${skillResourceChangesSummary(resourceChanges)})`);
|
|
2955
|
+
}
|
|
2804
2956
|
if (changed.length > 0) {
|
|
2805
2957
|
changes.push({
|
|
2806
2958
|
type: "update",
|
|
@@ -2808,12 +2960,64 @@ async function planSkills(api, localSkills, projectId, appName) {
|
|
|
2808
2960
|
id: skill.slug,
|
|
2809
2961
|
name: skill.name,
|
|
2810
2962
|
details: `changed: ${changed.join(", ")}`,
|
|
2811
|
-
textDiffs: changed.includes("instructions") || changed.includes("summary") ? [{ field: "SKILL.md", oldText: skillMarkdown({
|
|
2963
|
+
textDiffs: changed.includes("instructions") || changed.includes("summary") ? [{ field: "SKILL.md", oldText: skillMarkdown({ name: remote.name, summary: remote.summary || "", instructions: remote.instructions || "" }), newText: skillMarkdown(skill) }] : void 0
|
|
2812
2964
|
});
|
|
2813
2965
|
}
|
|
2814
2966
|
}
|
|
2815
2967
|
return changes;
|
|
2816
2968
|
}
|
|
2969
|
+
async function uploadSkillResource(api, skillID, resource) {
|
|
2970
|
+
const bytes = readFileSync2(resource.filePath);
|
|
2971
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
2972
|
+
if (bytes.byteLength !== resource.size || sha256 !== resource.sha256) {
|
|
2973
|
+
throw new Error(`${resource.path} changed while apply was running`);
|
|
2974
|
+
}
|
|
2975
|
+
const metadata = {
|
|
2976
|
+
path: resource.path,
|
|
2977
|
+
size: resource.size,
|
|
2978
|
+
content_type: skillResourceContentType(resource.path),
|
|
2979
|
+
sha256: resource.sha256
|
|
2980
|
+
};
|
|
2981
|
+
const upload = await api.startAgentSkillResourceUpload(skillID, metadata);
|
|
2982
|
+
const headers = { ...upload.headers ?? {} };
|
|
2983
|
+
if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
|
|
2984
|
+
headers["Content-Type"] = metadata.content_type;
|
|
2985
|
+
}
|
|
2986
|
+
const response = await fetchWithRetry(upload.upload_url, {
|
|
2987
|
+
method: "PUT",
|
|
2988
|
+
headers,
|
|
2989
|
+
body: bytes
|
|
2990
|
+
}, { attempts: 3, retryMethods: ["PUT"] });
|
|
2991
|
+
if (!response.ok) throw new Error(`${resource.path}: upload failed with HTTP ${response.status}`);
|
|
2992
|
+
await api.completeAgentSkillResourceUpload(skillID, upload.upload_id, metadata);
|
|
2993
|
+
}
|
|
2994
|
+
async function reconcileSkillResources(api, skill, localResources) {
|
|
2995
|
+
let remoteResources = skill.resources ?? [];
|
|
2996
|
+
let changes = compareSkillResources(localResources, remoteResources);
|
|
2997
|
+
if (changes.deleted.length > 0) {
|
|
2998
|
+
const deletions = changes.deleted;
|
|
2999
|
+
const expectedByPath = new Map(remoteResources.map((resource) => [resource.path, resource]));
|
|
3000
|
+
remoteResources = (await api.getAgentSkill(skill.id)).resources ?? [];
|
|
3001
|
+
for (const resourcePath of deletions) {
|
|
3002
|
+
const expected = expectedByPath.get(resourcePath);
|
|
3003
|
+
const current = remoteResources.find((resource) => resource.path === resourcePath);
|
|
3004
|
+
if (!current) continue;
|
|
3005
|
+
if (!expected || current.size !== expected.size || current.sha256 !== expected.sha256) {
|
|
3006
|
+
throw new Error(`${resourcePath} changed while apply was running`);
|
|
3007
|
+
}
|
|
3008
|
+
const result = await api.deleteAgentSkillResource(skill.id, resourcePath);
|
|
3009
|
+
remoteResources = result.resources;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
changes = compareSkillResources(localResources, remoteResources);
|
|
3013
|
+
if (changes.deleted.length > 0) throw new Error("remote resources changed while apply was running");
|
|
3014
|
+
const uploadPaths = /* @__PURE__ */ new Set([...changes.added, ...changes.replaced]);
|
|
3015
|
+
const remoteByPath = new Map(remoteResources.map((resource) => [resource.path, resource]));
|
|
3016
|
+
const uploads = localResources.filter((resource) => uploadPaths.has(resource.path)).sort(
|
|
3017
|
+
(a, b) => a.size - (remoteByPath.get(a.path)?.size ?? 0) - (b.size - (remoteByPath.get(b.path)?.size ?? 0))
|
|
3018
|
+
);
|
|
3019
|
+
for (const resource of uploads) await uploadSkillResource(api, skill.id, resource);
|
|
3020
|
+
}
|
|
2817
3021
|
async function applySkills(api, localSkills, projectId, appName) {
|
|
2818
3022
|
if (localSkills.length === 0) return 0;
|
|
2819
3023
|
validateProjectSkillStorageSlugs(localSkills, appName);
|
|
@@ -2824,21 +3028,19 @@ async function applySkills(api, localSkills, projectId, appName) {
|
|
|
2824
3028
|
for (const skill of localSkills) {
|
|
2825
3029
|
const remote = remoteBySlug.get(skill.slug);
|
|
2826
3030
|
try {
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
console.log(pc2.green(" \u2713"), `${skill.name} (created)`);
|
|
2841
|
-
}
|
|
3031
|
+
const saved = remote ? await api.updateAgentSkill(remote.id, {
|
|
3032
|
+
name: skill.name,
|
|
3033
|
+
summary: skill.summary,
|
|
3034
|
+
instructions: skill.instructions
|
|
3035
|
+
}) : await api.createAgentSkill({
|
|
3036
|
+
name: skill.name,
|
|
3037
|
+
summary: skill.summary,
|
|
3038
|
+
instructions: skill.instructions,
|
|
3039
|
+
slug: scopedProjectSkillSlug(appName, skill.slug),
|
|
3040
|
+
project_id: scopedProjectID
|
|
3041
|
+
});
|
|
3042
|
+
await reconcileSkillResources(api, saved, skill.resources);
|
|
3043
|
+
console.log(pc2.green(" \u2713"), `${skill.name} (${remote ? "updated" : "created"})`);
|
|
2842
3044
|
} catch (error3) {
|
|
2843
3045
|
console.log(pc2.red(" \u2717"), `${skill.name}: ${error3}`);
|
|
2844
3046
|
errors++;
|
|
@@ -2846,6 +3048,54 @@ async function applySkills(api, localSkills, projectId, appName) {
|
|
|
2846
3048
|
}
|
|
2847
3049
|
return errors;
|
|
2848
3050
|
}
|
|
3051
|
+
function validateRemoteSkillResources(resources) {
|
|
3052
|
+
if (resources.length > SKILL_RESOURCE_MAX_COUNT) throw new Error(`remote skill has more than ${SKILL_RESOURCE_MAX_COUNT} resources`);
|
|
3053
|
+
const paths = [];
|
|
3054
|
+
let totalBytes = 0;
|
|
3055
|
+
for (const resource of resources) {
|
|
3056
|
+
const pathError = skillResourcePathError(resource.path);
|
|
3057
|
+
if (pathError) throw new Error(`remote resource ${resource.path}: ${pathError}`);
|
|
3058
|
+
const conflict = paths.find((path) => skillResourcePathsCollide(path, resource.path));
|
|
3059
|
+
if (conflict) throw new Error(`remote resource paths conflict: ${conflict} and ${resource.path}`);
|
|
3060
|
+
if (!Number.isSafeInteger(resource.size) || resource.size < 0 || resource.size > SKILL_RESOURCE_MAX_BYTES) {
|
|
3061
|
+
throw new Error(`remote resource ${resource.path} has an invalid size`);
|
|
3062
|
+
}
|
|
3063
|
+
if (!/^[a-f0-9]{64}$/.test(resource.sha256)) throw new Error(`remote resource ${resource.path} has an invalid checksum`);
|
|
3064
|
+
paths.push(resource.path);
|
|
3065
|
+
totalBytes += resource.size;
|
|
3066
|
+
}
|
|
3067
|
+
if (totalBytes > SKILL_RESOURCES_MAX_TOTAL_BYTES) throw new Error("remote skill resources exceed 100 MiB");
|
|
3068
|
+
}
|
|
3069
|
+
async function downloadSkillResource(resource, skillDir) {
|
|
3070
|
+
if (!resource.url) throw new Error(`${resource.path}: download URL is unavailable`);
|
|
3071
|
+
let response;
|
|
3072
|
+
try {
|
|
3073
|
+
response = await fetchWithRetry(resource.url);
|
|
3074
|
+
} catch {
|
|
3075
|
+
throw new Error(`${resource.path}: download failed`);
|
|
3076
|
+
}
|
|
3077
|
+
if (!response.ok) throw new Error(`${resource.path}: download failed with HTTP ${response.status}`);
|
|
3078
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
3079
|
+
if (bytes.byteLength !== resource.size) throw new Error(`${resource.path}: downloaded size does not match`);
|
|
3080
|
+
if (createHash("sha256").update(bytes).digest("hex") !== resource.sha256) {
|
|
3081
|
+
throw new Error(`${resource.path}: downloaded checksum does not match`);
|
|
3082
|
+
}
|
|
3083
|
+
const destination = join2(skillDir, ...resource.path.split("/"));
|
|
3084
|
+
mkdirSync(dirname2(destination), { recursive: true });
|
|
3085
|
+
writeFileSync(destination, bytes);
|
|
3086
|
+
}
|
|
3087
|
+
function replaceSkillDirectory(stagingDir, skillDir) {
|
|
3088
|
+
const backupDir = `${stagingDir}.previous`;
|
|
3089
|
+
const hadExisting = existsSync2(skillDir);
|
|
3090
|
+
if (hadExisting) renameSync(skillDir, backupDir);
|
|
3091
|
+
try {
|
|
3092
|
+
renameSync(stagingDir, skillDir);
|
|
3093
|
+
} catch (error3) {
|
|
3094
|
+
if (hadExisting && !existsSync2(skillDir)) renameSync(backupDir, skillDir);
|
|
3095
|
+
throw error3;
|
|
3096
|
+
}
|
|
3097
|
+
if (hadExisting) rmSync(backupDir, { recursive: true, force: true });
|
|
3098
|
+
}
|
|
2849
3099
|
async function pullSkills(api, platformDir, filterName, projectId, appName) {
|
|
2850
3100
|
if (!projectId) {
|
|
2851
3101
|
console.log(pc2.dim(" Skipped \u2014 project not registered (run `lumera register`)"));
|
|
@@ -2857,24 +3107,34 @@ async function pullSkills(api, platformDir, filterName, projectId, appName) {
|
|
|
2857
3107
|
await api.listAgentSkills({ project_id: scopedProjectID }),
|
|
2858
3108
|
appName
|
|
2859
3109
|
);
|
|
2860
|
-
|
|
3110
|
+
const resolvedFilter = resolveProjectSkillLocalSlug(remoteSkills, filterName);
|
|
3111
|
+
for (const [localSlug, listedSkill] of remoteSkills) {
|
|
2861
3112
|
if (!SKILL_SLUG_RE.test(localSlug)) {
|
|
2862
|
-
console.log(pc2.yellow(` \u26A0 Skipping ${
|
|
3113
|
+
console.log(pc2.yellow(` \u26A0 Skipping ${listedSkill.name}: skill slug ${listedSkill.slug} cannot be represented in platform/skills`));
|
|
2863
3114
|
continue;
|
|
2864
3115
|
}
|
|
2865
|
-
if (
|
|
3116
|
+
if (resolvedFilter && localSlug !== resolvedFilter) continue;
|
|
3117
|
+
const skill = await api.getAgentSkill(listedSkill.id, { resource_urls: "attachment" });
|
|
2866
3118
|
if (!(skill.summary || "").trim() || !(skill.instructions || "").trim()) {
|
|
2867
3119
|
console.log(pc2.yellow(` \u26A0 Skipping ${skill.name}: source-managed skills require both summary and instructions`));
|
|
2868
3120
|
continue;
|
|
2869
3121
|
}
|
|
3122
|
+
const resources = [...skill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
|
|
3123
|
+
validateRemoteSkillResources(resources);
|
|
2870
3124
|
const skillDir = join2(skillsDir, localSlug);
|
|
2871
|
-
mkdirSync(
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
3125
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
3126
|
+
const stagingDir = mkdtempSync(join2(platformDir, `.skill-${localSlug}-pull-`));
|
|
3127
|
+
try {
|
|
3128
|
+
writeFileSync(join2(stagingDir, "SKILL.md"), skillMarkdown({
|
|
3129
|
+
name: skill.name,
|
|
3130
|
+
summary: skill.summary || "",
|
|
3131
|
+
instructions: skill.instructions || ""
|
|
3132
|
+
}));
|
|
3133
|
+
for (const resource of resources) await downloadSkillResource(resource, stagingDir);
|
|
3134
|
+
replaceSkillDirectory(stagingDir, skillDir);
|
|
3135
|
+
} finally {
|
|
3136
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
3137
|
+
}
|
|
2878
3138
|
console.log(pc2.green(" \u2713"), `${skill.name} \u2192 skills/${localSlug}/`);
|
|
2879
3139
|
}
|
|
2880
3140
|
}
|
|
@@ -2928,14 +3188,6 @@ function loadLocalAgents(platformDir, filterName, appName) {
|
|
|
2928
3188
|
continue;
|
|
2929
3189
|
}
|
|
2930
3190
|
}
|
|
2931
|
-
if (config.allowed_user_tags !== void 0) {
|
|
2932
|
-
const tags = config.allowed_user_tags;
|
|
2933
|
-
if (!Array.isArray(tags) || tags.length > 100 || tags.some((tag) => typeof tag !== "string" || !tag || Buffer.byteLength(tag) > 64 || tag !== tag.trim() || tag !== tag.toLowerCase())) {
|
|
2934
|
-
errors.push(`${entry.name}: allowed_user_tags must be an array of at most 100 canonical tag keys (1\u201364 bytes each)`);
|
|
2935
|
-
continue;
|
|
2936
|
-
}
|
|
2937
|
-
config.allowed_user_tags = [...new Set(tags)].sort();
|
|
2938
|
-
}
|
|
2939
3191
|
let systemPrompt = readFileSync2(promptPath, "utf-8");
|
|
2940
3192
|
let policyScript = existsSync2(policyPath) ? readFileSync2(policyPath, "utf-8") : "";
|
|
2941
3193
|
if (appName) {
|
|
@@ -2956,9 +3208,6 @@ function loadLocalAgents(platformDir, filterName, appName) {
|
|
|
2956
3208
|
}
|
|
2957
3209
|
return agents;
|
|
2958
3210
|
}
|
|
2959
|
-
function allowedUserTagsChanged(local, remote) {
|
|
2960
|
-
return local.allowed_user_tags !== void 0 && JSON.stringify([...new Set(local.allowed_user_tags)].sort()) !== JSON.stringify([...new Set(remote.allowed_user_tags ?? [])].sort());
|
|
2961
|
-
}
|
|
2962
3211
|
async function planAgents(api, localAgents, projectId, localSourceSkillSlugs = /* @__PURE__ */ new Set(), pendingLocalSkillSlugs = /* @__PURE__ */ new Set(), appName) {
|
|
2963
3212
|
const changes = [];
|
|
2964
3213
|
let validationErrors = 0;
|
|
@@ -3011,7 +3260,6 @@ async function planAgents(api, localAgents, projectId, localSourceSkillSlugs = /
|
|
|
3011
3260
|
if ((remote.system_prompt || "").trim() !== systemPrompt.trim()) diffs.push("system_prompt");
|
|
3012
3261
|
if ((remote.model || "") !== (agent.model || "")) diffs.push("model");
|
|
3013
3262
|
if (agent.thinking_level !== void 0 && (remote.thinking_level || "") !== agent.thinking_level) diffs.push("thinking_level");
|
|
3014
|
-
if (allowedUserTagsChanged(agent, remote)) diffs.push("allowed_user_tags");
|
|
3015
3263
|
if ((remote.idle_reap_timeout_seconds ?? 0) !== (agent.idle_reap_timeout_seconds ?? 0)) diffs.push("idle_reap_timeout_seconds");
|
|
3016
3264
|
if ((remote.policy_script || "").trim() !== (policyScript || "").trim()) diffs.push("policy_script");
|
|
3017
3265
|
if ((remote.policy_enabled || false) !== (agent.policy_enabled || false)) diffs.push("policy_enabled");
|
|
@@ -3115,7 +3363,6 @@ async function applyAgents(api, localAgents, projectId, localSourceSkillSlugs =
|
|
|
3115
3363
|
system_prompt: systemPrompt,
|
|
3116
3364
|
model: agent.model || "",
|
|
3117
3365
|
...agent.thinking_level !== void 0 ? { thinking_level: agent.thinking_level } : {},
|
|
3118
|
-
...agent.allowed_user_tags !== void 0 ? { allowed_user_tags: agent.allowed_user_tags } : {},
|
|
3119
3366
|
idle_reap_timeout_seconds: agent.idle_reap_timeout_seconds ?? 0,
|
|
3120
3367
|
skill_ids: skillIds,
|
|
3121
3368
|
policy_script: policyScript || "",
|
|
@@ -3177,7 +3424,6 @@ async function pullAgents(api, platformDir, filterName, projectId, appName) {
|
|
|
3177
3424
|
if (agent.description) config.description = agent.description;
|
|
3178
3425
|
if (agent.model) config.model = agent.model;
|
|
3179
3426
|
if (agent.thinking_level) config.thinking_level = agent.thinking_level;
|
|
3180
|
-
if (agent.allowed_user_tags != null) config.allowed_user_tags = agent.allowed_user_tags;
|
|
3181
3427
|
if (agent.idle_reap_timeout_seconds) config.idle_reap_timeout_seconds = agent.idle_reap_timeout_seconds;
|
|
3182
3428
|
if (skillSlugs.length > 0) config.skills = skillSlugs;
|
|
3183
3429
|
if (agent.policy_enabled) config.policy_enabled = true;
|
|
@@ -3314,16 +3560,21 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
|
|
|
3314
3560
|
for (const skill of localSkills) {
|
|
3315
3561
|
const remote = remoteBySlug.get(skill.slug);
|
|
3316
3562
|
if (!remote) {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
results.push({ name: skill.name, type: "skills", status: "changed", details: "SKILL.md" });
|
|
3563
|
+
const resourceChanges = skillResourceChangesSummary(compareSkillResources(skill.resources, []));
|
|
3564
|
+
results.push(resourceChanges ? { name: skill.name, type: "skills", status: "local-only", details: `resources (${resourceChanges})` } : { name: skill.name, type: "skills", status: "local-only" });
|
|
3320
3565
|
} else {
|
|
3321
|
-
|
|
3566
|
+
const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
|
|
3567
|
+
const details = [
|
|
3568
|
+
remote.name !== skill.name || (remote.summary || "").trim() !== skill.summary.trim() || (remote.instructions || "").trim() !== skill.instructions.trim() ? "SKILL.md" : "",
|
|
3569
|
+
skillResourceChangesEmpty(resourceChanges) ? "" : `resources (${skillResourceChangesSummary(resourceChanges)})`
|
|
3570
|
+
].filter(Boolean).join(", ");
|
|
3571
|
+
results.push(details ? { name: skill.name, type: "skills", status: "changed", details } : { name: skill.name, type: "skills", status: "synced" });
|
|
3322
3572
|
}
|
|
3323
3573
|
}
|
|
3324
3574
|
for (const remote of remoteSkills) {
|
|
3325
3575
|
if (!localSlugs.has(localProjectSkillSlug(appName, remote.slug))) {
|
|
3326
|
-
|
|
3576
|
+
const resourceChanges = skillResourceChangesSummary(compareSkillResources([], remote.resources ?? []));
|
|
3577
|
+
results.push(resourceChanges ? { name: remote.name, type: "skills", status: "remote-only", details: `resources (${resourceChanges})` } : { name: remote.name, type: "skills", status: "remote-only" });
|
|
3327
3578
|
}
|
|
3328
3579
|
}
|
|
3329
3580
|
}
|
|
@@ -3825,8 +4076,12 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
|
|
|
3825
4076
|
console.log(pc2.bold(` Skill: ${local?.name || remote?.name}`));
|
|
3826
4077
|
console.log();
|
|
3827
4078
|
if (local && remote) {
|
|
3828
|
-
const
|
|
4079
|
+
const resourceChanges = compareSkillResources(local.resources, remote.resources ?? []);
|
|
4080
|
+
const changed = local.name !== remote.name || local.summary.trim() !== (remote.summary || "").trim() || local.instructions.trim() !== (remote.instructions || "").trim() || !skillResourceChangesEmpty(resourceChanges);
|
|
3829
4081
|
console.log(` Status: ${changed ? pc2.yellow("changed") : pc2.green("synced")}`);
|
|
4082
|
+
if (!skillResourceChangesEmpty(resourceChanges)) {
|
|
4083
|
+
console.log(` Resources: ${skillResourceChangesSummary(resourceChanges)}`);
|
|
4084
|
+
}
|
|
3830
4085
|
} else if (local) {
|
|
3831
4086
|
console.log(` Status: ${pc2.yellow("local only")}`);
|
|
3832
4087
|
} else {
|
|
@@ -3835,6 +4090,8 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
|
|
|
3835
4090
|
console.log(` Slug: ${local?.slug || (remote ? localProjectSkillSlug(appName, remote.slug) : "")}`);
|
|
3836
4091
|
const summary = local?.summary || remote?.summary;
|
|
3837
4092
|
if (summary) console.log(` Summary: ${summary}`);
|
|
4093
|
+
const resourcePaths = (local?.resources ?? remote?.resources ?? []).map((resource) => resource.path).sort();
|
|
4094
|
+
if (resourcePaths.length > 0) console.log(` Resource files: ${resourcePaths.join(", ")}`);
|
|
3838
4095
|
console.log();
|
|
3839
4096
|
} else if (resourceType === "mailboxes") {
|
|
3840
4097
|
const localMailboxes = loadLocalMailboxes(platformDir, resourceName);
|
|
@@ -4278,11 +4535,22 @@ async function pull(args) {
|
|
|
4278
4535
|
collections = /* @__PURE__ */ new Map();
|
|
4279
4536
|
}
|
|
4280
4537
|
if (projectId && (!type || type === "skills")) {
|
|
4281
|
-
const
|
|
4538
|
+
const remoteBySlug = remoteSkillsByLocalSlug(
|
|
4539
|
+
await api.listAgentSkills({ project_id: projectId }),
|
|
4540
|
+
appName
|
|
4541
|
+
);
|
|
4542
|
+
const localSkills = loadLocalSkills(
|
|
4543
|
+
platformDir,
|
|
4544
|
+
resolveProjectSkillLocalSlug(remoteBySlug, name || void 0)
|
|
4545
|
+
);
|
|
4282
4546
|
if (localSkills.length > 0) {
|
|
4283
|
-
const
|
|
4284
|
-
|
|
4285
|
-
if (
|
|
4547
|
+
for (const skill of localSkills) {
|
|
4548
|
+
const remote = remoteBySlug.get(skill.slug);
|
|
4549
|
+
if (!remote) continue;
|
|
4550
|
+
const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
|
|
4551
|
+
if (remote.name !== skill.name || (remote.summary || "").trim() !== skill.summary.trim() || (remote.instructions || "").trim() !== skill.instructions.trim() || resourceChanges.added.length > 0 || resourceChanges.replaced.length > 0) {
|
|
4552
|
+
conflicts.push(`skills/${skill.slug}`);
|
|
4553
|
+
}
|
|
4286
4554
|
}
|
|
4287
4555
|
}
|
|
4288
4556
|
}
|
|
@@ -4517,7 +4785,7 @@ ${pc2.dim("Usage:")}
|
|
|
4517
4785
|
|
|
4518
4786
|
${pc2.dim("Resources:")}
|
|
4519
4787
|
agents/<name> Diff agent (system_prompt, policy_script)
|
|
4520
|
-
skills/<slug> Diff project
|
|
4788
|
+
skills/<slug> Diff project SKILL.md and resource paths
|
|
4521
4789
|
automations/<name> Diff automation code
|
|
4522
4790
|
hooks/<name> Diff hook script
|
|
4523
4791
|
|
|
@@ -4597,11 +4865,7 @@ async function diff(args) {
|
|
|
4597
4865
|
const promptChanged = (remote.system_prompt || "").trim() !== local.systemPrompt.trim();
|
|
4598
4866
|
const policyChanged = (remote.policy_script || "").trim() !== (local.policyScript || "").trim();
|
|
4599
4867
|
const thinkingLevelChanged = local.agent.thinking_level !== void 0 && (remote.thinking_level || "") !== local.agent.thinking_level;
|
|
4600
|
-
|
|
4601
|
-
if (userTagsChanged) {
|
|
4602
|
-
console.log(` allowed_user_tags: ${pc2.red(JSON.stringify(remote.allowed_user_tags ?? []))} \u2192 ${pc2.green(JSON.stringify(local.agent.allowed_user_tags))}`);
|
|
4603
|
-
}
|
|
4604
|
-
if (!promptChanged && !policyChanged && !thinkingLevelChanged && !userTagsChanged && remote.name === local.agent.name && (remote.description || "") === (local.agent.description || "") && (remote.model || "") === (local.agent.model || "") && (remote.idle_reap_timeout_seconds ?? 0) === (local.agent.idle_reap_timeout_seconds ?? 0) && (remote.policy_enabled || false) === (local.agent.policy_enabled || false)) {
|
|
4868
|
+
if (!promptChanged && !policyChanged && !thinkingLevelChanged && remote.name === local.agent.name && (remote.description || "") === (local.agent.description || "") && (remote.model || "") === (local.agent.model || "") && (remote.idle_reap_timeout_seconds ?? 0) === (local.agent.idle_reap_timeout_seconds ?? 0) && (remote.policy_enabled || false) === (local.agent.policy_enabled || false)) {
|
|
4605
4869
|
console.log(pc2.green(` \u2713 No changes`));
|
|
4606
4870
|
} else {
|
|
4607
4871
|
if (promptChanged) renderFullDiff("system_prompt.md", remote.system_prompt || "", local.systemPrompt);
|
|
@@ -4697,16 +4961,22 @@ async function diff(args) {
|
|
|
4697
4961
|
console.log(` summary: ${pc2.red(remote.summary || "(empty)")} \u2192 ${pc2.green(local.summary)}`);
|
|
4698
4962
|
}
|
|
4699
4963
|
const remoteMarkdown = skillMarkdown({
|
|
4700
|
-
slug: localProjectSkillSlug(appName, remote.slug),
|
|
4701
4964
|
name: remote.name,
|
|
4702
4965
|
summary: remote.summary || "",
|
|
4703
4966
|
instructions: remote.instructions || ""
|
|
4704
4967
|
});
|
|
4705
4968
|
const localMarkdown = skillMarkdown(local);
|
|
4706
|
-
|
|
4969
|
+
const resourceChanges = compareSkillResources(local.resources, remote.resources ?? []);
|
|
4970
|
+
if (remoteMarkdown === localMarkdown && skillResourceChangesEmpty(resourceChanges)) {
|
|
4707
4971
|
console.log(pc2.green(" \u2713 No changes"));
|
|
4708
4972
|
} else {
|
|
4709
|
-
renderFullDiff("SKILL.md", remoteMarkdown, localMarkdown);
|
|
4973
|
+
if (remoteMarkdown !== localMarkdown) renderFullDiff("SKILL.md", remoteMarkdown, localMarkdown);
|
|
4974
|
+
if (!skillResourceChangesEmpty(resourceChanges)) {
|
|
4975
|
+
console.log(pc2.bold(" Resources:"));
|
|
4976
|
+
for (const path of resourceChanges.added) console.log(pc2.green(` + ${path}`));
|
|
4977
|
+
for (const path of resourceChanges.replaced) console.log(pc2.yellow(` ~ ${path}`));
|
|
4978
|
+
for (const path of resourceChanges.deleted) console.log(pc2.red(` - ${path}`));
|
|
4979
|
+
}
|
|
4710
4980
|
}
|
|
4711
4981
|
} else {
|
|
4712
4982
|
console.log(pc2.red(` Diff not supported for "${type}" \u2014 use agents, skills, automations, or hooks`));
|
|
@@ -4720,11 +4990,11 @@ export {
|
|
|
4720
4990
|
applyAutomations,
|
|
4721
4991
|
applyCollections,
|
|
4722
4992
|
applySkills,
|
|
4993
|
+
compareSkillResources,
|
|
4723
4994
|
convertCollectionToApiFormat,
|
|
4724
4995
|
destroy,
|
|
4725
4996
|
diff,
|
|
4726
4997
|
list,
|
|
4727
|
-
loadLocalAgents,
|
|
4728
4998
|
loadLocalCollections,
|
|
4729
4999
|
loadLocalSkills,
|
|
4730
5000
|
localProjectSkillSlug,
|
|
@@ -4734,10 +5004,10 @@ export {
|
|
|
4734
5004
|
planSkills,
|
|
4735
5005
|
projectSkillRefCandidates,
|
|
4736
5006
|
pull,
|
|
4737
|
-
pullAgents,
|
|
4738
5007
|
pullCollections,
|
|
4739
5008
|
pullSkills,
|
|
4740
5009
|
resolveAgentSkillRefs,
|
|
5010
|
+
resolveProjectSkillLocalSlug,
|
|
4741
5011
|
scopedProjectSkillSlug,
|
|
4742
5012
|
show
|
|
4743
5013
|
};
|