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
5 changes: 5 additions & 0 deletions .changeset/green-skills-sh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Add skills.sh as a built-in registry, so `eve registry search --registry @skills <query>` and `eve add @skills/<skill>` work without project configuration.
12 changes: 11 additions & 1 deletion docs/install-integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,17 @@ Inspect an integration before you install it:
eve registry view extension/agent-browser
```

`list` and `search` include the official eve catalog and every source you add to the project.
`list` includes the official eve catalog and every source you add to the project. `search` also includes [skills.sh](https://skills.sh), available as the built-in `@skills` source.

## Add a skill

Add a known [skills.sh](https://skills.sh) item directly:

```bash
eve add @skills/vercel-labs/agent-skills/vercel-react-best-practices
```

Skills from skills.sh are community-authored project files. Review their source and the resulting diff before you run your agent.

## Add a third-party source

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ eve add @acme/my-extension

`eve add` asks before running setup declared by an official item and prints the matching `eve add <item> --skip-install` command when setup is skipped or cancelled. `--skip-install` reruns setup without reinstalling the item.

`eve registry add` records configured sources in `package.json#registries`. `eve registry list` and `search` aggregate the official catalog and all configured sources by default, or browse one supplied URL or namespace. Search returns up to 10 matches by default; pass `--limit <count>` to request between 1 and 100. Official and other universal items with explicit file targets do not require shadcn project configuration.
`eve registry add` records configured sources in `package.json#registries`. `eve registry list` aggregates the official catalog and all configured sources by default. `eve registry search` also includes [skills.sh](https://skills.sh), available without configuration at `@skills`, and groups results by source with each source's available result count. Search returns up to 10 matches per source by default; pass `--limit <count>` to request between 1 and 100. Either command can browse one supplied URL or namespace. Official and other universal items with explicit file targets do not require shadcn project configuration.

## `eve info`

Expand Down
2 changes: 1 addition & 1 deletion packages/eve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@
"react": "catalog:",
"react-test-renderer": "19.2.6",
"semver": "7.8.4",
"shadcn": "4.16.0",
"shadcn": "4.16.1",
"svelte": "^5.0.0",
"turndown": "7.2.4",
"vite": "^8.1.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/eve/src/cli/commands/registry-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export async function addRegistryMappings(
};

for (const mapping of mappings) {
if (mapping.namespace === "@shadcn") {
if (mapping.namespace === "@shadcn" || mapping.namespace === "@skills") {
result.skippedBuiltIn.push(mapping.namespace);
} else if (configured[mapping.namespace] !== undefined) {
result.skippedExisting.push(mapping.namespace);
Expand Down
230 changes: 179 additions & 51 deletions packages/eve/src/cli/commands/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe("registry commands", () => {
vi.fn(async () => new Response(JSON.stringify({ items: [] }))),
);
isEveProject.mockResolvedValue(true);
getRegistryItems.mockResolvedValue([]);
readFile.mockResolvedValue(
JSON.stringify({
name: "project",
Expand Down Expand Up @@ -101,12 +102,18 @@ describe("registry commands", () => {

expect(getRegistryItems).toHaveBeenCalledWith(["https://eve.dev/r/extension/browser.json"], {
config: {
registries: { "@acme": "https://example.com/r/{name}.json" },
registries: {
"@skills": "https://www.skills.sh/r/{name}?agent=eve",
"@acme": "https://example.com/r/{name}.json",
},
},
});
expect(addRegistryItems).toHaveBeenCalledWith(["https://eve.dev/r/extension/browser.json"], {
config: {
registries: { "@acme": "https://example.com/r/{name}.json" },
registries: {
"@skills": "https://www.skills.sh/r/{name}?agent=eve",
"@acme": "https://example.com/r/{name}.json",
},
},
cwd: "/project",
overwrite: true,
Expand Down Expand Up @@ -467,7 +474,11 @@ describe("registry commands", () => {
],
});
expect(searchRegistries).toHaveBeenCalledWith(
["https://eve.dev/r/registry.json", "@acme"],
["https://eve.dev/r/registry.json"],
expect.objectContaining({ limit: 100, query: "sdk" }),
);
expect(searchRegistries).toHaveBeenCalledWith(
["@acme"],
expect.objectContaining({ limit: 100, query: "sdk" }),
);
});
Expand All @@ -483,8 +494,7 @@ describe("registry commands", () => {
await runRegistryListCommand(logger, "/project");

expect(searchRegistries).toHaveBeenCalledWith(["https://eve.dev/r/registry.json"], {
config: { registries: {} },
continueOnError: false,
config: { registries: { "@skills": "https://www.skills.sh/r/{name}?agent=eve" } },
limit: 100,
query: undefined,
});
Expand All @@ -501,7 +511,16 @@ describe("registry commands", () => {

await runRegistryListCommand(logger, "/project", undefined, { json: true });

expect(logger.logs).toEqual([JSON.stringify(result, null, 2)]);
expect(logger.logs).toEqual([
JSON.stringify(
{
...result,
pagination: { hasMore: false, limit: 100, offset: 0, total: 1 },
},
null,
2,
),
]);
});

it("preserves explicit registry URLs in list output", async () => {
Expand All @@ -522,53 +541,90 @@ describe("registry commands", () => {

expect(logger.logs).toEqual([
[
"Found 1 item in 1 registry",
"",
"https://example.com/r/search.json",
" External search tools",
"https://example.com/r/registry.json (1 result)",
" search",
" https://example.com/r/search.json",
" External search tools",
].join("\n"),
]);
});

it("searches the official catalog and configured registries", async () => {
it("segments search results by source and shows each source's available results", async () => {
const logger = createLogger();
searchRegistries.mockResolvedValue({
items: [
{
registry: "https://eve.dev/r/registry.json",
name: "extension/agent-browser",
addCommandArgument: "https://eve.dev/r/extension/agent-browser.json",
description: "Browser automation",
},
{
registry: "@acme",
name: "browser",
addCommandArgument: "@acme/browser",
description: "Browser tools",
},
],
pagination: { total: 2, offset: 0, limit: 2, hasMore: false },
searchRegistries.mockImplementation(async ([source]: string[]) => {
if (source === "https://eve.dev/r/registry.json") {
return {
items: [
{
registry: source,
name: "extension/agent-browser",
addCommandArgument: "https://eve.dev/r/extension/agent-browser.json",
description: "Browser automation",
},
],
pagination: { total: 1, offset: 0, limit: 10, hasMore: false },
};
}
if (source === "@skills") {
return {
items: [
{
registry: source,
name: "browser",
addCommandArgument: "@skills/example/browser",
description: "Browser skills",
},
],
pagination: { total: 200, offset: 0, limit: 10, hasMore: true },
};
}
return {
items: [
{
registry: source!,
name: "browser",
addCommandArgument: "@acme/browser",
description: "Browser tools",
},
],
pagination: { total: 1, offset: 0, limit: 10, hasMore: false },
};
});

await runRegistrySearchCommand(logger, "/project", "browser");

expect(searchRegistries).toHaveBeenCalledWith(["https://eve.dev/r/registry.json", "@acme"], {
expect(searchRegistries).toHaveBeenCalledWith(["https://eve.dev/r/registry.json"], {
config: {
registries: { "@acme": "https://example.com/r/{name}.json" },
registries: {
"@skills": "https://www.skills.sh/r/{name}?agent=eve",
"@acme": "https://example.com/r/{name}.json",
},
},
continueOnError: true,
limit: 10,
query: "browser",
});
expect(searchRegistries).toHaveBeenCalledWith(
["@skills"],
expect.objectContaining({ limit: 10 }),
);
expect(searchRegistries).toHaveBeenCalledWith(
["@acme"],
expect.objectContaining({ limit: 10 }),
);
expect(logger.logs).toEqual([
[
'Found 2 items matching "browser" in 2 registries',
"",
"extension/agent-browser",
" Browser automation",
"",
"@acme/browser",
" Browser tools",
"eve (1 result)",
" agent-browser",
" extension/agent-browser",
" Browser automation",
"skills.sh (showing 1 of 200 results)",
" browser",
" @skills/example/browser",
" Browser skills",
"@acme (1 result)",
" browser",
" @acme/browser",
" Browser tools",
].join("\n"),
]);
});
Expand All @@ -587,10 +643,48 @@ describe("registry commands", () => {
await runRegistrySearchCommand(logger, "/project", "web", undefined, { limit: 5 });

expect(searchRegistries).toHaveBeenCalledWith(
["https://eve.dev/r/registry.json", "@acme"],
["https://eve.dev/r/registry.json"],
expect.objectContaining({ limit: 5, query: "web" }),
);
expect(logger.logs[0]).toMatch(/^Showing 5 of 21 items matching "web" in 2 registries/);
expect(logger.logs[0]).toMatch(/^@acme \(showing 5 of 21 results\)/);
});

it("puts descriptions below long addresses instead of creating a narrow second column", async () => {
const logger = createLogger();
const columnsDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "columns");
Object.defineProperty(process.stdout, "columns", { configurable: true, value: 120 });
searchRegistries.mockResolvedValue({
items: [
{
registry: "@skills",
name: "vercel-react-best-practices",
addCommandArgument:
"@skills/vercel-labs/agent-skills/vercel-react-best-practices-with-a-long-name",
description:
"React and Next.js performance optimization guidelines from Vercel Engineering.",
},
],
pagination: { total: 200, offset: 0, limit: 10, hasMore: true },
});

try {
await runRegistrySearchCommand(logger, "/project", "react", "@skills");
} finally {
if (columnsDescriptor === undefined) {
Reflect.deleteProperty(process.stdout, "columns");
} else {
Object.defineProperty(process.stdout, "columns", columnsDescriptor);
}
}

expect(logger.logs).toEqual([
[
"skills.sh (showing 1 of 200 results)",
" vercel-react-best-practices",
" @skills/vercel-labs/agent-skills/vercel-react-best-practices-with-a-long-name",
" React and Next.js performance optimization guidelines from Vercel Engineering.",
].join("\n"),
]);
});

it("sanitizes and wraps registry descriptions beneath their addresses", async () => {
Expand Down Expand Up @@ -622,11 +716,11 @@ describe("registry commands", () => {

expect(logger.logs).toEqual([
[
'Found 1 item matching "browser" in 2 registries',
"",
"@acme/browser",
" A long registry description that wraps",
" cleanly beneath its address.",
"@acme (1 result)",
" browser",
" @acme/browser",
" A long registry description that",
" wraps cleanly beneath its address.",
].join("\n"),
]);
expect(logger.logs[0]).not.toContain("spoofed");
Expand All @@ -652,14 +746,26 @@ describe("registry commands", () => {

expect(logger.logs).toEqual([
[
'Found 1 item matching "resources" in 2 registries',
"",
"@acme/resources",
' Use when asked to "list resources".',
"@acme (1 result)",
" resources",
" @acme/resources",
' Use when asked to "list resources".',
].join("\n"),
]);
});

it("names the active filter in an empty search result", async () => {
const logger = createLogger();
searchRegistries.mockResolvedValue({
items: [],
pagination: { total: 0, offset: 0, limit: 10, hasMore: false },
});

await runRegistrySearchCommand(logger, "/project", "missing");

expect(logger.logs).toEqual(['No registry items match "missing".']);
});

it("emits search results as JSON", async () => {
const logger = createLogger();
const result = {
Expand All @@ -670,7 +776,16 @@ describe("registry commands", () => {

await runRegistrySearchCommand(logger, "/project", "browser", undefined, { json: true });

expect(logger.logs).toEqual([JSON.stringify(result, null, 2)]);
expect(logger.logs).toEqual([
JSON.stringify(
{
...result,
pagination: { hasMore: false, limit: 10, offset: 0, total: 1 },
},
null,
2,
),
]);
});

it("emits JSON when every registry search fails", async () => {
Expand All @@ -684,7 +799,17 @@ describe("registry commands", () => {

await runRegistryListCommand(logger, "/project", undefined, { json: true });

expect(logger.logs).toEqual([JSON.stringify(result, null, 2)]);
expect(logger.logs).toEqual([
JSON.stringify(
{
items: [],
pagination: { hasMore: false, limit: 100, offset: 0, total: 0 },
errors: result.errors,
},
null,
2,
),
]);
expect(logger.errors).toEqual(["https://eve.dev/r/registry.json: eve unavailable"]);
expect(process.exitCode).toBe(1);
});
Expand Down Expand Up @@ -718,7 +843,10 @@ describe("registry commands", () => {

expect(getRegistryItems).toHaveBeenCalledWith(["https://eve.dev/r/extension/browser.json"], {
config: {
registries: { "@acme": "https://example.com/r/{name}.json" },
registries: {
"@skills": "https://www.skills.sh/r/{name}?agent=eve",
"@acme": "https://example.com/r/{name}.json",
},
},
});
expect(logger.logs).toEqual([
Expand Down
Loading