feat: add skills extraction to --from-af import (#823)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
cthomas
2026-02-05 12:46:42 -08:00
committed by GitHub
parent 22243c9296
commit d786ad470a
8 changed files with 761 additions and 9 deletions

View File

@@ -0,0 +1,82 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { packageSkills } from "../../agent/export";
describe("packageSkills from .skills/ directory", () => {
const testDir = join(process.cwd(), ".test-skills-export");
const skillsDir = join(testDir, ".skills");
const originalCwd = process.cwd();
beforeEach(() => {
mkdirSync(testDir, { recursive: true });
process.chdir(testDir);
});
afterEach(() => {
process.chdir(originalCwd);
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
test("packages single skill", async () => {
mkdirSync(join(skillsDir, "test-skill"), { recursive: true });
writeFileSync(
join(skillsDir, "test-skill", "SKILL.md"),
"---\nname: test-skill\ndescription: Test\n---\n\n# Test Skill",
);
writeFileSync(join(skillsDir, "test-skill", "config.yaml"), "version: 1.0");
const skills = await packageSkills(undefined, skillsDir);
expect(skills).toHaveLength(1);
expect(skills[0]?.name).toBe("test-skill");
expect(skills[0]?.files?.["SKILL.md"]).toContain("Test Skill");
expect(skills[0]?.files?.["config.yaml"]).toBe("version: 1.0");
});
test("packages multiple skills", async () => {
for (const name of ["skill-one", "skill-two"]) {
mkdirSync(join(skillsDir, name), { recursive: true });
writeFileSync(join(skillsDir, name, "SKILL.md"), `# ${name}`);
}
const skills = await packageSkills(undefined, skillsDir);
expect(skills).toHaveLength(2);
expect(skills.map((s) => s.name).sort()).toEqual([
"skill-one",
"skill-two",
]);
});
test("includes nested files", async () => {
mkdirSync(join(skillsDir, "nested-skill", "scripts"), { recursive: true });
writeFileSync(join(skillsDir, "nested-skill", "SKILL.md"), "# Nested");
writeFileSync(
join(skillsDir, "nested-skill", "scripts", "run.sh"),
"#!/bin/bash\necho hello",
);
const skills = await packageSkills(undefined, skillsDir);
expect(skills).toHaveLength(1);
expect(skills[0]?.files?.["SKILL.md"]).toBeDefined();
expect(skills[0]?.files?.["scripts/run.sh"]).toBeDefined();
});
test("skips skills without SKILL.md", async () => {
mkdirSync(join(skillsDir, "invalid-skill"), { recursive: true });
writeFileSync(join(skillsDir, "invalid-skill", "README.md"), "No SKILL.md");
const skills = await packageSkills(undefined, skillsDir);
expect(skills).toHaveLength(0);
});
test("returns empty array when .skills/ missing", async () => {
const skills = await packageSkills(undefined, skillsDir);
expect(skills).toEqual([]);
});
});

View File

@@ -0,0 +1,215 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import { extractSkillsFromAf } from "../../agent/import";
describe("skills extraction from .af files", () => {
const testDir = join(process.cwd(), ".test-skills-import");
const skillsDir = join(testDir, ".skills");
const afPath = join(testDir, "test-agent.af");
const originalCwd = process.cwd();
beforeEach(() => {
mkdirSync(testDir, { recursive: true });
process.chdir(testDir);
});
afterEach(() => {
process.chdir(originalCwd);
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
test("extracts single skill with multiple files", async () => {
const afContent = {
agents: [],
blocks: [],
sources: [],
tools: [],
mcp_servers: [],
skills: [
{
name: "test-skill",
files: {
"SKILL.md":
"---\nname: test-skill\ndescription: A test skill\n---\n\n# Test Skill\n\nThis is a test.",
"scripts/hello": "#!/bin/bash\necho 'Hello from test skill'",
"config.yaml": "version: 1.0\nfeatures:\n - testing",
},
},
],
};
writeFileSync(afPath, JSON.stringify(afContent, null, 2));
const extracted = await extractSkillsFromAf(afPath, skillsDir);
expect(extracted).toEqual(["test-skill"]);
expect(existsSync(join(skillsDir, "test-skill", "SKILL.md"))).toBe(true);
expect(existsSync(join(skillsDir, "test-skill", "scripts", "hello"))).toBe(
true,
);
expect(existsSync(join(skillsDir, "test-skill", "config.yaml"))).toBe(true);
const skillContent = await readFile(
join(skillsDir, "test-skill", "SKILL.md"),
"utf-8",
);
expect(skillContent).toContain("Test Skill");
// Check executable permissions (skip on Windows - chmod not supported)
if (process.platform !== "win32") {
const scriptStats = await stat(
join(skillsDir, "test-skill", "scripts", "hello"),
);
expect(scriptStats.mode & 0o111).not.toBe(0);
}
});
test("extracts skill with source_url metadata", async () => {
const afContent = {
agents: [],
blocks: [],
sources: [],
tools: [],
mcp_servers: [],
skills: [
{
name: "slack",
files: {
"SKILL.md":
"---\nname: slack\ndescription: Slack integration\n---\n\n# Slack Skill",
"scripts/slack": "#!/bin/bash\necho 'Slack CLI'",
},
source_url: "letta-ai/skills/tools/slack",
},
],
};
writeFileSync(afPath, JSON.stringify(afContent, null, 2));
const extracted = await extractSkillsFromAf(afPath, skillsDir);
expect(extracted).toEqual(["slack"]);
expect(existsSync(join(skillsDir, "slack", "SKILL.md"))).toBe(true);
expect(existsSync(join(skillsDir, "slack", "scripts", "slack"))).toBe(true);
});
test("overwrites existing skills", async () => {
mkdirSync(join(skillsDir, "existing-skill"), { recursive: true });
writeFileSync(
join(skillsDir, "existing-skill", "SKILL.md"),
"# Old Version\n\nThis will be overwritten.",
);
const afContent = {
agents: [],
blocks: [],
sources: [],
tools: [],
mcp_servers: [],
skills: [
{
name: "existing-skill",
files: {
"SKILL.md": "# New Version\n\nThis is the updated version.",
},
},
],
};
writeFileSync(afPath, JSON.stringify(afContent, null, 2));
const extracted = await extractSkillsFromAf(afPath, skillsDir);
expect(extracted).toEqual(["existing-skill"]);
const newContent = await readFile(
join(skillsDir, "existing-skill", "SKILL.md"),
"utf-8",
);
expect(newContent).toContain("New Version");
expect(newContent).not.toContain("Old Version");
});
test("handles multiple skills", async () => {
const afContent = {
agents: [],
blocks: [],
sources: [],
tools: [],
mcp_servers: [],
skills: [
{
name: "skill-one",
files: {
"SKILL.md": "# Skill One",
},
},
{
name: "skill-two",
files: {
"SKILL.md": "# Skill Two",
},
},
{
name: "skill-three",
files: {
"SKILL.md": "# Skill Three",
},
},
],
};
writeFileSync(afPath, JSON.stringify(afContent, null, 2));
const extracted = await extractSkillsFromAf(afPath, skillsDir);
expect(extracted).toEqual(["skill-one", "skill-two", "skill-three"]);
expect(existsSync(join(skillsDir, "skill-one", "SKILL.md"))).toBe(true);
expect(existsSync(join(skillsDir, "skill-two", "SKILL.md"))).toBe(true);
expect(existsSync(join(skillsDir, "skill-three", "SKILL.md"))).toBe(true);
});
test("handles .af without skills", async () => {
const afContent = {
agents: [],
blocks: [],
sources: [],
tools: [],
mcp_servers: [],
skills: [],
};
writeFileSync(afPath, JSON.stringify(afContent, null, 2));
const extracted = await extractSkillsFromAf(afPath, skillsDir);
expect(extracted).toEqual([]);
});
test("fetches skill from remote source_url (integration)", async () => {
const afContent = {
agents: [],
blocks: [],
sources: [],
tools: [],
mcp_servers: [],
skills: [
{
name: "imessage",
source_url: "letta-ai/skills/main/tools/imessage",
},
],
};
writeFileSync(afPath, JSON.stringify(afContent, null, 2));
const extracted = await extractSkillsFromAf(afPath, skillsDir);
expect(extracted).toEqual(["imessage"]);
expect(existsSync(join(skillsDir, "imessage", "SKILL.md"))).toBe(true);
});
});