@lumerahq/cli 0.30.20 → 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-BRUU7JY3.js → chunk-HS63O4MH.js} +1 -1
- package/dist/{chunk-HFNPBTT3.js → chunk-IVII7OZZ.js} +1 -1
- package/dist/{chunk-3JDRRFNT.js → chunk-ZJ7M7JD3.js} +24 -0
- package/dist/{deps-GL2XDDPO.js → deps-SIZ77NYE.js} +2 -2
- package/dist/{dev-H6NSDTAA.js → dev-7ZQRZJXX.js} +3 -3
- package/dist/index.js +12 -12
- package/dist/{init-54CLJUYA.js → init-WE722M4X.js} +1 -1
- package/dist/{register-FNG4ZVDW.js → register-ZXNNU3NJ.js} +1 -1
- package/dist/{resources-3MNMQI3N.js → resources-GM2OWDKP.js} +337 -48
- package/dist/{run-J3XKXSV7.js → run-7KJPBOIY.js} +1 -1
- package/package.json +1 -1
|
@@ -327,6 +327,12 @@ var ApiClient = class {
|
|
|
327
327
|
}
|
|
328
328
|
return all;
|
|
329
329
|
}
|
|
330
|
+
async getAgentSkill(id, opts) {
|
|
331
|
+
const params = new URLSearchParams();
|
|
332
|
+
if (opts?.resource_urls) params.set("resource_urls", opts.resource_urls);
|
|
333
|
+
const query = params.toString();
|
|
334
|
+
return this.request(`/api/lm_agent_skills/${encodeURIComponent(id)}${query ? `?${query}` : ""}`);
|
|
335
|
+
}
|
|
330
336
|
async createAgentSkill(def) {
|
|
331
337
|
return this.request("/api/lm_agent_skills", {
|
|
332
338
|
method: "POST",
|
|
@@ -342,6 +348,24 @@ var ApiClient = class {
|
|
|
342
348
|
async deleteAgentSkill(id) {
|
|
343
349
|
await this.request(`/api/lm_agent_skills/${id}`, { method: "DELETE" });
|
|
344
350
|
}
|
|
351
|
+
async startAgentSkillResourceUpload(id, resource) {
|
|
352
|
+
return this.request(`/api/lm_agent_skills/${encodeURIComponent(id)}/resource-uploads`, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
body: JSON.stringify(resource)
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async completeAgentSkillResourceUpload(id, uploadId, resource) {
|
|
358
|
+
return this.request(`/api/lm_agent_skills/${encodeURIComponent(id)}/resource-uploads/${encodeURIComponent(uploadId)}/complete`, {
|
|
359
|
+
method: "POST",
|
|
360
|
+
body: JSON.stringify(resource)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
async deleteAgentSkillResource(id, path) {
|
|
364
|
+
return this.request(`/api/lm_agent_skills/${encodeURIComponent(id)}/resources`, {
|
|
365
|
+
method: "PATCH",
|
|
366
|
+
body: JSON.stringify({ operation: "delete", path })
|
|
367
|
+
});
|
|
368
|
+
}
|
|
345
369
|
// Resolve a list of refs (slugs or IDs) to skill records. Chunks
|
|
346
370
|
// requests so callers don't have to think about the server-side
|
|
347
371
|
// per-request cap.
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
deps,
|
|
3
3
|
syncDeps,
|
|
4
4
|
syncResourceShareDeps
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-HS63O4MH.js";
|
|
6
6
|
import "./chunk-2CR762KB.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-ZJ7M7JD3.js";
|
|
8
8
|
import "./chunk-JLVVHTBY.js";
|
|
9
9
|
import "./chunk-FJFIWC7G.js";
|
|
10
10
|
import "./chunk-PNKVD2UK.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dev
|
|
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";
|
|
10
10
|
import {
|
|
11
11
|
createApiClient
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ZJ7M7JD3.js";
|
|
13
13
|
import {
|
|
14
14
|
findProjectRoot,
|
|
15
15
|
getApiUrl,
|
package/dist/index.js
CHANGED
|
@@ -240,32 +240,32 @@ async function main() {
|
|
|
240
240
|
switch (command) {
|
|
241
241
|
// Resource commands
|
|
242
242
|
case "plan":
|
|
243
|
-
await import("./resources-
|
|
243
|
+
await import("./resources-GM2OWDKP.js").then((m) => m.plan(args.slice(1)));
|
|
244
244
|
break;
|
|
245
245
|
case "apply":
|
|
246
|
-
await import("./resources-
|
|
246
|
+
await import("./resources-GM2OWDKP.js").then((m) => m.apply(args.slice(1)));
|
|
247
247
|
break;
|
|
248
248
|
case "pull":
|
|
249
|
-
await import("./resources-
|
|
249
|
+
await import("./resources-GM2OWDKP.js").then((m) => m.pull(args.slice(1)));
|
|
250
250
|
break;
|
|
251
251
|
case "destroy":
|
|
252
|
-
await import("./resources-
|
|
252
|
+
await import("./resources-GM2OWDKP.js").then((m) => m.destroy(args.slice(1)));
|
|
253
253
|
break;
|
|
254
254
|
case "list":
|
|
255
|
-
await import("./resources-
|
|
255
|
+
await import("./resources-GM2OWDKP.js").then((m) => m.list(args.slice(1)));
|
|
256
256
|
break;
|
|
257
257
|
case "show":
|
|
258
|
-
await import("./resources-
|
|
258
|
+
await import("./resources-GM2OWDKP.js").then((m) => m.show(args.slice(1)));
|
|
259
259
|
break;
|
|
260
260
|
case "diff":
|
|
261
|
-
await import("./resources-
|
|
261
|
+
await import("./resources-GM2OWDKP.js").then((m) => m.diff(args.slice(1)));
|
|
262
262
|
break;
|
|
263
263
|
// Development
|
|
264
264
|
case "dev":
|
|
265
|
-
await import("./dev-
|
|
265
|
+
await import("./dev-7ZQRZJXX.js").then((m) => m.dev(args.slice(1)));
|
|
266
266
|
break;
|
|
267
267
|
case "run":
|
|
268
|
-
await import("./run-
|
|
268
|
+
await import("./run-7KJPBOIY.js").then((m) => m.run(args.slice(1)));
|
|
269
269
|
break;
|
|
270
270
|
case "functions":
|
|
271
271
|
await import("./functions-WPHGKE54.js").then(
|
|
@@ -279,10 +279,10 @@ async function main() {
|
|
|
279
279
|
break;
|
|
280
280
|
// Project
|
|
281
281
|
case "init":
|
|
282
|
-
await import("./init-
|
|
282
|
+
await import("./init-WE722M4X.js").then((m) => m.init(args.slice(1)));
|
|
283
283
|
break;
|
|
284
284
|
case "register":
|
|
285
|
-
await import("./register-
|
|
285
|
+
await import("./register-ZXNNU3NJ.js").then((m) => m.register(args.slice(1)));
|
|
286
286
|
break;
|
|
287
287
|
case "templates":
|
|
288
288
|
await import("./templates-LNUOTNLN.js").then((m) => m.templates(subcommand, args.slice(2)));
|
|
@@ -299,7 +299,7 @@ async function main() {
|
|
|
299
299
|
break;
|
|
300
300
|
// Dependencies
|
|
301
301
|
case "deps":
|
|
302
|
-
await import("./deps-
|
|
302
|
+
await import("./deps-SIZ77NYE.js").then((m) => m.deps(args.slice(1)));
|
|
303
303
|
break;
|
|
304
304
|
// Feature flags (read the in-sandbox snapshot)
|
|
305
305
|
case "flags":
|
|
@@ -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";
|
|
@@ -2643,6 +2644,130 @@ var SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
2643
2644
|
var SKILL_NAME_MAX_LENGTH = 100;
|
|
2644
2645
|
var SKILL_SLUG_MAX_LENGTH = 100;
|
|
2645
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
|
+
}
|
|
2646
2771
|
function loadLocalSkills(platformDir, filterName) {
|
|
2647
2772
|
const skillsDir = join2(platformDir, "skills");
|
|
2648
2773
|
if (!existsSync2(skillsDir)) return [];
|
|
@@ -2655,11 +2780,19 @@ function loadLocalSkills(platformDir, filterName) {
|
|
|
2655
2780
|
errors.push(`${entry.name}: directory name must be a lowercase slug`);
|
|
2656
2781
|
continue;
|
|
2657
2782
|
}
|
|
2658
|
-
const
|
|
2783
|
+
const skillDir = join2(skillsDir, entry.name);
|
|
2784
|
+
const path = join2(skillDir, "SKILL.md");
|
|
2659
2785
|
if (!existsSync2(path)) {
|
|
2660
2786
|
errors.push(`${entry.name}: missing SKILL.md`);
|
|
2661
2787
|
continue;
|
|
2662
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}`));
|
|
2663
2796
|
const content = readFileSync2(path, "utf-8");
|
|
2664
2797
|
const separator = /\r?\n---\r?\n/.exec(content);
|
|
2665
2798
|
const parts = separator ? [content.slice(0, separator.index), content.slice(separator.index + separator[0].length)] : [content];
|
|
@@ -2685,7 +2818,7 @@ function loadLocalSkills(platformDir, filterName) {
|
|
|
2685
2818
|
if (summary.length > SKILL_SUMMARY_MAX_LENGTH) errors.push(`${entry.name}: summary must be at most ${SKILL_SUMMARY_MAX_LENGTH} characters`);
|
|
2686
2819
|
if (!instructions) errors.push(`${entry.name}: SKILL.md must include instructions after ---`);
|
|
2687
2820
|
if (name && name.length <= SKILL_NAME_MAX_LENGTH && summary && summary.length <= SKILL_SUMMARY_MAX_LENGTH && instructions) {
|
|
2688
|
-
skills.push({ slug: entry.name, name, summary, instructions });
|
|
2821
|
+
skills.push({ slug: entry.name, name, summary, instructions, resources });
|
|
2689
2822
|
}
|
|
2690
2823
|
}
|
|
2691
2824
|
if (errors.length > 0) {
|
|
@@ -2729,6 +2862,13 @@ function remoteSkillsByLocalSlug(remoteSkills, appName) {
|
|
|
2729
2862
|
}
|
|
2730
2863
|
return byLocalSlug;
|
|
2731
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
|
+
}
|
|
2732
2872
|
function projectSkillRefCandidates(refs, appName) {
|
|
2733
2873
|
return [...new Set(refs.flatMap((ref) => appName && SKILL_SLUG_RE.test(ref) ? [scopedProjectSkillSlug(appName, ref), ref] : [ref]))];
|
|
2734
2874
|
}
|
|
@@ -2795,13 +2935,24 @@ async function planSkills(api, localSkills, projectId, appName) {
|
|
|
2795
2935
|
for (const skill of localSkills) {
|
|
2796
2936
|
const remote = remoteBySlug.get(skill.slug);
|
|
2797
2937
|
if (!remote) {
|
|
2798
|
-
|
|
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
|
+
});
|
|
2799
2946
|
continue;
|
|
2800
2947
|
}
|
|
2801
2948
|
const changed = [];
|
|
2802
2949
|
if (remote.name !== skill.name) changed.push("name");
|
|
2803
2950
|
if ((remote.summary || "").trim() !== skill.summary.trim()) changed.push("summary");
|
|
2804
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
|
+
}
|
|
2805
2956
|
if (changed.length > 0) {
|
|
2806
2957
|
changes.push({
|
|
2807
2958
|
type: "update",
|
|
@@ -2809,12 +2960,64 @@ async function planSkills(api, localSkills, projectId, appName) {
|
|
|
2809
2960
|
id: skill.slug,
|
|
2810
2961
|
name: skill.name,
|
|
2811
2962
|
details: `changed: ${changed.join(", ")}`,
|
|
2812
|
-
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
|
|
2813
2964
|
});
|
|
2814
2965
|
}
|
|
2815
2966
|
}
|
|
2816
2967
|
return changes;
|
|
2817
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
|
+
}
|
|
2818
3021
|
async function applySkills(api, localSkills, projectId, appName) {
|
|
2819
3022
|
if (localSkills.length === 0) return 0;
|
|
2820
3023
|
validateProjectSkillStorageSlugs(localSkills, appName);
|
|
@@ -2825,21 +3028,19 @@ async function applySkills(api, localSkills, projectId, appName) {
|
|
|
2825
3028
|
for (const skill of localSkills) {
|
|
2826
3029
|
const remote = remoteBySlug.get(skill.slug);
|
|
2827
3030
|
try {
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
console.log(pc2.green(" \u2713"), `${skill.name} (created)`);
|
|
2842
|
-
}
|
|
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"})`);
|
|
2843
3044
|
} catch (error3) {
|
|
2844
3045
|
console.log(pc2.red(" \u2717"), `${skill.name}: ${error3}`);
|
|
2845
3046
|
errors++;
|
|
@@ -2847,6 +3048,54 @@ async function applySkills(api, localSkills, projectId, appName) {
|
|
|
2847
3048
|
}
|
|
2848
3049
|
return errors;
|
|
2849
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
|
+
}
|
|
2850
3099
|
async function pullSkills(api, platformDir, filterName, projectId, appName) {
|
|
2851
3100
|
if (!projectId) {
|
|
2852
3101
|
console.log(pc2.dim(" Skipped \u2014 project not registered (run `lumera register`)"));
|
|
@@ -2858,24 +3107,34 @@ async function pullSkills(api, platformDir, filterName, projectId, appName) {
|
|
|
2858
3107
|
await api.listAgentSkills({ project_id: scopedProjectID }),
|
|
2859
3108
|
appName
|
|
2860
3109
|
);
|
|
2861
|
-
|
|
3110
|
+
const resolvedFilter = resolveProjectSkillLocalSlug(remoteSkills, filterName);
|
|
3111
|
+
for (const [localSlug, listedSkill] of remoteSkills) {
|
|
2862
3112
|
if (!SKILL_SLUG_RE.test(localSlug)) {
|
|
2863
|
-
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`));
|
|
2864
3114
|
continue;
|
|
2865
3115
|
}
|
|
2866
|
-
if (
|
|
3116
|
+
if (resolvedFilter && localSlug !== resolvedFilter) continue;
|
|
3117
|
+
const skill = await api.getAgentSkill(listedSkill.id, { resource_urls: "attachment" });
|
|
2867
3118
|
if (!(skill.summary || "").trim() || !(skill.instructions || "").trim()) {
|
|
2868
3119
|
console.log(pc2.yellow(` \u26A0 Skipping ${skill.name}: source-managed skills require both summary and instructions`));
|
|
2869
3120
|
continue;
|
|
2870
3121
|
}
|
|
3122
|
+
const resources = [...skill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
|
|
3123
|
+
validateRemoteSkillResources(resources);
|
|
2871
3124
|
const skillDir = join2(skillsDir, localSlug);
|
|
2872
|
-
mkdirSync(
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
+
}
|
|
2879
3138
|
console.log(pc2.green(" \u2713"), `${skill.name} \u2192 skills/${localSlug}/`);
|
|
2880
3139
|
}
|
|
2881
3140
|
}
|
|
@@ -3301,16 +3560,21 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
|
|
|
3301
3560
|
for (const skill of localSkills) {
|
|
3302
3561
|
const remote = remoteBySlug.get(skill.slug);
|
|
3303
3562
|
if (!remote) {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
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" });
|
|
3307
3565
|
} else {
|
|
3308
|
-
|
|
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" });
|
|
3309
3572
|
}
|
|
3310
3573
|
}
|
|
3311
3574
|
for (const remote of remoteSkills) {
|
|
3312
3575
|
if (!localSlugs.has(localProjectSkillSlug(appName, remote.slug))) {
|
|
3313
|
-
|
|
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" });
|
|
3314
3578
|
}
|
|
3315
3579
|
}
|
|
3316
3580
|
}
|
|
@@ -3812,8 +4076,12 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
|
|
|
3812
4076
|
console.log(pc2.bold(` Skill: ${local?.name || remote?.name}`));
|
|
3813
4077
|
console.log();
|
|
3814
4078
|
if (local && remote) {
|
|
3815
|
-
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);
|
|
3816
4081
|
console.log(` Status: ${changed ? pc2.yellow("changed") : pc2.green("synced")}`);
|
|
4082
|
+
if (!skillResourceChangesEmpty(resourceChanges)) {
|
|
4083
|
+
console.log(` Resources: ${skillResourceChangesSummary(resourceChanges)}`);
|
|
4084
|
+
}
|
|
3817
4085
|
} else if (local) {
|
|
3818
4086
|
console.log(` Status: ${pc2.yellow("local only")}`);
|
|
3819
4087
|
} else {
|
|
@@ -3822,6 +4090,8 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
|
|
|
3822
4090
|
console.log(` Slug: ${local?.slug || (remote ? localProjectSkillSlug(appName, remote.slug) : "")}`);
|
|
3823
4091
|
const summary = local?.summary || remote?.summary;
|
|
3824
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(", ")}`);
|
|
3825
4095
|
console.log();
|
|
3826
4096
|
} else if (resourceType === "mailboxes") {
|
|
3827
4097
|
const localMailboxes = loadLocalMailboxes(platformDir, resourceName);
|
|
@@ -4265,11 +4535,22 @@ async function pull(args) {
|
|
|
4265
4535
|
collections = /* @__PURE__ */ new Map();
|
|
4266
4536
|
}
|
|
4267
4537
|
if (projectId && (!type || type === "skills")) {
|
|
4268
|
-
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
|
+
);
|
|
4269
4546
|
if (localSkills.length > 0) {
|
|
4270
|
-
const
|
|
4271
|
-
|
|
4272
|
-
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
|
+
}
|
|
4273
4554
|
}
|
|
4274
4555
|
}
|
|
4275
4556
|
}
|
|
@@ -4504,7 +4785,7 @@ ${pc2.dim("Usage:")}
|
|
|
4504
4785
|
|
|
4505
4786
|
${pc2.dim("Resources:")}
|
|
4506
4787
|
agents/<name> Diff agent (system_prompt, policy_script)
|
|
4507
|
-
skills/<slug> Diff project
|
|
4788
|
+
skills/<slug> Diff project SKILL.md and resource paths
|
|
4508
4789
|
automations/<name> Diff automation code
|
|
4509
4790
|
hooks/<name> Diff hook script
|
|
4510
4791
|
|
|
@@ -4680,16 +4961,22 @@ async function diff(args) {
|
|
|
4680
4961
|
console.log(` summary: ${pc2.red(remote.summary || "(empty)")} \u2192 ${pc2.green(local.summary)}`);
|
|
4681
4962
|
}
|
|
4682
4963
|
const remoteMarkdown = skillMarkdown({
|
|
4683
|
-
slug: localProjectSkillSlug(appName, remote.slug),
|
|
4684
4964
|
name: remote.name,
|
|
4685
4965
|
summary: remote.summary || "",
|
|
4686
4966
|
instructions: remote.instructions || ""
|
|
4687
4967
|
});
|
|
4688
4968
|
const localMarkdown = skillMarkdown(local);
|
|
4689
|
-
|
|
4969
|
+
const resourceChanges = compareSkillResources(local.resources, remote.resources ?? []);
|
|
4970
|
+
if (remoteMarkdown === localMarkdown && skillResourceChangesEmpty(resourceChanges)) {
|
|
4690
4971
|
console.log(pc2.green(" \u2713 No changes"));
|
|
4691
4972
|
} else {
|
|
4692
|
-
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
|
+
}
|
|
4693
4980
|
}
|
|
4694
4981
|
} else {
|
|
4695
4982
|
console.log(pc2.red(` Diff not supported for "${type}" \u2014 use agents, skills, automations, or hooks`));
|
|
@@ -4703,6 +4990,7 @@ export {
|
|
|
4703
4990
|
applyAutomations,
|
|
4704
4991
|
applyCollections,
|
|
4705
4992
|
applySkills,
|
|
4993
|
+
compareSkillResources,
|
|
4706
4994
|
convertCollectionToApiFormat,
|
|
4707
4995
|
destroy,
|
|
4708
4996
|
diff,
|
|
@@ -4719,6 +5007,7 @@ export {
|
|
|
4719
5007
|
pullCollections,
|
|
4720
5008
|
pullSkills,
|
|
4721
5009
|
resolveAgentSkillRefs,
|
|
5010
|
+
resolveProjectSkillLocalSlug,
|
|
4722
5011
|
scopedProjectSkillSlug,
|
|
4723
5012
|
show
|
|
4724
5013
|
};
|