Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/web/src/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ export default function DocsPage() {
<InlineCode>rules/</InlineCode>, for skills{" "}
<InlineCode>.agents/skills</InlineCode> then{" "}
<InlineCode>skills/</InlineCode> then a root{" "}
<InlineCode>SKILL.md</InlineCode> fallback.
<InlineCode>SKILL.md</InlineCode> fallback, then root{" "}
<InlineCode>&lt;name&gt;/SKILL.md</InlineCode> directories.
</Prose>
</section>

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Source path resolution is additive and priority-ordered:
- Agents: `.agents/agents` -> `agents`
- Commands: `.agents/commands` -> `commands` -> `prompts` -> provider fallbacks `.github/prompts` + `.gemini/commands`
- Rules: `.agents/rules` -> `rules`
- Skills: `.agents/skills` -> `skills` -> root `SKILL.md` fallback
- Skills: `.agents/skills` -> `skills` -> root `SKILL.md` -> root `<name>/SKILL.md` fallback
- MCP: `.agents/mcp.json` -> `mcp.json`

Aggregate `agentloom add <source>` can import command/skill/MCP-only repositories even when no `agents/` directory exists.
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentloom",
"version": "0.1.10",
"version": "0.1.11",
"description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/core/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Source discovery:
agents: .agents/agents -> agents
commands: .agents/commands -> commands -> prompts -> (.github/prompts + .gemini/commands fallback)
rules: .agents/rules -> rules
skills: .agents/skills -> skills -> root SKILL.md
skills: .agents/skills -> skills -> root SKILL.md -> root <name>/SKILL.md

Usage:
agentloom add <source> [options]
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/core/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export async function importSource(
sourceSkillsDirs.length === 0
) {
throw new Error(
`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md, including plugin sources declared in .claude-plugin/marketplace.json).`,
`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, root SKILL.md, or root <name>/SKILL.md directories, including plugin sources declared in .claude-plugin/marketplace.json).`,
);
}
if (
Expand All @@ -313,7 +313,7 @@ export async function importSource(
Object.keys(sourceMcp?.mcpServers ?? {}).length === 0
) {
throw new Error(
`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, or plugin sources from .claude-plugin/marketplace.json.`,
`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, root <name>/SKILL.md directories, or plugin sources from .claude-plugin/marketplace.json.`,
);
}

Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/core/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ function discoverSourceSkillsDirsForRoot(importRoot: string): string[] {
return [importRoot];
}

if (hasImmediateRootSkillDirs(importRoot)) {
return [importRoot];
}

return [];
}

Expand All @@ -334,6 +338,21 @@ function dedupePaths(paths: string[]): string[] {
return [...new Set(paths)];
}

function hasImmediateRootSkillDirs(importRoot: string): boolean {
for (const entry of fs.readdirSync(importRoot, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}

const skillFile = path.join(importRoot, entry.name, "SKILL.md");
if (fs.existsSync(skillFile) && fs.statSync(skillFile).isFile()) {
return true;
}
}

return false;
}

function isPathWithinRoot(rootPath: string, targetPath: string): boolean {
const relative = path.relative(rootPath, targetPath);
return (
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/tests/unit/copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ describe("copy helpers", () => {

it("includes targeted add import help", () => {
const help = getAddHelpText();
expect(help).toContain(
"skills: .agents/skills -> skills -> root SKILL.md -> root <name>/SKILL.md",
);
expect(help).toContain("--agents <name>");
expect(help).toContain("--rule <name>");
expect(help).toContain("--rules <name>");
Expand Down
41 changes: 41 additions & 0 deletions packages/cli/tests/unit/importer-conflict.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,47 @@ name: visual-explainer
description: Explain visuals
---

Skill body.
`,
);

const paths = buildScopePaths(workspaceRoot, "local");
const summary = await importSource({
source: sourceRoot,
paths,
yes: true,
nonInteractive: true,
importAgents: false,
importCommands: false,
importMcp: false,
importSkills: true,
requireSkills: true,
skillSelectors: ["visual-explainer"],
});

expect(summary.importedSkills).toEqual(["visual-explainer"]);
expect(
fs.existsSync(path.join(paths.skillsDir, "visual-explainer", "SKILL.md")),
).toBe(true);
});

it("recognizes root <name>/SKILL.md directories and imports selected skills natively", async () => {
const sourceRoot = fs.mkdtempSync(
path.join(os.tmpdir(), "agentloom-source-"),
);
const workspaceRoot = fs.mkdtempSync(
path.join(os.tmpdir(), "agentloom-workspace-"),
);
tempDirs.push(sourceRoot, workspaceRoot);

ensureDir(path.join(sourceRoot, "visual-explainer"));
writeTextAtomic(
path.join(sourceRoot, "visual-explainer", "SKILL.md"),
`---
name: visual-explainer
description: Explain visuals
---

Skill body.
`,
);
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/tests/unit/sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ describe("source parsing and revision", () => {
expect(discoverSourceSkillsDir(root)).toBe(path.join(root, "skills"));
});

it("falls back to root <name>/SKILL.md directories when canonical skill paths are absent", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "agentloom-sources-"));
tempDirs.push(root);

ensureDir(path.join(root, "reviewer"));
writeTextAtomic(path.join(root, "reviewer", "SKILL.md"), "# reviewer\n");

expect(discoverSourceSkillsDirs(root)).toEqual([root]);
expect(discoverSourceSkillsDir(root)).toBe(root);

ensureDir(path.join(root, "skills"));
expect(discoverSourceSkillsDirs(root)).toEqual([path.join(root, "skills")]);
});

it("discovers plugin roots from .claude-plugin marketplace metadata", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "agentloom-sources-"));
tempDirs.push(root);
Expand Down
Loading