A new skill_install agent tool reads every SKILL.md behind a public github.com repository, folder or file link and saves each as one of the user's own, editable skills. Only GitHub's API and raw hosts are contacted; invalid files and name clashes are skipped and reported. Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ Co-authored-by: Claude <noreply@anthropic.com>
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { GithubFetch } from "./skill-github.js";
|
|
import { fetchGithubSkills, parseGithubSkillUrl } from "./skill-github.js";
|
|
import { skillInstallFromTool } from "./skill-tools.js";
|
|
|
|
const skill = (name: string) =>
|
|
`---\nname: ${name}\ndescription: Pretend ${name} recipe\n---\n\n1. Do the thing.\n`;
|
|
|
|
/** A pretend public repo: two skills under skills/, one at the root, one unrelated file. */
|
|
function pretendGithub(files: Record<string, string>, defaultBranch = "main") {
|
|
const calls: string[] = [];
|
|
const fetchImpl: GithubFetch = vi.fn(async (url: string) => {
|
|
calls.push(url);
|
|
const { hostname, pathname } = new URL(url);
|
|
if (hostname === "api.github.com" && pathname === "/repos/someone/founder-kit") {
|
|
return Response.json({ default_branch: defaultBranch });
|
|
}
|
|
if (
|
|
hostname === "api.github.com" &&
|
|
pathname.startsWith("/repos/someone/founder-kit/git/trees/")
|
|
) {
|
|
return Response.json({
|
|
tree: [
|
|
...Object.keys(files).map((path) => ({ path, type: "blob" })),
|
|
{ path: "README.md", type: "blob" },
|
|
{ path: "skills", type: "tree" },
|
|
],
|
|
});
|
|
}
|
|
if (hostname === "raw.githubusercontent.com") {
|
|
const path = pathname.split("/").slice(4).join("/");
|
|
const body = files[decodeURIComponent(path)];
|
|
return body === undefined ? new Response("", { status: 404 }) : new Response(body);
|
|
}
|
|
return new Response("", { status: 404 });
|
|
});
|
|
return { fetchImpl, calls };
|
|
}
|
|
|
|
function makePrisma() {
|
|
const store: Array<Record<string, unknown>> = [];
|
|
return {
|
|
agentSkill: {
|
|
findMany: vi.fn(async () => store.map((row) => ({ ...row }))),
|
|
create: vi.fn(async ({ data }: { data: Record<string, unknown> }) => {
|
|
const row = { id: `skill-${store.length + 1}`, source: "user", ...data };
|
|
store.push(row);
|
|
return row;
|
|
}),
|
|
},
|
|
store,
|
|
};
|
|
}
|
|
|
|
const owner = { spaceId: "ws-1", userId: "user-1" };
|
|
|
|
describe("GitHub skill links", () => {
|
|
it("reads repository, folder, file and raw links and rejects other hosts", () => {
|
|
expect(parseGithubSkillUrl("https://github.com/someone/founder-kit")).toEqual({
|
|
owner: "someone",
|
|
repo: "founder-kit",
|
|
path: "",
|
|
});
|
|
expect(parseGithubSkillUrl("github.com/someone/founder-kit.git/tree/dev/skills/cfo")).toEqual({
|
|
owner: "someone",
|
|
repo: "founder-kit",
|
|
ref: "dev",
|
|
path: "skills/cfo",
|
|
});
|
|
expect(
|
|
parseGithubSkillUrl("https://raw.githubusercontent.com/someone/founder-kit/main/SKILL.md"),
|
|
).toMatchObject({ ref: "main", path: "SKILL.md" });
|
|
expect(parseGithubSkillUrl("https://example.com/someone/founder-kit")).toBeNull();
|
|
expect(parseGithubSkillUrl("https://github.com/someone")).toBeNull();
|
|
expect(parseGithubSkillUrl("https://github.com.evil.test/someone/kit")).toBeNull();
|
|
});
|
|
|
|
it("installs every SKILL.md of a repo and only fetches from GitHub", async () => {
|
|
const { fetchImpl, calls } = pretendGithub({
|
|
"SKILL.md": skill("Launch kit"),
|
|
"skills/cfo/SKILL.md": skill("CFO"),
|
|
"skills/panel/SKILL.md": skill("Buyer panel"),
|
|
});
|
|
const prisma = makePrisma();
|
|
const result = await skillInstallFromTool(
|
|
prisma as never,
|
|
owner,
|
|
{ url: "https://github.com/someone/founder-kit" },
|
|
fetchImpl,
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
repo: "someone/founder-kit",
|
|
installed: ["Launch kit", "CFO", "Buyer panel"],
|
|
skipped: [],
|
|
});
|
|
expect(prisma.store.map((row) => row.source)).toEqual(["user", "user", "user"]);
|
|
expect(
|
|
calls.every((url) => /^https:\/\/(api\.github\.com|raw\.githubusercontent\.com)\//.test(url)),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("limits a folder link to that folder and skips invalid or clashing skills", async () => {
|
|
const { fetchImpl } = pretendGithub({
|
|
"skills/cfo/SKILL.md": skill("CFO"),
|
|
"skills/broken/SKILL.md": "no frontmatter",
|
|
"skills/zz-copy/SKILL.md": skill("cfo"),
|
|
"other/SKILL.md": skill("Elsewhere"),
|
|
});
|
|
const prisma = makePrisma();
|
|
const result = await skillInstallFromTool(
|
|
prisma as never,
|
|
owner,
|
|
{ url: "https://github.com/someone/founder-kit/tree/main/skills" },
|
|
fetchImpl,
|
|
);
|
|
expect(result).toMatchObject({ ok: true, installed: ["CFO"] });
|
|
expect((result.skipped as Array<{ path: string }>).map((s) => s.path)).toEqual([
|
|
"skills/broken/SKILL.md",
|
|
"skills/zz-copy/SKILL.md",
|
|
]);
|
|
});
|
|
|
|
it("explains links that hold no skill", async () => {
|
|
const { fetchImpl } = pretendGithub({});
|
|
await expect(
|
|
fetchGithubSkills("https://github.com/someone/founder-kit", fetchImpl),
|
|
).rejects.toThrow("No SKILL.md found");
|
|
expect(
|
|
await skillInstallFromTool(makePrisma() as never, owner, { url: "https://example.com/x/y" }),
|
|
).toEqual({ error: "Paste a github.com link to a repository, folder or SKILL.md." });
|
|
expect(
|
|
await skillInstallFromTool(
|
|
makePrisma() as never,
|
|
owner,
|
|
{ url: "https://github.com/nobody/missing" },
|
|
pretendGithub({}).fetchImpl,
|
|
),
|
|
).toEqual({ error: "Repository not found or not public." });
|
|
});
|
|
});
|