@lumerahq/cli 0.31.0-dev.0 → 0.31.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.
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-2K4ZYGCE.js";
3
+ } from "./chunk-IVII7OZZ.js";
4
4
  import {
5
5
  syncDeps
6
- } from "./chunk-ODANFLL7.js";
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-E7YZ6QS6.js";
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 { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
35
- import { join as join2, resolve } from "path";
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,133 @@ 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 foldSkillResourcePathPart(value) {
2665
+ return value.toUpperCase();
2666
+ }
2667
+ function skillResourcePathError(value) {
2668
+ if (!value || Buffer.byteLength(value) > SKILL_RESOURCE_MAX_PATH_BYTES) {
2669
+ return `path must be at most ${SKILL_RESOURCE_MAX_PATH_BYTES} bytes`;
2670
+ }
2671
+ if (value.startsWith("/") || value.endsWith("/") || value.includes("\\")) {
2672
+ return "path must be relative and slash-separated";
2673
+ }
2674
+ if (/\p{Cc}/u.test(value)) return "path cannot contain control characters";
2675
+ for (const part of value.split("/")) {
2676
+ if (!part || part === "." || part === "..") return "path cannot contain empty, dot, or parent segments";
2677
+ if (part !== part.trim()) return "path segments cannot start or end with whitespace";
2678
+ if (Buffer.byteLength(part) > SKILL_RESOURCE_MAX_PATH_PART_BYTES) {
2679
+ return `path segments must be at most ${SKILL_RESOURCE_MAX_PATH_PART_BYTES} bytes`;
2680
+ }
2681
+ if (foldSkillResourcePathPart(part) === "SKILL.MD") return "SKILL.md is reserved";
2682
+ }
2683
+ return null;
2684
+ }
2685
+ function skillResourcePathsCollide(left, right) {
2686
+ const leftParts = left.split("/");
2687
+ const rightParts = right.split("/");
2688
+ const limit = Math.min(leftParts.length, rightParts.length);
2689
+ for (let i = 0; i < limit; i++) {
2690
+ if (leftParts[i] === rightParts[i]) continue;
2691
+ return foldSkillResourcePathPart(leftParts[i] ?? "") === foldSkillResourcePathPart(rightParts[i] ?? "");
2692
+ }
2693
+ return true;
2694
+ }
2695
+ function localSkillResources(skillDir) {
2696
+ const resources = [];
2697
+ const errors = [];
2698
+ let totalBytes = 0;
2699
+ let fileCount = 0;
2700
+ const decoder = new TextDecoder("utf-8", { fatal: true });
2701
+ const scan = (directory, parentParts) => {
2702
+ const names = readdirSync2(directory, { encoding: "buffer" }).map((rawName) => {
2703
+ try {
2704
+ return decoder.decode(rawName);
2705
+ } catch {
2706
+ errors.push(`${parentParts.join("/") || "."}: file names must be valid UTF-8`);
2707
+ return null;
2708
+ }
2709
+ }).filter((name) => name !== null).sort();
2710
+ for (const name of names) {
2711
+ if (parentParts.length === 0 && name === "SKILL.md") continue;
2712
+ const parts = [...parentParts, name];
2713
+ const resourcePath = parts.join("/");
2714
+ const filePath = join2(directory, name);
2715
+ const stat = lstatSync(filePath);
2716
+ if (stat.isSymbolicLink()) {
2717
+ errors.push(`${resourcePath}: symlinks are not supported`);
2718
+ continue;
2719
+ }
2720
+ if (stat.isDirectory()) {
2721
+ scan(filePath, parts);
2722
+ continue;
2723
+ }
2724
+ if (!stat.isFile()) {
2725
+ errors.push(`${resourcePath}: only regular files are supported`);
2726
+ continue;
2727
+ }
2728
+ fileCount++;
2729
+ totalBytes += stat.size;
2730
+ const pathError = skillResourcePathError(resourcePath);
2731
+ if (pathError) {
2732
+ errors.push(`${resourcePath}: ${pathError}`);
2733
+ continue;
2734
+ }
2735
+ if (stat.size > SKILL_RESOURCE_MAX_BYTES) {
2736
+ errors.push(`${resourcePath}: file exceeds the 10 MiB limit`);
2737
+ continue;
2738
+ }
2739
+ const conflict = resources.find((resource) => skillResourcePathsCollide(resource.path, resourcePath));
2740
+ if (conflict) {
2741
+ errors.push(`${resourcePath}: path conflicts ignoring letter case with ${conflict.path}`);
2742
+ continue;
2743
+ }
2744
+ const sha256 = createHash("sha256").update(readFileSync2(filePath)).digest("hex");
2745
+ resources.push({ path: resourcePath, filePath, size: stat.size, sha256 });
2746
+ }
2747
+ };
2748
+ scan(skillDir, []);
2749
+ if (fileCount > SKILL_RESOURCE_MAX_COUNT) errors.push(`resources: a skill can have at most ${SKILL_RESOURCE_MAX_COUNT} files`);
2750
+ if (totalBytes > SKILL_RESOURCES_MAX_TOTAL_BYTES) errors.push("resources: files can total at most 100 MiB");
2751
+ return { resources: resources.sort((a, b) => a.path.localeCompare(b.path)), errors };
2752
+ }
2753
+ function compareSkillResources(local, remote) {
2754
+ const localByPath = new Map(local.map((resource) => [resource.path, resource]));
2755
+ const remoteByPath = new Map(remote.map((resource) => [resource.path, resource]));
2756
+ const added = [...localByPath.keys()].filter((path) => !remoteByPath.has(path)).sort();
2757
+ const replaced = [...localByPath].filter(([path, resource]) => {
2758
+ const current = remoteByPath.get(path);
2759
+ return current && (current.size !== resource.size || current.sha256 !== resource.sha256);
2760
+ }).map(([path]) => path).sort();
2761
+ const deleted = [...remoteByPath.keys()].filter((path) => !localByPath.has(path)).sort();
2762
+ return { added, replaced, deleted };
2763
+ }
2764
+ function skillResourceChangesEmpty(changes) {
2765
+ return changes.added.length === 0 && changes.replaced.length === 0 && changes.deleted.length === 0;
2766
+ }
2767
+ function skillResourceChangesSummary(changes) {
2768
+ return [
2769
+ ...changes.added.map((path) => `+${path}`),
2770
+ ...changes.replaced.map((path) => `~${path}`),
2771
+ ...changes.deleted.map((path) => `-${path}`)
2772
+ ].join(", ");
2773
+ }
2645
2774
  function loadLocalSkills(platformDir, filterName) {
2646
2775
  const skillsDir = join2(platformDir, "skills");
2647
2776
  if (!existsSync2(skillsDir)) return [];
@@ -2654,11 +2783,19 @@ function loadLocalSkills(platformDir, filterName) {
2654
2783
  errors.push(`${entry.name}: directory name must be a lowercase slug`);
2655
2784
  continue;
2656
2785
  }
2657
- const path = join2(skillsDir, entry.name, "SKILL.md");
2786
+ const skillDir = join2(skillsDir, entry.name);
2787
+ const path = join2(skillDir, "SKILL.md");
2658
2788
  if (!existsSync2(path)) {
2659
2789
  errors.push(`${entry.name}: missing SKILL.md`);
2660
2790
  continue;
2661
2791
  }
2792
+ const skillMarkdownStat = lstatSync(path);
2793
+ if (skillMarkdownStat.isSymbolicLink() || !skillMarkdownStat.isFile()) {
2794
+ errors.push(`${entry.name}: SKILL.md must be a regular file`);
2795
+ continue;
2796
+ }
2797
+ const { resources, errors: resourceErrors } = localSkillResources(skillDir);
2798
+ errors.push(...resourceErrors.map((error3) => `${entry.name}: ${error3}`));
2662
2799
  const content = readFileSync2(path, "utf-8");
2663
2800
  const separator = /\r?\n---\r?\n/.exec(content);
2664
2801
  const parts = separator ? [content.slice(0, separator.index), content.slice(separator.index + separator[0].length)] : [content];
@@ -2684,7 +2821,7 @@ function loadLocalSkills(platformDir, filterName) {
2684
2821
  if (summary.length > SKILL_SUMMARY_MAX_LENGTH) errors.push(`${entry.name}: summary must be at most ${SKILL_SUMMARY_MAX_LENGTH} characters`);
2685
2822
  if (!instructions) errors.push(`${entry.name}: SKILL.md must include instructions after ---`);
2686
2823
  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 });
2824
+ skills.push({ slug: entry.name, name, summary, instructions, resources });
2688
2825
  }
2689
2826
  }
2690
2827
  if (errors.length > 0) {
@@ -2710,6 +2847,13 @@ ${skill.summary}
2710
2847
  ${skill.instructions}
2711
2848
  `;
2712
2849
  }
2850
+ function skillMetadataChanges(local, remote) {
2851
+ const changes = [];
2852
+ if (local.name !== remote.name) changes.push("name");
2853
+ if (local.summary.trim() !== (remote.summary || "").trim()) changes.push("summary");
2854
+ if (local.instructions.trim() !== (remote.instructions || "").trim()) changes.push("instructions");
2855
+ return changes;
2856
+ }
2713
2857
  function scopedProjectSkillSlug(appName, localSlug) {
2714
2858
  return appName ? `${appName}:${localSlug}` : localSlug;
2715
2859
  }
@@ -2728,6 +2872,13 @@ function remoteSkillsByLocalSlug(remoteSkills, appName) {
2728
2872
  }
2729
2873
  return byLocalSlug;
2730
2874
  }
2875
+ function resolveProjectSkillLocalSlug(remoteSkills, selector) {
2876
+ if (!selector) return void 0;
2877
+ for (const [localSlug, skill] of remoteSkills) {
2878
+ if (selector === localSlug || selector === skill.slug || selector === skill.name) return localSlug;
2879
+ }
2880
+ return selector;
2881
+ }
2731
2882
  function projectSkillRefCandidates(refs, appName) {
2732
2883
  return [...new Set(refs.flatMap((ref) => appName && SKILL_SLUG_RE.test(ref) ? [scopedProjectSkillSlug(appName, ref), ref] : [ref]))];
2733
2884
  }
@@ -2794,13 +2945,21 @@ async function planSkills(api, localSkills, projectId, appName) {
2794
2945
  for (const skill of localSkills) {
2795
2946
  const remote = remoteBySlug.get(skill.slug);
2796
2947
  if (!remote) {
2797
- changes.push({ type: "create", resource: "skill", id: skill.slug, name: skill.name });
2948
+ const resourceSummary = skillResourceChangesSummary(compareSkillResources(skill.resources, []));
2949
+ changes.push({
2950
+ type: "create",
2951
+ resource: "skill",
2952
+ id: skill.slug,
2953
+ name: skill.name,
2954
+ details: resourceSummary ? `resources (${resourceSummary})` : void 0
2955
+ });
2798
2956
  continue;
2799
2957
  }
2800
- const changed = [];
2801
- if (remote.name !== skill.name) changed.push("name");
2802
- if ((remote.summary || "").trim() !== skill.summary.trim()) changed.push("summary");
2803
- if ((remote.instructions || "").trim() !== skill.instructions.trim()) changed.push("instructions");
2958
+ const changed = skillMetadataChanges(skill, remote);
2959
+ const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
2960
+ if (!skillResourceChangesEmpty(resourceChanges)) {
2961
+ changed.push(`resources (${skillResourceChangesSummary(resourceChanges)})`);
2962
+ }
2804
2963
  if (changed.length > 0) {
2805
2964
  changes.push({
2806
2965
  type: "update",
@@ -2808,12 +2967,64 @@ async function planSkills(api, localSkills, projectId, appName) {
2808
2967
  id: skill.slug,
2809
2968
  name: skill.name,
2810
2969
  details: `changed: ${changed.join(", ")}`,
2811
- textDiffs: changed.includes("instructions") || changed.includes("summary") ? [{ field: "SKILL.md", oldText: skillMarkdown({ slug: skill.slug, name: remote.name, summary: remote.summary || "", instructions: remote.instructions || "" }), newText: skillMarkdown(skill) }] : void 0
2970
+ 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
2971
  });
2813
2972
  }
2814
2973
  }
2815
2974
  return changes;
2816
2975
  }
2976
+ async function uploadSkillResource(api, skillID, resource) {
2977
+ const bytes = readFileSync2(resource.filePath);
2978
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
2979
+ if (bytes.byteLength !== resource.size || sha256 !== resource.sha256) {
2980
+ throw new Error(`${resource.path} changed while apply was running`);
2981
+ }
2982
+ const metadata = {
2983
+ path: resource.path,
2984
+ size: resource.size,
2985
+ content_type: skillResourceContentType(resource.path),
2986
+ sha256: resource.sha256
2987
+ };
2988
+ const upload = await api.startAgentSkillResourceUpload(skillID, metadata);
2989
+ const headers = { ...upload.headers ?? {} };
2990
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === "content-type")) {
2991
+ headers["Content-Type"] = metadata.content_type;
2992
+ }
2993
+ const response = await fetchWithRetry(upload.upload_url, {
2994
+ method: "PUT",
2995
+ headers,
2996
+ body: bytes
2997
+ }, { attempts: 3, retryMethods: ["PUT"] });
2998
+ if (!response.ok) throw new Error(`${resource.path}: upload failed with HTTP ${response.status}`);
2999
+ await api.completeAgentSkillResourceUpload(skillID, upload.upload_id, metadata);
3000
+ }
3001
+ async function reconcileSkillResources(api, skill, localResources) {
3002
+ let remoteResources = skill.resources ?? [];
3003
+ let changes = compareSkillResources(localResources, remoteResources);
3004
+ if (changes.deleted.length > 0) {
3005
+ const deletions = changes.deleted;
3006
+ const expectedByPath = new Map(remoteResources.map((resource) => [resource.path, resource]));
3007
+ remoteResources = (await api.getAgentSkill(skill.id)).resources ?? [];
3008
+ for (const resourcePath of deletions) {
3009
+ const expected = expectedByPath.get(resourcePath);
3010
+ const current = remoteResources.find((resource) => resource.path === resourcePath);
3011
+ if (!current) continue;
3012
+ if (!expected || current.size !== expected.size || current.sha256 !== expected.sha256) {
3013
+ throw new Error(`${resourcePath} changed while apply was running`);
3014
+ }
3015
+ const result = await api.deleteAgentSkillResource(skill.id, resourcePath);
3016
+ remoteResources = result.resources;
3017
+ }
3018
+ }
3019
+ changes = compareSkillResources(localResources, remoteResources);
3020
+ if (changes.deleted.length > 0) throw new Error("remote resources changed while apply was running");
3021
+ const uploadPaths = /* @__PURE__ */ new Set([...changes.added, ...changes.replaced]);
3022
+ const remoteByPath = new Map(remoteResources.map((resource) => [resource.path, resource]));
3023
+ const uploads = localResources.filter((resource) => uploadPaths.has(resource.path)).sort(
3024
+ (a, b) => a.size - (remoteByPath.get(a.path)?.size ?? 0) - (b.size - (remoteByPath.get(b.path)?.size ?? 0))
3025
+ );
3026
+ for (const resource of uploads) await uploadSkillResource(api, skill.id, resource);
3027
+ }
2817
3028
  async function applySkills(api, localSkills, projectId, appName) {
2818
3029
  if (localSkills.length === 0) return 0;
2819
3030
  validateProjectSkillStorageSlugs(localSkills, appName);
@@ -2824,21 +3035,19 @@ async function applySkills(api, localSkills, projectId, appName) {
2824
3035
  for (const skill of localSkills) {
2825
3036
  const remote = remoteBySlug.get(skill.slug);
2826
3037
  try {
2827
- if (remote) {
2828
- await api.updateAgentSkill(remote.id, {
2829
- name: skill.name,
2830
- summary: skill.summary,
2831
- instructions: skill.instructions
2832
- });
2833
- console.log(pc2.green(" \u2713"), `${skill.name} (updated)`);
2834
- } else {
2835
- await api.createAgentSkill({
2836
- ...skill,
2837
- slug: scopedProjectSkillSlug(appName, skill.slug),
2838
- project_id: scopedProjectID
2839
- });
2840
- console.log(pc2.green(" \u2713"), `${skill.name} (created)`);
2841
- }
3038
+ const saved = remote ? await api.updateAgentSkill(remote.id, {
3039
+ name: skill.name,
3040
+ summary: skill.summary,
3041
+ instructions: skill.instructions
3042
+ }) : await api.createAgentSkill({
3043
+ name: skill.name,
3044
+ summary: skill.summary,
3045
+ instructions: skill.instructions,
3046
+ slug: scopedProjectSkillSlug(appName, skill.slug),
3047
+ project_id: scopedProjectID
3048
+ });
3049
+ await reconcileSkillResources(api, saved, skill.resources);
3050
+ console.log(pc2.green(" \u2713"), `${skill.name} (${remote ? "updated" : "created"})`);
2842
3051
  } catch (error3) {
2843
3052
  console.log(pc2.red(" \u2717"), `${skill.name}: ${error3}`);
2844
3053
  errors++;
@@ -2846,7 +3055,57 @@ async function applySkills(api, localSkills, projectId, appName) {
2846
3055
  }
2847
3056
  return errors;
2848
3057
  }
2849
- async function pullSkills(api, platformDir, filterName, projectId, appName) {
3058
+ function validateRemoteSkillResources(resources) {
3059
+ if (resources.length > SKILL_RESOURCE_MAX_COUNT) throw new Error(`remote skill has more than ${SKILL_RESOURCE_MAX_COUNT} resources`);
3060
+ const paths = [];
3061
+ let totalBytes = 0;
3062
+ for (const resource of resources) {
3063
+ const pathError = skillResourcePathError(resource.path);
3064
+ if (pathError) throw new Error(`remote resource ${resource.path}: ${pathError}`);
3065
+ const conflict = paths.find((path) => skillResourcePathsCollide(path, resource.path));
3066
+ if (conflict) throw new Error(`remote resource paths conflict: ${conflict} and ${resource.path}`);
3067
+ if (!Number.isSafeInteger(resource.size) || resource.size < 0 || resource.size > SKILL_RESOURCE_MAX_BYTES) {
3068
+ throw new Error(`remote resource ${resource.path} has an invalid size`);
3069
+ }
3070
+ if (!/^[a-f0-9]{64}$/.test(resource.sha256)) throw new Error(`remote resource ${resource.path} has an invalid checksum`);
3071
+ paths.push(resource.path);
3072
+ totalBytes += resource.size;
3073
+ }
3074
+ if (totalBytes > SKILL_RESOURCES_MAX_TOTAL_BYTES) throw new Error("remote skill resources exceed 100 MiB");
3075
+ }
3076
+ function writeVerifiedSkillResource(resource, bytes, skillDir, source) {
3077
+ if (bytes.byteLength !== resource.size) throw new Error(`${resource.path}: ${source} size does not match`);
3078
+ if (createHash("sha256").update(bytes).digest("hex") !== resource.sha256) {
3079
+ throw new Error(`${resource.path}: ${source} checksum does not match`);
3080
+ }
3081
+ const destination = join2(skillDir, ...resource.path.split("/"));
3082
+ mkdirSync(dirname2(destination), { recursive: true });
3083
+ writeFileSync(destination, bytes);
3084
+ }
3085
+ async function downloadSkillResource(resource, skillDir) {
3086
+ if (!resource.url) throw new Error(`${resource.path}: download URL is unavailable`);
3087
+ let response;
3088
+ try {
3089
+ response = await fetchWithRetry(resource.url);
3090
+ } catch {
3091
+ throw new Error(`${resource.path}: download failed`);
3092
+ }
3093
+ if (!response.ok) throw new Error(`${resource.path}: download failed with HTTP ${response.status}`);
3094
+ writeVerifiedSkillResource(resource, Buffer.from(await response.arrayBuffer()), skillDir, "downloaded");
3095
+ }
3096
+ function replaceSkillDirectory(stagingDir, skillDir) {
3097
+ const backupDir = `${stagingDir}.previous`;
3098
+ const hadExisting = existsSync2(skillDir);
3099
+ if (hadExisting) renameSync(skillDir, backupDir);
3100
+ try {
3101
+ renameSync(stagingDir, skillDir);
3102
+ } catch (error3) {
3103
+ if (hadExisting && !existsSync2(skillDir)) renameSync(backupDir, skillDir);
3104
+ throw error3;
3105
+ }
3106
+ if (hadExisting) rmSync(backupDir, { recursive: true, force: true });
3107
+ }
3108
+ async function pullSkills(api, platformDir, filterName, projectId, appName, checkedLocalSkills) {
2850
3109
  if (!projectId) {
2851
3110
  console.log(pc2.dim(" Skipped \u2014 project not registered (run `lumera register`)"));
2852
3111
  return;
@@ -2857,24 +3116,56 @@ async function pullSkills(api, platformDir, filterName, projectId, appName) {
2857
3116
  await api.listAgentSkills({ project_id: scopedProjectID }),
2858
3117
  appName
2859
3118
  );
2860
- for (const [localSlug, skill] of remoteSkills) {
3119
+ const resolvedFilter = resolveProjectSkillLocalSlug(remoteSkills, filterName);
3120
+ const checkedLocalSkillsBySlug = new Map(checkedLocalSkills?.map((skill) => [skill.slug, skill]) ?? []);
3121
+ for (const [localSlug, listedSkill] of remoteSkills) {
2861
3122
  if (!SKILL_SLUG_RE.test(localSlug)) {
2862
- console.log(pc2.yellow(` \u26A0 Skipping ${skill.name}: skill slug ${skill.slug} cannot be represented in platform/skills`));
3123
+ console.log(pc2.yellow(` \u26A0 Skipping ${listedSkill.name}: skill slug ${listedSkill.slug} cannot be represented in platform/skills`));
3124
+ continue;
3125
+ }
3126
+ if (resolvedFilter && localSlug !== resolvedFilter) continue;
3127
+ const listedResources = [...listedSkill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
3128
+ validateRemoteSkillResources(listedResources);
3129
+ let local = checkedLocalSkillsBySlug.get(localSlug);
3130
+ if (!checkedLocalSkills) {
3131
+ try {
3132
+ [local] = loadLocalSkills(platformDir, localSlug);
3133
+ } catch {
3134
+ }
3135
+ }
3136
+ if (local && skillMetadataChanges(local, listedSkill).length === 0 && skillResourceChangesEmpty(compareSkillResources(local.resources, listedResources))) {
3137
+ console.log(pc2.green(" \u2713"), `${listedSkill.name} (unchanged)`);
2863
3138
  continue;
2864
3139
  }
2865
- if (filterName && localSlug !== filterName && skill.slug !== filterName && skill.name !== filterName) continue;
3140
+ const skill = await api.getAgentSkill(listedSkill.id, { resource_urls: "attachment" });
2866
3141
  if (!(skill.summary || "").trim() || !(skill.instructions || "").trim()) {
2867
3142
  console.log(pc2.yellow(` \u26A0 Skipping ${skill.name}: source-managed skills require both summary and instructions`));
2868
3143
  continue;
2869
3144
  }
3145
+ const resources = [...skill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
3146
+ validateRemoteSkillResources(resources);
3147
+ const localResources = new Map(local?.resources.map((resource) => [resource.path, resource]) ?? []);
2870
3148
  const skillDir = join2(skillsDir, localSlug);
2871
- mkdirSync(skillDir, { recursive: true });
2872
- writeFileSync(join2(skillDir, "SKILL.md"), skillMarkdown({
2873
- slug: localSlug,
2874
- name: skill.name,
2875
- summary: skill.summary || "",
2876
- instructions: skill.instructions || ""
2877
- }));
3149
+ mkdirSync(skillsDir, { recursive: true });
3150
+ const stagingDir = mkdtempSync(join2(platformDir, `.skill-${localSlug}-pull-`));
3151
+ try {
3152
+ writeFileSync(join2(stagingDir, "SKILL.md"), skillMarkdown({
3153
+ name: skill.name,
3154
+ summary: skill.summary || "",
3155
+ instructions: skill.instructions || ""
3156
+ }));
3157
+ for (const resource of resources) {
3158
+ const localResource = localResources.get(resource.path);
3159
+ if (localResource?.size === resource.size && localResource.sha256 === resource.sha256) {
3160
+ writeVerifiedSkillResource(resource, readFileSync2(localResource.filePath), stagingDir, "local");
3161
+ } else {
3162
+ await downloadSkillResource(resource, stagingDir);
3163
+ }
3164
+ }
3165
+ replaceSkillDirectory(stagingDir, skillDir);
3166
+ } finally {
3167
+ rmSync(stagingDir, { recursive: true, force: true });
3168
+ }
2878
3169
  console.log(pc2.green(" \u2713"), `${skill.name} \u2192 skills/${localSlug}/`);
2879
3170
  }
2880
3171
  }
@@ -2928,14 +3219,6 @@ function loadLocalAgents(platformDir, filterName, appName) {
2928
3219
  continue;
2929
3220
  }
2930
3221
  }
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
3222
  let systemPrompt = readFileSync2(promptPath, "utf-8");
2940
3223
  let policyScript = existsSync2(policyPath) ? readFileSync2(policyPath, "utf-8") : "";
2941
3224
  if (appName) {
@@ -2956,9 +3239,6 @@ function loadLocalAgents(platformDir, filterName, appName) {
2956
3239
  }
2957
3240
  return agents;
2958
3241
  }
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
3242
  async function planAgents(api, localAgents, projectId, localSourceSkillSlugs = /* @__PURE__ */ new Set(), pendingLocalSkillSlugs = /* @__PURE__ */ new Set(), appName) {
2963
3243
  const changes = [];
2964
3244
  let validationErrors = 0;
@@ -3011,7 +3291,6 @@ async function planAgents(api, localAgents, projectId, localSourceSkillSlugs = /
3011
3291
  if ((remote.system_prompt || "").trim() !== systemPrompt.trim()) diffs.push("system_prompt");
3012
3292
  if ((remote.model || "") !== (agent.model || "")) diffs.push("model");
3013
3293
  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
3294
  if ((remote.idle_reap_timeout_seconds ?? 0) !== (agent.idle_reap_timeout_seconds ?? 0)) diffs.push("idle_reap_timeout_seconds");
3016
3295
  if ((remote.policy_script || "").trim() !== (policyScript || "").trim()) diffs.push("policy_script");
3017
3296
  if ((remote.policy_enabled || false) !== (agent.policy_enabled || false)) diffs.push("policy_enabled");
@@ -3115,7 +3394,6 @@ async function applyAgents(api, localAgents, projectId, localSourceSkillSlugs =
3115
3394
  system_prompt: systemPrompt,
3116
3395
  model: agent.model || "",
3117
3396
  ...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
3397
  idle_reap_timeout_seconds: agent.idle_reap_timeout_seconds ?? 0,
3120
3398
  skill_ids: skillIds,
3121
3399
  policy_script: policyScript || "",
@@ -3177,7 +3455,6 @@ async function pullAgents(api, platformDir, filterName, projectId, appName) {
3177
3455
  if (agent.description) config.description = agent.description;
3178
3456
  if (agent.model) config.model = agent.model;
3179
3457
  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
3458
  if (agent.idle_reap_timeout_seconds) config.idle_reap_timeout_seconds = agent.idle_reap_timeout_seconds;
3182
3459
  if (skillSlugs.length > 0) config.skills = skillSlugs;
3183
3460
  if (agent.policy_enabled) config.policy_enabled = true;
@@ -3314,16 +3591,21 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
3314
3591
  for (const skill of localSkills) {
3315
3592
  const remote = remoteBySlug.get(skill.slug);
3316
3593
  if (!remote) {
3317
- results.push({ name: skill.name, type: "skills", status: "local-only" });
3318
- } else if (remote.name !== skill.name || (remote.summary || "").trim() !== skill.summary.trim() || (remote.instructions || "").trim() !== skill.instructions.trim()) {
3319
- results.push({ name: skill.name, type: "skills", status: "changed", details: "SKILL.md" });
3594
+ const resourceChanges = skillResourceChangesSummary(compareSkillResources(skill.resources, []));
3595
+ results.push(resourceChanges ? { name: skill.name, type: "skills", status: "local-only", details: `resources (${resourceChanges})` } : { name: skill.name, type: "skills", status: "local-only" });
3320
3596
  } else {
3321
- results.push({ name: skill.name, type: "skills", status: "synced" });
3597
+ const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
3598
+ const details = [
3599
+ skillMetadataChanges(skill, remote).length > 0 ? "SKILL.md" : "",
3600
+ skillResourceChangesEmpty(resourceChanges) ? "" : `resources (${skillResourceChangesSummary(resourceChanges)})`
3601
+ ].filter(Boolean).join(", ");
3602
+ results.push(details ? { name: skill.name, type: "skills", status: "changed", details } : { name: skill.name, type: "skills", status: "synced" });
3322
3603
  }
3323
3604
  }
3324
3605
  for (const remote of remoteSkills) {
3325
3606
  if (!localSlugs.has(localProjectSkillSlug(appName, remote.slug))) {
3326
- results.push({ name: remote.name, type: "skills", status: "remote-only" });
3607
+ const resourceChanges = skillResourceChangesSummary(compareSkillResources([], remote.resources ?? []));
3608
+ results.push(resourceChanges ? { name: remote.name, type: "skills", status: "remote-only", details: `resources (${resourceChanges})` } : { name: remote.name, type: "skills", status: "remote-only" });
3327
3609
  }
3328
3610
  }
3329
3611
  }
@@ -3825,8 +4107,12 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
3825
4107
  console.log(pc2.bold(` Skill: ${local?.name || remote?.name}`));
3826
4108
  console.log();
3827
4109
  if (local && remote) {
3828
- const changed = local.name !== remote.name || local.summary.trim() !== (remote.summary || "").trim() || local.instructions.trim() !== (remote.instructions || "").trim();
4110
+ const resourceChanges = compareSkillResources(local.resources, remote.resources ?? []);
4111
+ const changed = skillMetadataChanges(local, remote).length > 0 || !skillResourceChangesEmpty(resourceChanges);
3829
4112
  console.log(` Status: ${changed ? pc2.yellow("changed") : pc2.green("synced")}`);
4113
+ if (!skillResourceChangesEmpty(resourceChanges)) {
4114
+ console.log(` Resources: ${skillResourceChangesSummary(resourceChanges)}`);
4115
+ }
3830
4116
  } else if (local) {
3831
4117
  console.log(` Status: ${pc2.yellow("local only")}`);
3832
4118
  } else {
@@ -3835,6 +4121,8 @@ async function showResource(api, platformDir, resourceType, resourceName, appNam
3835
4121
  console.log(` Slug: ${local?.slug || (remote ? localProjectSkillSlug(appName, remote.slug) : "")}`);
3836
4122
  const summary = local?.summary || remote?.summary;
3837
4123
  if (summary) console.log(` Summary: ${summary}`);
4124
+ const resourcePaths = (local?.resources ?? remote?.resources ?? []).map((resource) => resource.path).sort();
4125
+ if (resourcePaths.length > 0) console.log(` Resource files: ${resourcePaths.join(", ")}`);
3838
4126
  console.log();
3839
4127
  } else if (resourceType === "mailboxes") {
3840
4128
  const localMailboxes = loadLocalMailboxes(platformDir, resourceName);
@@ -4268,6 +4556,7 @@ async function pull(args) {
4268
4556
  const api = createApiClient(void 0, void 0, appName);
4269
4557
  const projectId = getProjectId(projectRoot);
4270
4558
  const { type, name } = parseResource(filteredArgs[0]);
4559
+ let checkedLocalSkills;
4271
4560
  if (!force) {
4272
4561
  const conflicts = [];
4273
4562
  let collections;
@@ -4278,11 +4567,22 @@ async function pull(args) {
4278
4567
  collections = /* @__PURE__ */ new Map();
4279
4568
  }
4280
4569
  if (projectId && (!type || type === "skills")) {
4281
- const localSkills = loadLocalSkills(platformDir, name || void 0);
4282
- if (localSkills.length > 0) {
4283
- const changes = await planSkills(api, localSkills, projectId, appName);
4284
- for (const change of changes) {
4285
- if (change.type === "update") conflicts.push(`skills/${change.id}`);
4570
+ const remoteBySlug = remoteSkillsByLocalSlug(
4571
+ await api.listAgentSkills({ project_id: projectId }),
4572
+ appName
4573
+ );
4574
+ checkedLocalSkills = loadLocalSkills(
4575
+ platformDir,
4576
+ resolveProjectSkillLocalSlug(remoteBySlug, name || void 0)
4577
+ );
4578
+ if (checkedLocalSkills.length > 0) {
4579
+ for (const skill of checkedLocalSkills) {
4580
+ const remote = remoteBySlug.get(skill.slug);
4581
+ if (!remote) continue;
4582
+ const resourceChanges = compareSkillResources(skill.resources, remote.resources ?? []);
4583
+ if (skillMetadataChanges(skill, remote).length > 0 || resourceChanges.added.length > 0 || resourceChanges.replaced.length > 0) {
4584
+ conflicts.push(`skills/${skill.slug}`);
4585
+ }
4286
4586
  }
4287
4587
  }
4288
4588
  }
@@ -4360,7 +4660,7 @@ async function pull(args) {
4360
4660
  }
4361
4661
  if (!type || type === "skills") {
4362
4662
  console.log(pc2.bold(" Skills:"));
4363
- await pullSkills(api, platformDir, name || void 0, projectId, appName);
4663
+ await pullSkills(api, platformDir, name || void 0, projectId, appName, checkedLocalSkills);
4364
4664
  console.log();
4365
4665
  }
4366
4666
  if (!type || type === "agents") {
@@ -4517,7 +4817,7 @@ ${pc2.dim("Usage:")}
4517
4817
 
4518
4818
  ${pc2.dim("Resources:")}
4519
4819
  agents/<name> Diff agent (system_prompt, policy_script)
4520
- skills/<slug> Diff project skill instructions
4820
+ skills/<slug> Diff project SKILL.md and resource paths
4521
4821
  automations/<name> Diff automation code
4522
4822
  hooks/<name> Diff hook script
4523
4823
 
@@ -4597,11 +4897,7 @@ async function diff(args) {
4597
4897
  const promptChanged = (remote.system_prompt || "").trim() !== local.systemPrompt.trim();
4598
4898
  const policyChanged = (remote.policy_script || "").trim() !== (local.policyScript || "").trim();
4599
4899
  const thinkingLevelChanged = local.agent.thinking_level !== void 0 && (remote.thinking_level || "") !== local.agent.thinking_level;
4600
- const userTagsChanged = allowedUserTagsChanged(local.agent, remote);
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)) {
4900
+ 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
4901
  console.log(pc2.green(` \u2713 No changes`));
4606
4902
  } else {
4607
4903
  if (promptChanged) renderFullDiff("system_prompt.md", remote.system_prompt || "", local.systemPrompt);
@@ -4697,16 +4993,22 @@ async function diff(args) {
4697
4993
  console.log(` summary: ${pc2.red(remote.summary || "(empty)")} \u2192 ${pc2.green(local.summary)}`);
4698
4994
  }
4699
4995
  const remoteMarkdown = skillMarkdown({
4700
- slug: localProjectSkillSlug(appName, remote.slug),
4701
4996
  name: remote.name,
4702
4997
  summary: remote.summary || "",
4703
4998
  instructions: remote.instructions || ""
4704
4999
  });
4705
5000
  const localMarkdown = skillMarkdown(local);
4706
- if (remoteMarkdown === localMarkdown) {
5001
+ const resourceChanges = compareSkillResources(local.resources, remote.resources ?? []);
5002
+ if (remoteMarkdown === localMarkdown && skillResourceChangesEmpty(resourceChanges)) {
4707
5003
  console.log(pc2.green(" \u2713 No changes"));
4708
5004
  } else {
4709
- renderFullDiff("SKILL.md", remoteMarkdown, localMarkdown);
5005
+ if (remoteMarkdown !== localMarkdown) renderFullDiff("SKILL.md", remoteMarkdown, localMarkdown);
5006
+ if (!skillResourceChangesEmpty(resourceChanges)) {
5007
+ console.log(pc2.bold(" Resources:"));
5008
+ for (const path of resourceChanges.added) console.log(pc2.green(` + ${path}`));
5009
+ for (const path of resourceChanges.replaced) console.log(pc2.yellow(` ~ ${path}`));
5010
+ for (const path of resourceChanges.deleted) console.log(pc2.red(` - ${path}`));
5011
+ }
4710
5012
  }
4711
5013
  } else {
4712
5014
  console.log(pc2.red(` Diff not supported for "${type}" \u2014 use agents, skills, automations, or hooks`));
@@ -4720,11 +5022,11 @@ export {
4720
5022
  applyAutomations,
4721
5023
  applyCollections,
4722
5024
  applySkills,
5025
+ compareSkillResources,
4723
5026
  convertCollectionToApiFormat,
4724
5027
  destroy,
4725
5028
  diff,
4726
5029
  list,
4727
- loadLocalAgents,
4728
5030
  loadLocalCollections,
4729
5031
  loadLocalSkills,
4730
5032
  localProjectSkillSlug,
@@ -4734,10 +5036,10 @@ export {
4734
5036
  planSkills,
4735
5037
  projectSkillRefCandidates,
4736
5038
  pull,
4737
- pullAgents,
4738
5039
  pullCollections,
4739
5040
  pullSkills,
4740
5041
  resolveAgentSkillRefs,
5042
+ resolveProjectSkillLocalSlug,
4741
5043
  scopedProjectSkillSlug,
4742
5044
  show
4743
5045
  };