From f80a86796ad9112f718d64fa2f34bcb08bf8ecd2 Mon Sep 17 00:00:00 2001 From: owenkephart Date: Tue, 4 Aug 2026 19:46:22 +0000 Subject: [PATCH 1/3] feat(eve): add skills.sh to /add Signed-off-by: owenkephart --- .changeset/green-skills-sh.md | 5 + docs/install-integrations.mdx | 12 +- docs/reference/cli.md | 2 +- packages/eve/package.json | 2 +- .../eve/src/cli/commands/registry-project.ts | 2 +- .../eve/src/cli/commands/registry.test.ts | 233 ++++++++++++++---- packages/eve/src/cli/commands/registry.ts | 208 ++++++++++++---- pnpm-lock.yaml | 11 +- 8 files changed, 374 insertions(+), 101 deletions(-) create mode 100644 .changeset/green-skills-sh.md diff --git a/.changeset/green-skills-sh.md b/.changeset/green-skills-sh.md new file mode 100644 index 000000000..c8ec44909 --- /dev/null +++ b/.changeset/green-skills-sh.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Add skills.sh as a built-in registry, so `eve registry search --registry @skills ` and `eve add @skills/` work without project configuration. diff --git a/docs/install-integrations.mdx b/docs/install-integrations.mdx index 26dca286d..666c91b83 100644 --- a/docs/install-integrations.mdx +++ b/docs/install-integrations.mdx @@ -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 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 4be60b18e..e4d4af791 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -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 --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 ` 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 ` 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` diff --git a/packages/eve/package.json b/packages/eve/package.json index 443538c7f..b0c3eb3da 100644 --- a/packages/eve/package.json +++ b/packages/eve/package.json @@ -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", diff --git a/packages/eve/src/cli/commands/registry-project.ts b/packages/eve/src/cli/commands/registry-project.ts index e02fdcc07..efffaa4c3 100644 --- a/packages/eve/src/cli/commands/registry-project.ts +++ b/packages/eve/src/cli/commands/registry-project.ts @@ -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); diff --git a/packages/eve/src/cli/commands/registry.test.ts b/packages/eve/src/cli/commands/registry.test.ts index c84725b71..b589de740 100644 --- a/packages/eve/src/cli/commands/registry.test.ts +++ b/packages/eve/src/cli/commands/registry.test.ts @@ -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", @@ -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, @@ -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" }), ); }); @@ -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, }); @@ -501,11 +511,21 @@ 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 () => { + it("uses sanitized manifest titles and preserves explicit registry URLs in list output", async () => { const logger = createLogger(); + getRegistryItems.mockResolvedValue([{ title: "External\u001B]0;spoofed\u0007 Search" }]); searchRegistries.mockResolvedValue({ items: [ { @@ -522,53 +542,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)", + " External 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"), ]); }); @@ -587,10 +644,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 () => { @@ -622,11 +717,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"); @@ -652,14 +747,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 = { @@ -670,7 +777,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 () => { @@ -684,7 +800,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); }); @@ -718,7 +844,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([ diff --git a/packages/eve/src/cli/commands/registry.ts b/packages/eve/src/cli/commands/registry.ts index a12f0a471..a3b35eaed 100644 --- a/packages/eve/src/cli/commands/registry.ts +++ b/packages/eve/src/cli/commands/registry.ts @@ -117,6 +117,8 @@ export function resolveOfficialRegistryUrl( const OFFICIAL_REGISTRY = resolveOfficialRegistryUrl(); const OFFICIAL_CATALOG = `${OFFICIAL_REGISTRY}/registry.json`; +const SKILLS_REGISTRY = "@skills"; +const SKILLS_REGISTRY_URL = "https://www.skills.sh/r/{name}?agent=eve"; const CATALOG_PAGE_SIZE = 100; const DEFAULT_SEARCH_LIMIT = 10; @@ -134,7 +136,7 @@ export async function installOfficialRegistryItem( item: string, options: AddCommandOptions = {}, ): Promise { - const config = await readRegistryConfig(appRoot); + const config = await readEveRegistryConfig(appRoot); await addRegistryItems([itemAddress(item)], { config, cwd: appRoot, @@ -214,8 +216,19 @@ async function runRegistryAction( } } +function withBuiltInRegistries(config: RegistryConfig): RegistryConfig { + return { + ...config, + registries: { [SKILLS_REGISTRY]: SKILLS_REGISTRY_URL, ...config.registries }, + }; +} + +async function readEveRegistryConfig(appRoot: string): Promise { + return withBuiltInRegistries(await readRegistryConfig(appRoot)); +} + function configuredRegistrySources(config: RegistryConfig): string[] { - return Object.keys(config.registries ?? {}); + return Object.keys(config.registries ?? {}).filter((source) => source !== SKILLS_REGISTRY); } function validateRegistrySource(source: string | undefined): void { @@ -237,56 +250,93 @@ function registryDescriptionSummary(description: string): string { return normalized.match(/^.*?[.!?](?=\s|$)/u)?.[0] ?? normalized; } +function searchItemAddress(item: RegistrySearchItem): string { + const address = item.registry === OFFICIAL_CATALOG ? item.name : item.addCommandArgument; + return normalizeRegistryText(address); +} + +function searchItemFallbackTitle(item: RegistrySearchItem): string { + const name = normalizeRegistryText(item.name); + return name.split("/").at(-1) ?? name; +} + function renderSearchItem( item: RegistrySearchItem, + title: string | undefined, width: number, theme: ReturnType, ): string { - const rawAddress = item.registry === OFFICIAL_CATALOG ? item.name : item.addCommandArgument; - const address = theme.accent(normalizeRegistryText(rawAddress)); - if (!item.description) return address; + const valueWidth = Math.max(1, width - 4); + const addressLines = wrapVisibleLine(searchItemAddress(item), valueWidth); + const lines = [ + ` ${theme.label(title ?? searchItemFallbackTitle(item))}`, + ...addressLines.map((line) => ` ${line}`), + ]; + if (!item.description) return lines.join("\n"); const description = registryDescriptionSummary(item.description); - if (description.length === 0) return address; + if (description.length === 0) return lines.join("\n"); - const descriptionWidth = Math.max(1, width - 2); - const wrapped = wrapVisibleLine(description, descriptionWidth); - const lines = + const wrapped = wrapVisibleLine(description, valueWidth); + const descriptionLines = wrapped.length <= 2 ? wrapped - : [wrapped[0]!, `${clipVisible(wrapped[1]!, Math.max(1, descriptionWidth - 1)).trimEnd()}…`]; - return [address, ...lines.map((line) => theme.muted(` ${line}`))].join("\n"); + : [wrapped[0]!, `${clipVisible(wrapped[1]!, Math.max(1, valueWidth - 1)).trimEnd()}…`]; + lines.push(...descriptionLines.map((line) => theme.muted(` ${line}`))); + return lines.join("\n"); +} + +type RegistrySearchResult = Awaited>; + +function registrySourceLabel(source: string): string { + if (source === OFFICIAL_CATALOG) return "eve"; + if (source === SKILLS_REGISTRY) return "skills.sh"; + return source; } function printSearchResults( logger: RegistryCommandLogger, - result: Awaited>, - options: { query: string | undefined; sources: string[]; json?: boolean }, + result: RegistrySearchResult, + options: { + json?: boolean; + query: string | undefined; + resultsBySource: ReadonlyMap; + sources: string[]; + titles: ReadonlyMap; + }, ): void { if (options.json) { logger.log(JSON.stringify(result, null, 2)); return; } - if (result.items.length === 0) { - logger.log("No registry items found."); + const query = options.query && normalizeRegistryText(options.query); + logger.log(query ? `No registry items match "${query}".` : "No registry items found."); return; } - const total = result.pagination.total; - const count = `${total} item${total === 1 ? "" : "s"}`; - const query = - options.query === undefined ? "" : ` matching "${normalizeRegistryText(options.query)}"`; - const registries = `${options.sources.length} registr${options.sources.length === 1 ? "y" : "ies"}`; - const resultCount = result.items.length; - const summary = - resultCount < total - ? `Showing ${resultCount} of ${count}${query} in ${registries}` - : `Found ${count}${query} in ${registries}`; const theme = createCliTheme(); const width = Math.max(20, process.stdout.columns ?? 80); - const items = result.items.map((item) => renderSearchItem(item, width, theme)); - logger.log([summary, ...items].join("\n\n")); + const sections = options.sources.flatMap((source) => { + const sourceResult = options.resultsBySource.get(source); + if (sourceResult === undefined || sourceResult.items.length === 0) return []; + const { pagination } = sourceResult; + const count = `${pagination.total} result${pagination.total === 1 ? "" : "s"}`; + const detail = + sourceResult.items.length < pagination.total + ? `showing ${sourceResult.items.length} of ${count}` + : count; + const heading = `${theme.label(registrySourceLabel(source))} ${theme.muted(`(${detail})`)}`; + return [ + [ + heading, + ...sourceResult.items.map((item) => + renderSearchItem(item, options.titles.get(item.addCommandArgument), width, theme), + ), + ].join("\n"), + ]; + }); + logger.log(sections.join("\n")); } async function searchRegistryCatalog( @@ -294,17 +344,68 @@ async function searchRegistryCatalog( options: { limit?: number; query?: string; source?: string }, ) { validateRegistrySource(options.source); - const config = await readRegistryConfig(appRoot); + const config = await readEveRegistryConfig(appRoot); const sources = options.source ? [options.source] - : [OFFICIAL_CATALOG, ...configuredRegistrySources(config)]; - const result = await searchRegistries(sources, { - config, - continueOnError: sources.length > 1, - limit: options.limit ?? CATALOG_PAGE_SIZE, - query: options.query, - }); - return { config, result, sources }; + : [ + OFFICIAL_CATALOG, + ...configuredRegistrySources(config).filter( + (source) => options.query !== undefined || source !== SKILLS_REGISTRY, + ), + ]; + const responses = await Promise.all( + sources.map(async (source) => { + try { + return { + result: await searchRegistries([source], { + config, + limit: options.limit ?? CATALOG_PAGE_SIZE, + query: options.query, + }), + source, + }; + } catch (error) { + return { error, source }; + } + }), + ); + const errors: NonNullable = []; + const resultsBySource = new Map(); + for (const response of responses) { + if ("error" in response) { + errors.push({ message: errorMessage(response.error), registry: response.source }); + } else { + const sourceErrors = response.result.errors ?? []; + errors.push(...sourceErrors); + if (sourceErrors.length === 0) { + const items = response.result.items.filter((item) => item.registry === response.source); + resultsBySource.set(response.source, { + ...response.result, + items, + pagination: { + ...response.result.pagination, + total: + response.result.items.length === items.length ? response.result.pagination.total : 0, + }, + }); + } + } + } + const uniqueErrors = new Map( + errors.map((error) => [`${error.registry}\0${error.message}`, error]), + ); + const results = [...resultsBySource.values()]; + const result: RegistrySearchResult = { + items: results.flatMap((entry) => entry.items), + pagination: { + hasMore: results.some((entry) => entry.pagination.hasMore), + limit: options.limit ?? CATALOG_PAGE_SIZE, + offset: 0, + total: results.reduce((total, entry) => total + entry.pagination.total, 0), + }, + ...(uniqueErrors.size > 0 ? { errors: [...uniqueErrors.values()] } : {}), + }; + return { config, result, resultsBySource, sources }; } function registryManifestTitle(manifest: unknown): string | undefined { @@ -344,6 +445,26 @@ export async function browseRegistryCatalog( }; } +async function loadRegistrySearchTitles( + items: readonly RegistrySearchItem[], + config: RegistryConfig, +): Promise> { + const entries = await Promise.all( + items.map(async (item) => { + try { + const [manifest] = await getRegistryItems([item.addCommandArgument], { config }); + const title = registryManifestTitle(manifest); + return [item.addCommandArgument, title && normalizeRegistryText(title)] as const; + } catch { + return [item.addCommandArgument, undefined] as const; + } + }), + ); + return new Map( + entries.filter((entry): entry is readonly [string, string] => entry[1] !== undefined), + ); +} + async function browseRegistryItems( logger: RegistryCommandLogger, appRoot: string, @@ -351,14 +472,17 @@ async function browseRegistryItems( source: string | undefined, options: RegistrySearchCommandOptions = {}, ): Promise { - const { result, sources } = await searchRegistryCatalog(appRoot, { + const { config, result, resultsBySource, sources } = await searchRegistryCatalog(appRoot, { limit: options.limit, query, source, }); const errors = result.errors ?? []; - if (options.json || errors.length < sources.length) { - printSearchResults(logger, result, { ...options, query, sources }); + if (options.json || resultsBySource.size > 0) { + const titles = options.json + ? new Map() + : await loadRegistrySearchTitles(result.items, config); + printSearchResults(logger, result, { ...options, query, resultsBySource, sources, titles }); } for (const error of errors) { logger.error(`${error.registry}: ${error.message}`); @@ -368,7 +492,7 @@ async function browseRegistryItems( /** Resolves one official, configured, or URL-addressed item manifest. */ export async function getRegistryItemManifest(appRoot: string, item: string): Promise { - const config = await readRegistryConfig(appRoot); + const config = await readEveRegistryConfig(appRoot); const items = await getRegistryItems([itemAddress(item)], { config }); return items.length === 1 ? items[0] : items; } @@ -432,7 +556,7 @@ export async function runAddCommand( dependencies: AddCommandDependencies = defaultAddCommandDependencies, ): Promise { await runRegistryAction(logger, appRoot, async () => { - const config = await readRegistryConfig(appRoot); + const config = await readEveRegistryConfig(appRoot); const address = itemAddress(item); if (options.skipInstall === true) { if (options.overwrite === true) { @@ -558,7 +682,7 @@ export async function runRegistryViewCommand( item: string, ): Promise { await runRegistryAction(logger, appRoot, async () => { - const config = await readRegistryConfig(appRoot); + const config = await readEveRegistryConfig(appRoot); const items = await getRegistryItems([itemAddress(item)], { config }); logger.log(JSON.stringify(items.length === 1 ? items[0] : items, null, 2)); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3b989a8c..4e95497d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1394,8 +1394,8 @@ importers: specifier: 7.8.4 version: 7.8.4 shadcn: - specifier: 4.16.0 - version: 4.16.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(typescript@7.0.2) + specifier: 4.16.1 + version: 4.16.1(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(typescript@7.0.2) svelte: specifier: ^5.0.0 version: 5.56.1(@typescript-eslint/types@8.59.4) @@ -13875,6 +13875,11 @@ packages: engines: {node: '>=20.18.1'} hasBin: true + shadcn@4.16.1: + resolution: {integrity: sha512-XLFzfNNIUPlUlyheFEzj0H4Vnhi9nI0nl3Nfgg8HYXW1FkUVhVT1X+mgmOUW8aWL5SeG0A+yJIV5fm3Hr9MVkQ==} + engines: {node: '>=20.18.1'} + hasBin: true + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -30496,7 +30501,7 @@ snapshots: - supports-color - typescript - shadcn@4.16.0(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(typescript@7.0.2): + shadcn@4.16.1(@cfworker/json-schema@4.1.1)(supports-color@10.2.2)(typescript@7.0.2): dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/parser': 7.29.7 From ffb5a1440f545fd0346842cf8833c6043dc32b52 Mon Sep 17 00:00:00 2001 From: owenkephart Date: Tue, 4 Aug 2026 16:52:59 -0500 Subject: [PATCH 2/3] fix(eve): include skills in registry search Signed-off-by: owenkephart --- packages/eve/src/cli/commands/registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eve/src/cli/commands/registry.ts b/packages/eve/src/cli/commands/registry.ts index a3b35eaed..6764c9e3e 100644 --- a/packages/eve/src/cli/commands/registry.ts +++ b/packages/eve/src/cli/commands/registry.ts @@ -228,7 +228,7 @@ async function readEveRegistryConfig(appRoot: string): Promise { } function configuredRegistrySources(config: RegistryConfig): string[] { - return Object.keys(config.registries ?? {}).filter((source) => source !== SKILLS_REGISTRY); + return Object.keys(config.registries ?? {}); } function validateRegistrySource(source: string | undefined): void { From 752e971ea08816ca89c52b6e9303436442ccc4f7 Mon Sep 17 00:00:00 2001 From: owenkephart Date: Tue, 4 Aug 2026 17:00:53 -0500 Subject: [PATCH 3/3] perf(eve): avoid registry title lookups Signed-off-by: owenkephart --- .../eve/src/cli/commands/registry.test.ts | 5 +-- packages/eve/src/cli/commands/registry.ts | 40 +++---------------- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/packages/eve/src/cli/commands/registry.test.ts b/packages/eve/src/cli/commands/registry.test.ts index b589de740..780ad9a80 100644 --- a/packages/eve/src/cli/commands/registry.test.ts +++ b/packages/eve/src/cli/commands/registry.test.ts @@ -523,9 +523,8 @@ describe("registry commands", () => { ]); }); - it("uses sanitized manifest titles and preserves explicit registry URLs in list output", async () => { + it("preserves explicit registry URLs in list output", async () => { const logger = createLogger(); - getRegistryItems.mockResolvedValue([{ title: "External\u001B]0;spoofed\u0007 Search" }]); searchRegistries.mockResolvedValue({ items: [ { @@ -543,7 +542,7 @@ describe("registry commands", () => { expect(logger.logs).toEqual([ [ "https://example.com/r/registry.json (1 result)", - " External Search", + " search", " https://example.com/r/search.json", " External search tools", ].join("\n"), diff --git a/packages/eve/src/cli/commands/registry.ts b/packages/eve/src/cli/commands/registry.ts index 6764c9e3e..26608a79e 100644 --- a/packages/eve/src/cli/commands/registry.ts +++ b/packages/eve/src/cli/commands/registry.ts @@ -262,14 +262,13 @@ function searchItemFallbackTitle(item: RegistrySearchItem): string { function renderSearchItem( item: RegistrySearchItem, - title: string | undefined, width: number, theme: ReturnType, ): string { const valueWidth = Math.max(1, width - 4); const addressLines = wrapVisibleLine(searchItemAddress(item), valueWidth); const lines = [ - ` ${theme.label(title ?? searchItemFallbackTitle(item))}`, + ` ${theme.label(searchItemFallbackTitle(item))}`, ...addressLines.map((line) => ` ${line}`), ]; if (!item.description) return lines.join("\n"); @@ -302,7 +301,6 @@ function printSearchResults( query: string | undefined; resultsBySource: ReadonlyMap; sources: string[]; - titles: ReadonlyMap; }, ): void { if (options.json) { @@ -328,12 +326,9 @@ function printSearchResults( : count; const heading = `${theme.label(registrySourceLabel(source))} ${theme.muted(`(${detail})`)}`; return [ - [ - heading, - ...sourceResult.items.map((item) => - renderSearchItem(item, options.titles.get(item.addCommandArgument), width, theme), - ), - ].join("\n"), + [heading, ...sourceResult.items.map((item) => renderSearchItem(item, width, theme))].join( + "\n", + ), ]; }); logger.log(sections.join("\n")); @@ -445,26 +440,6 @@ export async function browseRegistryCatalog( }; } -async function loadRegistrySearchTitles( - items: readonly RegistrySearchItem[], - config: RegistryConfig, -): Promise> { - const entries = await Promise.all( - items.map(async (item) => { - try { - const [manifest] = await getRegistryItems([item.addCommandArgument], { config }); - const title = registryManifestTitle(manifest); - return [item.addCommandArgument, title && normalizeRegistryText(title)] as const; - } catch { - return [item.addCommandArgument, undefined] as const; - } - }), - ); - return new Map( - entries.filter((entry): entry is readonly [string, string] => entry[1] !== undefined), - ); -} - async function browseRegistryItems( logger: RegistryCommandLogger, appRoot: string, @@ -472,17 +447,14 @@ async function browseRegistryItems( source: string | undefined, options: RegistrySearchCommandOptions = {}, ): Promise { - const { config, result, resultsBySource, sources } = await searchRegistryCatalog(appRoot, { + const { result, resultsBySource, sources } = await searchRegistryCatalog(appRoot, { limit: options.limit, query, source, }); const errors = result.errors ?? []; if (options.json || resultsBySource.size > 0) { - const titles = options.json - ? new Map() - : await loadRegistrySearchTitles(result.items, config); - printSearchResults(logger, result, { ...options, query, resultsBySource, sources, titles }); + printSearchResults(logger, result, { ...options, query, resultsBySource, sources }); } for (const error of errors) { logger.error(`${error.registry}: ${error.message}`);