Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Read against the public marketplaces as they are: a marketplace entry that is not strict names its skills outright, so the skills marketplace's plugins each bring their own skills and not every sibling folder; a plugin skill's long description is shortened for the listing rather than refused; and a skill whose folder is larger than one version holds (1.5 MB, within the database's row) keeps its text and leaves its biggest binaries out, named in the install's problems, since a runner fetches the plugin whole.

syntaqxcommitted Parent48471b3Browse files
6 files+87−190/6 viewed
+4−1
261261 const entry = parseMarketplaceManifest(SKILLS);
262262 assert.ok(entry.ok);
263263 const docs = pluginLayout(["skills/xlsx/SKILL.md", "skills/docx/SKILL.md", "skills/pdf/SKILL.md", "skills/pptx/SKILL.md", "skills/other/SKILL.md", "README.md"], null, entry.value.plugins[0]!);
264+ // The entry is not strict: it names its skills, so a sibling folder under skills/ is not its.
264265 assert.deepEqual(
265266 docs.skills.map((s) => s.name),
266− ["docx", "other", "pdf", "pptx", "xlsx"],
267+ ["docx", "pdf", "pptx", "xlsx"],
267268 );
269+ // An entry that names its skills gets exactly those, not every sibling folder under skills/.
270+ assert.deepEqual(pluginLayout(["skills/xlsx/SKILL.md", "skills/claude-api/SKILL.md", "skills/other/SKILL.md"], null, entry.value.plugins[1]!).skills, [{ path: "skills/claude-api", name: "claude-api" }]);
268271 // A plugin that is one skill at its root.
269272 assert.deepEqual(pluginLayout(["SKILL.md", "reference.md"], null, { ...entry.value.plugins[1]!, skills: ["."] }).skills, [{ path: ".", name: "claude-api" }]);
270273 });
+13−10
599599 const mdName = (path: string): string => path.replace(/^.*\//, "").replace(/\.md$/i, "");
600600
601601 /**
602− * The plugin's components from its files: the standard folders, then the
603− * manifest's and the marketplace entry's paths (the entry's added when it
604− * is strict, as Claude Code does). `files` are paths inside the plugin.
602+ * The plugin's components from its files: the standard folders
603+ * (`skills/`, `commands/`, `agents/`) plus whatever the manifest names,
604+ * plus the marketplace entry's paths. An entry that is not strict names
605+ * its components outright instead of the standard folders, as the public
606+ * skills marketplace does (one repository, one plugin per group of
607+ * skills). `files` are paths inside the plugin.
605608 */
606609 export function pluginLayout(files: string[], manifest: PluginManifest | null, entry: MarketplacePluginEntry | null): PluginLayout {
607610 const set = new Set(files.map((f) => f.replace(/^\.\//, "")));
613616 if (has(clean ? `${clean}/SKILL.md` : "SKILL.md")) skillDirs.add(clean || ".");
614617 else for (const f of under(clean || ".")) if (/(^|\/)SKILL\.md$/.test(f) && f.slice(clean.length + (clean ? 1 : 0)).split("/").length === 2) skillDirs.add(f.replace(/\/SKILL\.md$/, ""));
615618 };
616− addSkillDir("skills");
617− const strictExtras = entry && entry.strict ? entry : null;
618− for (const dir of [...(manifest?.skills ?? []), ...(strictExtras?.skills ?? []), ...(!manifest && entry ? entry.skills : [])]) addSkillDir(dir);
619+ const standard = !(entry && !entry.strict);
620+ if (standard) addSkillDir("skills");
621+ for (const dir of [...(manifest?.skills ?? []), ...(entry?.skills ?? [])]) addSkillDir(dir);
619622 const commandFiles = new Set<string>();
620623 const addCommands = (path: string) => {
621624 if (/\.md$/i.test(path) && has(path)) commandFiles.add(path);
622625 else for (const f of under(path)) if (/\.md$/i.test(f)) commandFiles.add(f);
623626 };
624− addCommands("commands");
625− for (const p of [...(manifest?.commands ?? []), ...(strictExtras?.commands ?? []), ...(!manifest && entry ? entry.commands : [])]) addCommands(p);
627+ if (standard) addCommands("commands");
628+ for (const p of [...(manifest?.commands ?? []), ...(entry?.commands ?? [])]) addCommands(p);
626629 const agentFiles = new Set<string>();
627630 const addAgents = (path: string) => {
628631 if (/\.md$/i.test(path) && has(path)) agentFiles.add(path);
629632 else for (const f of under(path)) if (/\.md$/i.test(f)) agentFiles.add(f);
630633 };
631− addAgents("agents");
632− for (const p of [...(manifest?.agents ?? []), ...(strictExtras?.agents ?? []), ...(!manifest && entry ? entry.agents : [])]) addAgents(p);
634+ if (standard) addAgents("agents");
635+ for (const p of [...(manifest?.agents ?? []), ...(entry?.agents ?? [])]) addAgents(p);
633636 const hooks = [...(has("hooks/hooks.json") ? ["hooks/hooks.json"] : []), ...(manifest?.hooks ?? []).filter((h) => h === "(inline)" || has(h))];
634637 return {
635638 skills: [...skillDirs].sort().map((path) => ({ path, name: path === "." ? (manifest?.name ?? entry?.name ?? "skill") : path.replace(/^.*\//, "") })),
+15−0
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4+import { PLUGIN_SKILL_LIMITS, checkSkillFolder as checkFolder } from "./skill-format.ts";
5+
6+test("a plugin's skill is checked leniently: its name keeps its case, a long description is shortened, Claude Code's tools are kept aside, and it may be larger", () => {
7+ const long = "Use when ".padEnd(1500, "x");
8+ const checked = checkFolder([{ path: "SKILL.md", content: `---\nname: Claude-API\ndescription: ${long}\ntools: Read, read_file\n---\n\nBody.\n` }], { lenient: true, limits: PLUGIN_SKILL_LIMITS });
9+ assert.ok(checked.ok, !checked.ok ? checked.message : "");
10+ assert.equal(checked.skill.name, "Claude-API");
11+ assert.equal(checked.skill.description.length, 1024);
12+ assert.ok(checked.skill.description.endsWith("…"));
13+ assert.deepEqual(checked.skill.tools, ["read_file"]);
14+ assert.deepEqual(checked.skill.extra.tools, ["Read"]);
15+ const strict = checkFolder([{ path: "SKILL.md", content: `---\nname: claude-api\ndescription: ${long}\n---\n\nBody.\n` }]);
16+ assert.equal(strict.ok, false);
17+});
18+
419 import {
520 AGENT_TOOL_NAMES,
621 RESERVED_SKILL_NAMES,
+12−5
359359 limits?: { files: number; bytes: number };
360360 };
361361
362−/** How large a plugin's skill may be: public skills carry references and schemas. */
363−export const PLUGIN_SKILL_LIMITS = { files: 1000, bytes: 4 * 1024 * 1024 };
362+/**
363+ * How large a plugin's skill kept here may be: public skills carry
364+ * references and schemas, and a version is one database row (2 MB at
365+ * most, with its JSON). Larger folders keep their text and leave their
366+ * biggest binaries out (the runner fetches the plugin whole anyway).
367+ */
368+export const PLUGIN_SKILL_LIMITS = { files: 1000, bytes: 1_500_000 };
364369
365370 export function checkSkillFolder(input: SkillFile[], options: SkillCheckOptions = {}): SkillCheck {
366371 const bad = (message: string): SkillCheck => ({ ok: false, message });
403408 const nameProblem = options.lenient ? lenientSkillNameProblem(name) : skillNameProblem(name);
404409 if (nameProblem) return bad(name ? nameProblem : "SKILL.md's front-matter needs a name: the skill's name, like release-notes.");
405410 if (options.expectName && options.expectName !== name) return bad(`The folder is ${options.expectName}, but its SKILL.md is named ${name}. They must match.`);
406− const description = typeof front.description === "string" ? front.description.replace(/\s+/g, " ").trim() : "";
407− if (!description) return bad("SKILL.md's front-matter needs a description: when an agent should use the skill.");
408− if (description.length > SKILL_DESCRIPTION_MAX) return bad(`A skill's description is at most ${SKILL_DESCRIPTION_MAX} characters.`);
411+ const full = typeof front.description === "string" ? front.description.replace(/\s+/g, " ").trim() : "";
412+ if (!full) return bad("SKILL.md's front-matter needs a description: when an agent should use the skill.");
413+ if (full.length > SKILL_DESCRIPTION_MAX && !options.lenient) return bad(`A skill's description is at most ${SKILL_DESCRIPTION_MAX} characters.`);
414+ // A plugin's skill keeps its text as written; the listing shows the first part.
415+ const description = full.length > SKILL_DESCRIPTION_MAX ? `${full.slice(0, SKILL_DESCRIPTION_MAX - 1).trimEnd()}…` : full;
409416 const listed = listOf(front.tools);
410417 if (!listed) return bad("tools: is a list of tool names, like [read_file, make_file].");
411418 const named = [...new Set(listed.map((t) => t.trim()))].filter(Boolean);
+15−0
229229 assert.deepEqual(plugins.installsOut([row], [listing])[0]!.update, { version: "1.2.0", sha: null });
230230 assert.equal(plugins.installsOut([row], [{ ...listing, version: "1.0.0" }])[0]!.update, null);
231231 });
232+
233+test("a plugin skill larger than a version holds keeps its text and leaves its biggest binaries out", () => {
234+ const big = "A".repeat(1_200_000);
235+ const files = [
236+ { path: "SKILL.md", content: "---\nname: canvas\ndescription: Use when drawing.\n---\n\nDraw.\n", encoding: "utf8" as const },
237+ { path: "reference.md", content: "# Reference\n", encoding: "utf8" as const },
238+ { path: "fonts/a.ttf", content: big, encoding: "base64" as const },
239+ { path: "fonts/b.ttf", content: big, encoding: "base64" as const },
240+ { path: "scripts/draw.py", content: "print(1)\n", encoding: "utf8" as const },
241+ ];
242+ const fitted = plugins.fitSkillFolder(files);
243+ assert.deepEqual(fitted.omitted, ["fonts/a.ttf"], "one font out brings it under the limit; text stays");
244+ assert.deepEqual(fitted.files.map((f) => f.path).sort(), ["SKILL.md", "fonts/b.ttf", "reference.md", "scripts/draw.py"]);
245+ assert.deepEqual(plugins.fitSkillFolder(files.slice(0, 2)).omitted, []);
246+});
+28−3
3535 } from "../../../packages/contracts/src/claude-plugin.ts";
3636 import { MAX_PLUGIN_INSTALLS, type PluginAgentTemplate, type PluginComponents, type PluginDetail, type PluginInstall, type PluginListing, componentsOf } from "../../../packages/contracts/src/customize.ts";
3737 import { newId } from "../../../packages/contracts/src/ids.ts";
38−import { PLUGIN_SKILL_LIMITS, type SkillFile, checkSkillFolder, parseFrontMatter, splitFrontMatter } from "../../../packages/contracts/src/skill-format.ts";
38+import { PLUGIN_SKILL_LIMITS, type SkillFile, checkSkillFolder, parseFrontMatter, skillFileBytes, splitFrontMatter } from "../../../packages/contracts/src/skill-format.ts";
3939 import { type FetchDeps, fetchPlugin } from "./customize-fetch.ts";
4040 import { type SkillRow, skillByName, writeVersion } from "./skill-library.ts";
4141
216216 };
217217 }
218218
219+/**
220+ * A skill's folder cut to what one version holds: its biggest binary files
221+ * go first, then its biggest other files besides SKILL.md, until it fits
222+ * `PLUGIN_SKILL_LIMITS`. What was left out is named, for the install's
223+ * problems and the version's note.
224+ */
225+export function fitSkillFolder(files: SkillFile[]): { files: SkillFile[]; omitted: string[] } {
226+ let total = files.reduce((n, f) => n + skillFileBytes(f), 0);
227+ if (files.length <= PLUGIN_SKILL_LIMITS.files && total <= PLUGIN_SKILL_LIMITS.bytes) return { files, omitted: [] };
228+ const keep = new Map(files.map((f) => [f.path, f]));
229+ const order = [...files]
230+ .filter((f) => f.path.toLowerCase() !== "skill.md")
231+ .sort((a, b) => Number(b.encoding === "base64") - Number(a.encoding === "base64") || skillFileBytes(b) - skillFileBytes(a));
232+ const omitted: string[] = [];
233+ for (const file of order) {
234+ if (keep.size <= PLUGIN_SKILL_LIMITS.files && total <= PLUGIN_SKILL_LIMITS.bytes) break;
235+ keep.delete(file.path);
236+ total -= skillFileBytes(file);
237+ omitted.push(file.path);
238+ }
239+ return { files: [...keep.values()], omitted: omitted.sort() };
240+}
241+
219242 /** A command file (`commands/<name>.md`) as a one-file skill: its front-matter's description, or its first line. */
220243 function commandAsSkill(name: string, text: string): SkillFile[] {
221244 const split = splitFrontMatter(text);
312335 ];
313336 const seen = new Set<string>();
314337 for (const one of folders) {
315− const checked = checkSkillFolder(one.files, { expectName: one.name, lenient: true, limits: PLUGIN_SKILL_LIMITS });
338+ const fitted = fitSkillFolder(one.files);
339+ if (fitted.omitted.length) problems.push(`${one.path}: ${fitted.omitted.length} ${fitted.omitted.length === 1 ? "file" : "files"} left out to fit (${fitted.omitted.slice(0, 5).join(", ")}${fitted.omitted.length > 5 ? "…" : ""}); Claude Code on a runner gets the whole plugin.`);
340+ const checked = checkSkillFolder(fitted.files, { expectName: one.name, lenient: true, limits: PLUGIN_SKILL_LIMITS });
316341 if (!checked.ok) {
317342 problems.push(`${one.path}: ${checked.message}`);
318343 skills.push({ name: one.name, id: "", problem: checked.message });
329354 existing,
330355 checked: checked.skill,
331356 origin: { kind: "plugin", plugin: listing.ref, marketplace: listing.marketplace, path: one.path, sha: plugin.sha },
332− note: `From ${listing.ref}${plugin.sha ? ` at ${plugin.sha.slice(0, 8)}` : ""}`,
357+ note: `From ${listing.ref}${plugin.sha ? ` at ${plugin.sha.slice(0, 8)}` : ""}${fitted.omitted.length ? ` (${fitted.omitted.length} large files left out)` : ""}`,
333358 by: input.by,
334359 now,
335360 status: "published",

Comments

Sign in to comment.