From c4aa7ec4b4f99f2f98ad464a9d4e7a39372698d2 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sun, 3 May 2026 00:56:42 -0600 Subject: [PATCH 1/3] feat(intent): add public read-only registry API at /api/v1/intent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five GET endpoints wrapping existing server fns / DB helpers so external consumers (e.g. the intent CLI) can search and resolve skills without scraping the registry UI. Markdown bodies aren't returned by default — responses include CDN URLs (unpkg + jsdelivr) pointing at the immutable npm tarball, so our egress stays near zero. Callers verify integrity via contentHash. The single-skill endpoint accepts ?include=markdown as an opt-in escape hatch. Auth-aware rate limiting: 60 req/min anonymous (IP-keyed) or 600 req/min authenticated (token-keyed) via the existing MCP bearer flow. Adds checkTokenRateLimit as a sibling to checkIpRateLimit. --- src/routeTree.gen.ts | 142 ++++++++++++++++++ src/routes/api/v1/intent/packages.$name.ts | 39 +++++ ...s.$name.versions.$version.skills.$skill.ts | 102 +++++++++++++ ...packages.$name.versions.$version.skills.ts | 61 ++++++++ src/routes/api/v1/intent/packages.ts | 41 +++++ src/routes/api/v1/intent/search.ts | 53 +++++++ src/utils/intent-api.server.ts | 138 +++++++++++++++++ src/utils/intent-db.server.ts | 2 + src/utils/rateLimit.server.ts | 32 +++- 9 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 src/routes/api/v1/intent/packages.$name.ts create mode 100644 src/routes/api/v1/intent/packages.$name.versions.$version.skills.$skill.ts create mode 100644 src/routes/api/v1/intent/packages.$name.versions.$version.skills.ts create mode 100644 src/routes/api/v1/intent/packages.ts create mode 100644 src/routes/api/v1/intent/search.ts create mode 100644 src/utils/intent-api.server.ts diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 1e88095e5..d3056d898 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -135,6 +135,8 @@ import { Route as IntentRegistryPackageNameIndexRouteImport } from './routes/int import { Route as LibraryIdVersionDocsIndexRouteImport } from './routes/$libraryId/$version.docs.index' import { Route as IntentRegistryPackageNameChar123Char125DotmdRouteImport } from './routes/intent/registry/$packageName.{$}[.]md' import { Route as IntentRegistryPackageNameSkillNameRouteImport } from './routes/intent/registry/$packageName.$skillName' +import { Route as ApiV1IntentSearchRouteImport } from './routes/api/v1/intent/search' +import { Route as ApiV1IntentPackagesRouteImport } from './routes/api/v1/intent/packages' import { Route as ApiBuilderDeployGithubRouteImport } from './routes/api/builder/deploy/github' import { Route as ApiBuilderDeployCheckNameRouteImport } from './routes/api/builder/deploy/check-name' import { Route as ApiAuthCliCreateTicketRouteImport } from './routes/api/auth/cli/create-ticket' @@ -145,11 +147,14 @@ import { Route as LibraryIdVersionDocsContributorsRouteImport } from './routes/$ import { Route as LibraryIdVersionDocsCommunityResourcesRouteImport } from './routes/$libraryId/$version.docs.community-resources' import { Route as LibraryIdVersionDocsSplatRouteImport } from './routes/$libraryId/$version.docs.$' import { Route as LibraryIdVersionDocsFrameworkIndexRouteImport } from './routes/$libraryId/$version.docs.framework.index' +import { Route as ApiV1IntentPackagesNameRouteImport } from './routes/api/v1/intent/packages.$name' import { Route as ApiAuthCliStatusTicketIdRouteImport } from './routes/api/auth/cli/status.$ticketId' import { Route as LibraryIdVersionDocsFrameworkFrameworkIndexRouteImport } from './routes/$libraryId/$version.docs.framework.$framework.index' import { Route as LibraryIdVersionDocsFrameworkFrameworkChar123Char125DotmdRouteImport } from './routes/$libraryId/$version.docs.framework.$framework.{$}[.]md' import { Route as LibraryIdVersionDocsFrameworkFrameworkSplatRouteImport } from './routes/$libraryId/$version.docs.framework.$framework.$' import { Route as LibraryIdVersionDocsFrameworkFrameworkExamplesSplatRouteImport } from './routes/$libraryId/$version.docs.framework.$framework.examples.$' +import { Route as ApiV1IntentPackagesNameVersionsVersionSkillsRouteImport } from './routes/api/v1/intent/packages.$name.versions.$version.skills' +import { Route as ApiV1IntentPackagesNameVersionsVersionSkillsSkillRouteImport } from './routes/api/v1/intent/packages.$name.versions.$version.skills.$skill' const WorkshopsRoute = WorkshopsRouteImport.update({ id: '/workshops', @@ -792,6 +797,16 @@ const IntentRegistryPackageNameSkillNameRoute = path: '/$skillName', getParentRoute: () => IntentRegistryPackageNameRoute, } as any) +const ApiV1IntentSearchRoute = ApiV1IntentSearchRouteImport.update({ + id: '/api/v1/intent/search', + path: '/api/v1/intent/search', + getParentRoute: () => rootRouteImport, +} as any) +const ApiV1IntentPackagesRoute = ApiV1IntentPackagesRouteImport.update({ + id: '/api/v1/intent/packages', + path: '/api/v1/intent/packages', + getParentRoute: () => rootRouteImport, +} as any) const ApiBuilderDeployGithubRoute = ApiBuilderDeployGithubRouteImport.update({ id: '/api/builder/deploy/github', path: '/api/builder/deploy/github', @@ -849,6 +864,11 @@ const LibraryIdVersionDocsFrameworkIndexRoute = path: '/framework/', getParentRoute: () => LibraryIdVersionDocsRoute, } as any) +const ApiV1IntentPackagesNameRoute = ApiV1IntentPackagesNameRouteImport.update({ + id: '/$name', + path: '/$name', + getParentRoute: () => ApiV1IntentPackagesRoute, +} as any) const ApiAuthCliStatusTicketIdRoute = ApiAuthCliStatusTicketIdRouteImport.update({ id: '/api/auth/cli/status/$ticketId', @@ -879,6 +899,18 @@ const LibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute = path: '/framework/$framework/examples/$', getParentRoute: () => LibraryIdVersionDocsRoute, } as any) +const ApiV1IntentPackagesNameVersionsVersionSkillsRoute = + ApiV1IntentPackagesNameVersionsVersionSkillsRouteImport.update({ + id: '/versions/$version/skills', + path: '/versions/$version/skills', + getParentRoute: () => ApiV1IntentPackagesNameRoute, + } as any) +const ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute = + ApiV1IntentPackagesNameVersionsVersionSkillsSkillRouteImport.update({ + id: '/$skill', + path: '/$skill', + getParentRoute: () => ApiV1IntentPackagesNameVersionsVersionSkillsRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -1012,16 +1044,21 @@ export interface FileRoutesByFullPath { '/api/auth/cli/create-ticket': typeof ApiAuthCliCreateTicketRoute '/api/builder/deploy/check-name': typeof ApiBuilderDeployCheckNameRoute '/api/builder/deploy/github': typeof ApiBuilderDeployGithubRoute + '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren + '/api/v1/intent/search': typeof ApiV1IntentSearchRoute '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute '/intent/registry/$packageName/': typeof IntentRegistryPackageNameIndexRoute '/api/auth/cli/status/$ticketId': typeof ApiAuthCliStatusTicketIdRoute + '/api/v1/intent/packages/$name': typeof ApiV1IntentPackagesNameRouteWithChildren '/$libraryId/$version/docs/framework/': typeof LibraryIdVersionDocsFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/$': typeof LibraryIdVersionDocsFrameworkFrameworkSplatRoute '/$libraryId/$version/docs/framework/$framework/{$}.md': typeof LibraryIdVersionDocsFrameworkFrameworkChar123Char125DotmdRoute '/$libraryId/$version/docs/framework/$framework/': typeof LibraryIdVersionDocsFrameworkFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/examples/$': typeof LibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute + '/api/v1/intent/packages/$name/versions/$version/skills': typeof ApiV1IntentPackagesNameVersionsVersionSkillsRouteWithChildren + '/api/v1/intent/packages/$name/versions/$version/skills/$skill': typeof ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -1145,16 +1182,21 @@ export interface FileRoutesByTo { '/api/auth/cli/create-ticket': typeof ApiAuthCliCreateTicketRoute '/api/builder/deploy/check-name': typeof ApiBuilderDeployCheckNameRoute '/api/builder/deploy/github': typeof ApiBuilderDeployGithubRoute + '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren + '/api/v1/intent/search': typeof ApiV1IntentSearchRoute '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version/docs': typeof LibraryIdVersionDocsIndexRoute '/intent/registry/$packageName': typeof IntentRegistryPackageNameIndexRoute '/api/auth/cli/status/$ticketId': typeof ApiAuthCliStatusTicketIdRoute + '/api/v1/intent/packages/$name': typeof ApiV1IntentPackagesNameRouteWithChildren '/$libraryId/$version/docs/framework': typeof LibraryIdVersionDocsFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/$': typeof LibraryIdVersionDocsFrameworkFrameworkSplatRoute '/$libraryId/$version/docs/framework/$framework/{$}.md': typeof LibraryIdVersionDocsFrameworkFrameworkChar123Char125DotmdRoute '/$libraryId/$version/docs/framework/$framework': typeof LibraryIdVersionDocsFrameworkFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/examples/$': typeof LibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute + '/api/v1/intent/packages/$name/versions/$version/skills': typeof ApiV1IntentPackagesNameVersionsVersionSkillsRouteWithChildren + '/api/v1/intent/packages/$name/versions/$version/skills/$skill': typeof ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -1289,16 +1331,21 @@ export interface FileRoutesById { '/api/auth/cli/create-ticket': typeof ApiAuthCliCreateTicketRoute '/api/builder/deploy/check-name': typeof ApiBuilderDeployCheckNameRoute '/api/builder/deploy/github': typeof ApiBuilderDeployGithubRoute + '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren + '/api/v1/intent/search': typeof ApiV1IntentSearchRoute '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute '/intent/registry/$packageName/': typeof IntentRegistryPackageNameIndexRoute '/api/auth/cli/status/$ticketId': typeof ApiAuthCliStatusTicketIdRoute + '/api/v1/intent/packages/$name': typeof ApiV1IntentPackagesNameRouteWithChildren '/$libraryId/$version/docs/framework/': typeof LibraryIdVersionDocsFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/$': typeof LibraryIdVersionDocsFrameworkFrameworkSplatRoute '/$libraryId/$version/docs/framework/$framework/{$}.md': typeof LibraryIdVersionDocsFrameworkFrameworkChar123Char125DotmdRoute '/$libraryId/$version/docs/framework/$framework/': typeof LibraryIdVersionDocsFrameworkFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/examples/$': typeof LibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute + '/api/v1/intent/packages/$name/versions/$version/skills': typeof ApiV1IntentPackagesNameVersionsVersionSkillsRouteWithChildren + '/api/v1/intent/packages/$name/versions/$version/skills/$skill': typeof ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -1434,16 +1481,21 @@ export interface FileRouteTypes { | '/api/auth/cli/create-ticket' | '/api/builder/deploy/check-name' | '/api/builder/deploy/github' + | '/api/v1/intent/packages' + | '/api/v1/intent/search' | '/intent/registry/$packageName/$skillName' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version/docs/' | '/intent/registry/$packageName/' | '/api/auth/cli/status/$ticketId' + | '/api/v1/intent/packages/$name' | '/$libraryId/$version/docs/framework/' | '/$libraryId/$version/docs/framework/$framework/$' | '/$libraryId/$version/docs/framework/$framework/{$}.md' | '/$libraryId/$version/docs/framework/$framework/' | '/$libraryId/$version/docs/framework/$framework/examples/$' + | '/api/v1/intent/packages/$name/versions/$version/skills' + | '/api/v1/intent/packages/$name/versions/$version/skills/$skill' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -1567,16 +1619,21 @@ export interface FileRouteTypes { | '/api/auth/cli/create-ticket' | '/api/builder/deploy/check-name' | '/api/builder/deploy/github' + | '/api/v1/intent/packages' + | '/api/v1/intent/search' | '/intent/registry/$packageName/$skillName' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version/docs' | '/intent/registry/$packageName' | '/api/auth/cli/status/$ticketId' + | '/api/v1/intent/packages/$name' | '/$libraryId/$version/docs/framework' | '/$libraryId/$version/docs/framework/$framework/$' | '/$libraryId/$version/docs/framework/$framework/{$}.md' | '/$libraryId/$version/docs/framework/$framework' | '/$libraryId/$version/docs/framework/$framework/examples/$' + | '/api/v1/intent/packages/$name/versions/$version/skills' + | '/api/v1/intent/packages/$name/versions/$version/skills/$skill' id: | '__root__' | '/' @@ -1710,16 +1767,21 @@ export interface FileRouteTypes { | '/api/auth/cli/create-ticket' | '/api/builder/deploy/check-name' | '/api/builder/deploy/github' + | '/api/v1/intent/packages' + | '/api/v1/intent/search' | '/intent/registry/$packageName/$skillName' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version/docs/' | '/intent/registry/$packageName/' | '/api/auth/cli/status/$ticketId' + | '/api/v1/intent/packages/$name' | '/$libraryId/$version/docs/framework/' | '/$libraryId/$version/docs/framework/$framework/$' | '/$libraryId/$version/docs/framework/$framework/{$}.md' | '/$libraryId/$version/docs/framework/$framework/' | '/$libraryId/$version/docs/framework/$framework/examples/$' + | '/api/v1/intent/packages/$name/versions/$version/skills' + | '/api/v1/intent/packages/$name/versions/$version/skills/$skill' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -1811,6 +1873,8 @@ export interface RootRouteChildren { ApiAuthCliCreateTicketRoute: typeof ApiAuthCliCreateTicketRoute ApiBuilderDeployCheckNameRoute: typeof ApiBuilderDeployCheckNameRoute ApiBuilderDeployGithubRoute: typeof ApiBuilderDeployGithubRoute + ApiV1IntentPackagesRoute: typeof ApiV1IntentPackagesRouteWithChildren + ApiV1IntentSearchRoute: typeof ApiV1IntentSearchRoute ApiAuthCliStatusTicketIdRoute: typeof ApiAuthCliStatusTicketIdRoute } @@ -2698,6 +2762,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IntentRegistryPackageNameSkillNameRouteImport parentRoute: typeof IntentRegistryPackageNameRoute } + '/api/v1/intent/search': { + id: '/api/v1/intent/search' + path: '/api/v1/intent/search' + fullPath: '/api/v1/intent/search' + preLoaderRoute: typeof ApiV1IntentSearchRouteImport + parentRoute: typeof rootRouteImport + } + '/api/v1/intent/packages': { + id: '/api/v1/intent/packages' + path: '/api/v1/intent/packages' + fullPath: '/api/v1/intent/packages' + preLoaderRoute: typeof ApiV1IntentPackagesRouteImport + parentRoute: typeof rootRouteImport + } '/api/builder/deploy/github': { id: '/api/builder/deploy/github' path: '/api/builder/deploy/github' @@ -2768,6 +2846,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibraryIdVersionDocsFrameworkIndexRouteImport parentRoute: typeof LibraryIdVersionDocsRoute } + '/api/v1/intent/packages/$name': { + id: '/api/v1/intent/packages/$name' + path: '/$name' + fullPath: '/api/v1/intent/packages/$name' + preLoaderRoute: typeof ApiV1IntentPackagesNameRouteImport + parentRoute: typeof ApiV1IntentPackagesRoute + } '/api/auth/cli/status/$ticketId': { id: '/api/auth/cli/status/$ticketId' path: '/api/auth/cli/status/$ticketId' @@ -2803,6 +2888,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibraryIdVersionDocsFrameworkFrameworkExamplesSplatRouteImport parentRoute: typeof LibraryIdVersionDocsRoute } + '/api/v1/intent/packages/$name/versions/$version/skills': { + id: '/api/v1/intent/packages/$name/versions/$version/skills' + path: '/versions/$version/skills' + fullPath: '/api/v1/intent/packages/$name/versions/$version/skills' + preLoaderRoute: typeof ApiV1IntentPackagesNameVersionsVersionSkillsRouteImport + parentRoute: typeof ApiV1IntentPackagesNameRoute + } + '/api/v1/intent/packages/$name/versions/$version/skills/$skill': { + id: '/api/v1/intent/packages/$name/versions/$version/skills/$skill' + path: '/$skill' + fullPath: '/api/v1/intent/packages/$name/versions/$version/skills/$skill' + preLoaderRoute: typeof ApiV1IntentPackagesNameVersionsVersionSkillsSkillRouteImport + parentRoute: typeof ApiV1IntentPackagesNameVersionsVersionSkillsRoute + } } } @@ -3023,6 +3122,47 @@ const IntentRegistryPackageNameRouteWithChildren = IntentRegistryPackageNameRouteChildren, ) +interface ApiV1IntentPackagesNameVersionsVersionSkillsRouteChildren { + ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute: typeof ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute +} + +const ApiV1IntentPackagesNameVersionsVersionSkillsRouteChildren: ApiV1IntentPackagesNameVersionsVersionSkillsRouteChildren = + { + ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute: + ApiV1IntentPackagesNameVersionsVersionSkillsSkillRoute, + } + +const ApiV1IntentPackagesNameVersionsVersionSkillsRouteWithChildren = + ApiV1IntentPackagesNameVersionsVersionSkillsRoute._addFileChildren( + ApiV1IntentPackagesNameVersionsVersionSkillsRouteChildren, + ) + +interface ApiV1IntentPackagesNameRouteChildren { + ApiV1IntentPackagesNameVersionsVersionSkillsRoute: typeof ApiV1IntentPackagesNameVersionsVersionSkillsRouteWithChildren +} + +const ApiV1IntentPackagesNameRouteChildren: ApiV1IntentPackagesNameRouteChildren = + { + ApiV1IntentPackagesNameVersionsVersionSkillsRoute: + ApiV1IntentPackagesNameVersionsVersionSkillsRouteWithChildren, + } + +const ApiV1IntentPackagesNameRouteWithChildren = + ApiV1IntentPackagesNameRoute._addFileChildren( + ApiV1IntentPackagesNameRouteChildren, + ) + +interface ApiV1IntentPackagesRouteChildren { + ApiV1IntentPackagesNameRoute: typeof ApiV1IntentPackagesNameRouteWithChildren +} + +const ApiV1IntentPackagesRouteChildren: ApiV1IntentPackagesRouteChildren = { + ApiV1IntentPackagesNameRoute: ApiV1IntentPackagesNameRouteWithChildren, +} + +const ApiV1IntentPackagesRouteWithChildren = + ApiV1IntentPackagesRoute._addFileChildren(ApiV1IntentPackagesRouteChildren) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LibraryIdRouteRoute: LibraryIdRouteRouteWithChildren, @@ -3113,6 +3253,8 @@ const rootRouteChildren: RootRouteChildren = { ApiAuthCliCreateTicketRoute: ApiAuthCliCreateTicketRoute, ApiBuilderDeployCheckNameRoute: ApiBuilderDeployCheckNameRoute, ApiBuilderDeployGithubRoute: ApiBuilderDeployGithubRoute, + ApiV1IntentPackagesRoute: ApiV1IntentPackagesRouteWithChildren, + ApiV1IntentSearchRoute: ApiV1IntentSearchRoute, ApiAuthCliStatusTicketIdRoute: ApiAuthCliStatusTicketIdRoute, } export const routeTree = rootRouteImport diff --git a/src/routes/api/v1/intent/packages.$name.ts b/src/routes/api/v1/intent/packages.$name.ts new file mode 100644 index 000000000..1f9bc69a4 --- /dev/null +++ b/src/routes/api/v1/intent/packages.$name.ts @@ -0,0 +1,39 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/v1/intent/packages/$name')({ + server: { + handlers: { + GET: async ({ + request, + params, + }: { + request: Request + params: { name: string } + }) => { + const [{ applyIntentRateLimit, intentJsonResponse, intentErrorResponse }, fns] = + await Promise.all([ + import('~/utils/intent-api.server'), + import('~/utils/intent.functions'), + ]) + + const decision = await applyIntentRateLimit(request) + if (decision.limited) return decision.response + + const detail = await fns.getIntentPackageDetail({ + data: { name: params.name }, + }) + + if (!detail) { + return intentErrorResponse( + `Package not found: ${params.name}`, + 'NOT_FOUND', + 404, + decision.rl, + ) + } + + return intentJsonResponse(detail, decision.rl) + }, + }, + }, +}) diff --git a/src/routes/api/v1/intent/packages.$name.versions.$version.skills.$skill.ts b/src/routes/api/v1/intent/packages.$name.versions.$version.skills.$skill.ts new file mode 100644 index 000000000..c1127147f --- /dev/null +++ b/src/routes/api/v1/intent/packages.$name.versions.$version.skills.$skill.ts @@ -0,0 +1,102 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute( + '/api/v1/intent/packages/$name/versions/$version/skills/$skill', +)({ + server: { + handlers: { + GET: async ({ + request, + params, + }: { + request: Request + params: { name: string; version: string; skill: string } + }) => { + const [ + { + applyIntentRateLimit, + intentJsonResponse, + intentErrorResponse, + buildSkillContentUrls, + }, + fns, + dbModule, + ] = await Promise.all([ + import('~/utils/intent-api.server'), + import('~/utils/intent.functions'), + import('~/utils/intent-db.server'), + ]) + + const decision = await applyIntentRateLimit(request) + if (decision.limited) return decision.response + + const versions = await dbModule.getPackageVersions(params.name) + const versionRecord = versions.find((v) => v.version === params.version) + if (!versionRecord) { + return intentErrorResponse( + `Version not found: ${params.name}@${params.version}`, + 'NOT_FOUND', + 404, + decision.rl, + ) + } + + const skills = await dbModule.getSkillsForVersion(versionRecord.id) + const skill = skills.find((s) => s.name === params.skill) + + if (!skill) { + return intentErrorResponse( + `Skill not found: ${params.skill} in ${params.name}@${params.version}`, + 'NOT_FOUND', + 404, + decision.rl, + ) + } + + // Markdown body is kept as an undocumented escape hatch only fetched + // when the caller explicitly opts in (?include=markdown). Default + // responses point at the CDN to keep our egress near zero. + const url = new URL(request.url) + const includeMarkdown = url.searchParams + .get('include') + ?.split(',') + .includes('markdown') + + const markdown = includeMarkdown + ? await fns.getIntentSkillMarkdown({ + data: { + packageName: params.name, + version: params.version, + skillName: params.skill, + }, + }) + : undefined + + return intentJsonResponse( + { + packageName: params.name, + version: params.version, + skill: { + id: skill.id, + name: skill.name, + description: skill.description, + type: skill.type, + framework: skill.framework, + requires: skill.requires, + skillPath: skill.skillPath, + contentHash: skill.contentHash, + lineCount: skill.lineCount, + }, + content: buildSkillContentUrls( + params.name, + params.version, + skill.skillPath, + ), + ...(includeMarkdown ? { markdown } : {}), + }, + decision.rl, + ) + }, + }, + }, +}) diff --git a/src/routes/api/v1/intent/packages.$name.versions.$version.skills.ts b/src/routes/api/v1/intent/packages.$name.versions.$version.skills.ts new file mode 100644 index 000000000..e65bae046 --- /dev/null +++ b/src/routes/api/v1/intent/packages.$name.versions.$version.skills.ts @@ -0,0 +1,61 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute( + '/api/v1/intent/packages/$name/versions/$version/skills', +)({ + server: { + handlers: { + GET: async ({ + request, + params, + }: { + request: Request + params: { name: string; version: string } + }) => { + const [ + { + applyIntentRateLimit, + intentJsonResponse, + intentErrorResponse, + buildSkillContentUrls, + }, + fns, + ] = await Promise.all([ + import('~/utils/intent-api.server'), + import('~/utils/intent.functions'), + ]) + + const decision = await applyIntentRateLimit(request) + if (decision.limited) return decision.response + + const result = await fns.getIntentVersionSkills({ + data: { packageName: params.name, version: params.version }, + }) + + if (!result) { + return intentErrorResponse( + `Version not found: ${params.name}@${params.version}`, + 'NOT_FOUND', + 404, + decision.rl, + ) + } + + return intentJsonResponse( + { + ...result, + skills: result.skills.map((skill) => ({ + ...skill, + content: buildSkillContentUrls( + params.name, + params.version, + skill.skillPath, + ), + })), + }, + decision.rl, + ) + }, + }, + }, +}) diff --git a/src/routes/api/v1/intent/packages.ts b/src/routes/api/v1/intent/packages.ts new file mode 100644 index 000000000..68502f088 --- /dev/null +++ b/src/routes/api/v1/intent/packages.ts @@ -0,0 +1,41 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/v1/intent/packages')({ + server: { + handlers: { + GET: async ({ request }: { request: Request }) => { + const [{ applyIntentRateLimit, intentJsonResponse }, fns] = + await Promise.all([ + import('~/utils/intent-api.server'), + import('~/utils/intent.functions'), + ]) + + const decision = await applyIntentRateLimit(request) + if (decision.limited) return decision.response + + const url = new URL(request.url) + const search = url.searchParams.get('q') ?? undefined + const framework = url.searchParams.get('framework') ?? undefined + const sortParam = url.searchParams.get('sort') + const sort: 'downloads' | 'name' | 'skills' | 'newest' | undefined = + sortParam === 'downloads' || + sortParam === 'name' || + sortParam === 'skills' || + sortParam === 'newest' + ? sortParam + : undefined + const page = parseInt(url.searchParams.get('page') ?? '0', 10) || 0 + const pageSize = Math.min( + Math.max(parseInt(url.searchParams.get('pageSize') ?? '24', 10) || 24, 1), + 100, + ) + + const result = await fns.getIntentDirectory({ + data: { search, framework, sort, page, pageSize }, + }) + + return intentJsonResponse(result, decision.rl) + }, + }, + }, +}) diff --git a/src/routes/api/v1/intent/search.ts b/src/routes/api/v1/intent/search.ts new file mode 100644 index 000000000..16dc6a83c --- /dev/null +++ b/src/routes/api/v1/intent/search.ts @@ -0,0 +1,53 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/v1/intent/search')({ + server: { + handlers: { + GET: async ({ request }: { request: Request }) => { + const [ + { + applyIntentRateLimit, + intentJsonResponse, + intentErrorResponse, + buildSkillContentUrls, + }, + dbModule, + ] = await Promise.all([ + import('~/utils/intent-api.server'), + import('~/utils/intent-db.server'), + ]) + + const decision = await applyIntentRateLimit(request) + if (decision.limited) return decision.response + + const url = new URL(request.url) + const q = url.searchParams.get('q')?.trim() ?? '' + const limitParam = url.searchParams.get('limit') + const limit = Math.min( + Math.max(parseInt(limitParam ?? '20', 10) || 20, 1), + 100, + ) + + if (!q) { + return intentErrorResponse( + 'Missing required query parameter: q', + 'INVALID_REQUEST', + 400, + decision.rl, + ) + } + + const rows = await dbModule.searchSkills(q, limit) + const results = rows.map((row) => ({ + ...row, + content: buildSkillContentUrls( + row.packageName, + row.version, + row.skillPath, + ), + })) + return intentJsonResponse({ query: q, limit, results }, decision.rl) + }, + }, + }, +}) diff --git a/src/utils/intent-api.server.ts b/src/utils/intent-api.server.ts new file mode 100644 index 000000000..421d11e77 --- /dev/null +++ b/src/utils/intent-api.server.ts @@ -0,0 +1,138 @@ +/** + * Shared helpers for the public Intent registry API (/api/v1/intent/*). + * + * Handles auth-aware rate limiting and response shaping so each route handler + * stays a thin wrapper around the underlying server functions / DB helpers. + */ + +import { + RATE_LIMITS, + checkIpRateLimit, + checkTokenRateLimit, + rateLimitedResponse, + type RateLimitResult, +} from './rateLimit.server' +import { validateMcpAuth } from '~/mcp/auth.server' + +export interface IntentApiAuth { + authenticated: boolean + userId: string | null +} + +export interface IntentRateLimitOutcome { + limited: false + rl: RateLimitResult + auth: IntentApiAuth +} + +export interface IntentRateLimitedOutcome { + limited: true + response: Response +} + +export type IntentRateLimitDecision = + | IntentRateLimitOutcome + | IntentRateLimitedOutcome + +/** + * Apply rate limiting to a request. If an Authorization: Bearer header is + * present and valid, uses the higher token-keyed tier. If absent, uses the + * anonymous IP-keyed tier. If present but invalid, returns 401. + */ +export async function applyIntentRateLimit( + request: Request, +): Promise { + const authHeader = request.headers.get('authorization') + + if (authHeader) { + const authResult = await validateMcpAuth(authHeader) + if (!authResult.success) { + return { + limited: true, + response: Response.json( + { error: authResult.error, code: 'UNAUTHORIZED' }, + { status: authResult.status }, + ), + } + } + + const rl = await checkTokenRateLimit( + authResult.keyId, + RATE_LIMITS.intentApiAuthed, + ) + if (!rl.allowed) return { limited: true, response: rateLimitedResponse(rl) } + return { + limited: false, + rl, + auth: { authenticated: true, userId: authResult.userId }, + } + } + + const rl = await checkIpRateLimit(request, RATE_LIMITS.intentApi) + if (!rl.allowed) return { limited: true, response: rateLimitedResponse(rl) } + return { + limited: false, + rl, + auth: { authenticated: false, userId: null }, + } +} + +/** + * Build a JSON response that merges rate-limit headers and standard + * Cache-Control for the public read endpoints. + */ +export function intentJsonResponse( + body: unknown, + rl: RateLimitResult, + init?: { status?: number; cache?: boolean }, +): Response { + const headers = new Headers(rl.headers) + headers.set('Content-Type', 'application/json') + if (init?.cache !== false) { + headers.set('Cache-Control', 'public, max-age=60, s-maxage=300') + } + return new Response(JSON.stringify(body), { + status: init?.status ?? 200, + headers, + }) +} + +/** + * CDN URLs for raw skill content. The skill file lives at + * `package/skills/{skillPath}/SKILL.md` inside the npm tarball; unpkg/jsdelivr + * serve it at `/{name}@{version}/skills/{skillPath}/SKILL.md`. + * + * Both URLs point at immutable, content-addressable npm tarball contents and + * are heavily edge-cached. Callers should verify integrity using `contentHash`. + */ +export interface SkillContentUrls { + unpkg: string + jsdelivr: string +} + +export function buildSkillContentUrls( + packageName: string, + version: string, + skillPath: string | null, +): SkillContentUrls | null { + if (!skillPath) return null + const path = `${packageName}@${version}/skills/${skillPath}/SKILL.md` + return { + unpkg: `https://unpkg.com/${path}`, + jsdelivr: `https://cdn.jsdelivr.net/npm/${path}`, + } +} + +export function intentErrorResponse( + error: string, + code: string, + status: number, + rl?: RateLimitResult, +): Response { + const headers = new Headers(rl?.headers) + headers.set('Content-Type', 'application/json') + return new Response(JSON.stringify({ error, code }), { + status, + headers, + }) +} diff --git a/src/utils/intent-db.server.ts b/src/utils/intent-db.server.ts index 02f55f083..fa3fffc21 100644 --- a/src/utils/intent-db.server.ts +++ b/src/utils/intent-db.server.ts @@ -216,6 +216,7 @@ export interface SkillSearchResult { description: string | null type: string | null framework: string | null + skillPath: string | null packageName: string version: string versionId: number @@ -236,6 +237,7 @@ export async function searchSkills( description: intentSkills.description, type: intentSkills.type, framework: intentSkills.framework, + skillPath: intentSkills.skillPath, packageName: intentPackageVersions.packageName, version: intentPackageVersions.version, versionId: intentPackageVersions.id, diff --git a/src/utils/rateLimit.server.ts b/src/utils/rateLimit.server.ts index 7a67cab1b..6b36dd7c0 100644 --- a/src/utils/rateLimit.server.ts +++ b/src/utils/rateLimit.server.ts @@ -33,8 +33,34 @@ export async function checkIpRateLimit( const result = await checkRateLimit(identifier, 'ip', options.limitPerMinute) + return buildRateLimitResult(result, options.limitPerMinute) +} + +/** + * Check rate limit keyed by an arbitrary token (e.g. API key id, session id). + * Same DB path and headers as checkIpRateLimit; just bypasses IP extraction. + */ +export async function checkTokenRateLimit( + token: string, + options: RateLimitOptions, +): Promise { + const identifier = options.keyPrefix ? `${options.keyPrefix}:${token}` : token + + const result = await checkRateLimit( + identifier, + 'api_key', + options.limitPerMinute, + ) + + return buildRateLimitResult(result, options.limitPerMinute) +} + +function buildRateLimitResult( + result: { allowed: boolean; remaining: number; resetAt: Date }, + limitPerMinute: number, +): RateLimitResult { const headers = new Headers() - headers.set('X-RateLimit-Limit', options.limitPerMinute.toString()) + headers.set('X-RateLimit-Limit', limitPerMinute.toString()) headers.set('X-RateLimit-Remaining', result.remaining.toString()) headers.set( 'X-RateLimit-Reset', @@ -187,4 +213,8 @@ export const RATE_LIMITS = { builderCompile: { limitPerMinute: 60, keyPrefix: 'builder-compile' }, // Deploy endpoint: 10 requests/minute (more sensitive) deploy: { limitPerMinute: 10, keyPrefix: 'deploy' }, + // Intent registry public API (anonymous, IP-keyed): 60 req/min + intentApi: { limitPerMinute: 60, keyPrefix: 'intent-api' }, + // Intent registry public API (authenticated, token-keyed): 600 req/min + intentApiAuthed: { limitPerMinute: 600, keyPrefix: 'intent-api-authed' }, } as const From 2b7da1bee64e666eacdb8d4f2c29a239fcc940c8 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sun, 3 May 2026 01:09:45 -0600 Subject: [PATCH 2/3] fix(intent): make skill .md route resolve and redirect to unpkg CDN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill page route used a named param ($skillName), so the more-specific .md sibling route ({$}.md) was being shadowed by it — TanStack Router's literal-suffix precedence only kicks in when both routes use the same param style. The legacy markdown handler was effectively unreachable; .md URLs were rendering the HTML skill page instead. Convert the page route to splat ({$}.tsx, matching the docs route pattern), which lets the .md sibling win precedence as intended. The .md handler now redirects to the unpkg CDN URL for the underlying SKILL.md, matching the public API behavior — zero egress for raw markdown bytes. Falls back to DB-served content for legacy records without a skillPath. Mechanical updates: every Link to="/intent/registry/$packageName/$skillName" becomes "/$packageName/{$}" with params { _splat: ... } instead of { skillName: ... }, and useParams destructures rename to _splat. --- .../intent/SkillDependencyGraph.tsx | 4 +- src/routeTree.gen.ts | 38 +++++++++---------- .../intent/registry/$packageName.index.tsx | 8 ++-- src/routes/intent/registry/$packageName.tsx | 8 ++-- ...me.$skillName.tsx => $packageName.{$}.tsx} | 18 +++++---- .../intent/registry/$packageName.{$}[.]md.tsx | 37 +++++++++++++++--- src/routes/intent/registry/index.tsx | 4 +- 7 files changed, 72 insertions(+), 45 deletions(-) rename src/routes/intent/registry/{$packageName.$skillName.tsx => $packageName.{$}.tsx} (95%) diff --git a/src/components/intent/SkillDependencyGraph.tsx b/src/components/intent/SkillDependencyGraph.tsx index 916230515..d2990f46f 100644 --- a/src/components/intent/SkillDependencyGraph.tsx +++ b/src/components/intent/SkillDependencyGraph.tsx @@ -227,8 +227,8 @@ export function SkillDependencyGraph({ return ( IntentRegistryPackageNameRoute, } as any) -const IntentRegistryPackageNameSkillNameRoute = - IntentRegistryPackageNameSkillNameRouteImport.update({ - id: '/$skillName', - path: '/$skillName', +const IntentRegistryPackageNameChar123Char125Route = + IntentRegistryPackageNameChar123Char125RouteImport.update({ + id: '/{$}', + path: '/{$}', getParentRoute: () => IntentRegistryPackageNameRoute, } as any) const ApiV1IntentSearchRoute = ApiV1IntentSearchRouteImport.update({ @@ -1046,7 +1046,7 @@ export interface FileRoutesByFullPath { '/api/builder/deploy/github': typeof ApiBuilderDeployGithubRoute '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren '/api/v1/intent/search': typeof ApiV1IntentSearchRoute - '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute + '/intent/registry/$packageName/{$}': typeof IntentRegistryPackageNameChar123Char125Route '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute '/intent/registry/$packageName/': typeof IntentRegistryPackageNameIndexRoute @@ -1184,7 +1184,7 @@ export interface FileRoutesByTo { '/api/builder/deploy/github': typeof ApiBuilderDeployGithubRoute '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren '/api/v1/intent/search': typeof ApiV1IntentSearchRoute - '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute + '/intent/registry/$packageName/{$}': typeof IntentRegistryPackageNameChar123Char125Route '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version/docs': typeof LibraryIdVersionDocsIndexRoute '/intent/registry/$packageName': typeof IntentRegistryPackageNameIndexRoute @@ -1333,7 +1333,7 @@ export interface FileRoutesById { '/api/builder/deploy/github': typeof ApiBuilderDeployGithubRoute '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren '/api/v1/intent/search': typeof ApiV1IntentSearchRoute - '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute + '/intent/registry/$packageName/{$}': typeof IntentRegistryPackageNameChar123Char125Route '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute '/intent/registry/$packageName/': typeof IntentRegistryPackageNameIndexRoute @@ -1483,7 +1483,7 @@ export interface FileRouteTypes { | '/api/builder/deploy/github' | '/api/v1/intent/packages' | '/api/v1/intent/search' - | '/intent/registry/$packageName/$skillName' + | '/intent/registry/$packageName/{$}' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version/docs/' | '/intent/registry/$packageName/' @@ -1621,7 +1621,7 @@ export interface FileRouteTypes { | '/api/builder/deploy/github' | '/api/v1/intent/packages' | '/api/v1/intent/search' - | '/intent/registry/$packageName/$skillName' + | '/intent/registry/$packageName/{$}' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version/docs' | '/intent/registry/$packageName' @@ -1769,7 +1769,7 @@ export interface FileRouteTypes { | '/api/builder/deploy/github' | '/api/v1/intent/packages' | '/api/v1/intent/search' - | '/intent/registry/$packageName/$skillName' + | '/intent/registry/$packageName/{$}' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version/docs/' | '/intent/registry/$packageName/' @@ -2755,11 +2755,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IntentRegistryPackageNameChar123Char125DotmdRouteImport parentRoute: typeof IntentRegistryPackageNameRoute } - '/intent/registry/$packageName/$skillName': { - id: '/intent/registry/$packageName/$skillName' - path: '/$skillName' - fullPath: '/intent/registry/$packageName/$skillName' - preLoaderRoute: typeof IntentRegistryPackageNameSkillNameRouteImport + '/intent/registry/$packageName/{$}': { + id: '/intent/registry/$packageName/{$}' + path: '/{$}' + fullPath: '/intent/registry/$packageName/{$}' + preLoaderRoute: typeof IntentRegistryPackageNameChar123Char125RouteImport parentRoute: typeof IntentRegistryPackageNameRoute } '/api/v1/intent/search': { @@ -3103,15 +3103,15 @@ const ShopRouteChildren: ShopRouteChildren = { const ShopRouteWithChildren = ShopRoute._addFileChildren(ShopRouteChildren) interface IntentRegistryPackageNameRouteChildren { - IntentRegistryPackageNameSkillNameRoute: typeof IntentRegistryPackageNameSkillNameRoute + IntentRegistryPackageNameChar123Char125Route: typeof IntentRegistryPackageNameChar123Char125Route IntentRegistryPackageNameChar123Char125DotmdRoute: typeof IntentRegistryPackageNameChar123Char125DotmdRoute IntentRegistryPackageNameIndexRoute: typeof IntentRegistryPackageNameIndexRoute } const IntentRegistryPackageNameRouteChildren: IntentRegistryPackageNameRouteChildren = { - IntentRegistryPackageNameSkillNameRoute: - IntentRegistryPackageNameSkillNameRoute, + IntentRegistryPackageNameChar123Char125Route: + IntentRegistryPackageNameChar123Char125Route, IntentRegistryPackageNameChar123Char125DotmdRoute: IntentRegistryPackageNameChar123Char125DotmdRoute, IntentRegistryPackageNameIndexRoute: IntentRegistryPackageNameIndexRoute, diff --git a/src/routes/intent/registry/$packageName.index.tsx b/src/routes/intent/registry/$packageName.index.tsx index bf82bdd65..44d474be0 100644 --- a/src/routes/intent/registry/$packageName.index.tsx +++ b/src/routes/intent/registry/$packageName.index.tsx @@ -244,8 +244,8 @@ function SkillsList({
{skill.name} @@ -607,8 +607,8 @@ function ChangelogSkillRow({ {statusConfig.prefix} {name} diff --git a/src/routes/intent/registry/$packageName.tsx b/src/routes/intent/registry/$packageName.tsx index 525b6f4aa..001c55962 100644 --- a/src/routes/intent/registry/$packageName.tsx +++ b/src/routes/intent/registry/$packageName.tsx @@ -142,9 +142,9 @@ function PackageLayoutInner({ readonly activeVersion: string readonly setVersion: (v: string) => void }) { - const { packageName, skillName } = useParams({ strict: false }) as { + const { packageName, _splat: skillName } = useParams({ strict: false }) as { packageName: string - skillName?: string + _splat?: string } const skillsQuery = useSuspenseQuery( @@ -410,8 +410,8 @@ function SkillsNav({ return ( ) export const Route = createFileRoute( - '/intent/registry/$packageName/$skillName', + '/intent/registry/$packageName/{$}', )({ loaderDeps: ({ search }) => ({ version: search.version }), loader: async ({ params, deps, context: { queryClient } }) => { const name = decodePkgName(params.packageName) + const skillName = params._splat ?? '' const detail = queryClient.getQueryData( intentPackageDetailQueryOptions(name).queryKey, ) const latestVersion = detail?.versions[0]?.version ?? '' const activeVersion = deps.version ?? latestVersion - if (activeVersion) { + if (activeVersion && skillName) { await queryClient.ensureQueryData( intentVersionSkillsQueryOptions({ packageName: name, @@ -48,7 +49,7 @@ export const Route = createFileRoute( return getIntentSkillPage({ data: { packageName: name, - skillName: params.skillName, + skillName, version: activeVersion, }, }) @@ -58,10 +59,11 @@ export const Route = createFileRoute( }, head: ({ params }) => { const pkgName = decodePkgName(params.packageName) + const skillName = params._splat ?? '' return { meta: seo({ - title: `${params.skillName} | ${pkgName} | Agent Skills Registry | TanStack Intent`, - description: `Agent Skill "${params.skillName}" from ${pkgName}.`, + title: `${skillName} | ${pkgName} | Agent Skills Registry | TanStack Intent`, + description: `Agent Skill "${skillName}" from ${pkgName}.`, }), } }, @@ -69,7 +71,7 @@ export const Route = createFileRoute( }) function SkillDetailPage() { - const { packageName, skillName } = Route.useParams() + const { packageName, _splat: skillName = '' } = Route.useParams() const skillPage = Route.useLoaderData() const pkgName = decodePkgName(packageName) const { activeVersion } = usePackageVersion() @@ -208,8 +210,8 @@ function SkillDetailPage() { {skill.requires.map((req) => ( {req} diff --git a/src/routes/intent/registry/$packageName.{$}[.]md.tsx b/src/routes/intent/registry/$packageName.{$}[.]md.tsx index 41968a2a8..5c3a58bbb 100644 --- a/src/routes/intent/registry/$packageName.{$}[.]md.tsx +++ b/src/routes/intent/registry/$packageName.{$}[.]md.tsx @@ -1,8 +1,15 @@ import { createFileRoute } from '@tanstack/react-router' import { getIntentSkillMarkdown } from '~/utils/intent.functions' +import { + getPackageVersions, + getSkillsForVersion, +} from '~/utils/intent-db.server' +import { buildSkillContentUrls } from '~/utils/intent-api.server' import { decodePkgName } from './$packageName' -export const Route = createFileRoute('/intent/registry/$packageName/{$}.md')({ +export const Route = createFileRoute( + '/intent/registry/$packageName/{$}.md', +)({ server: { handlers: { GET: async ({ request, params }) => { @@ -14,12 +21,30 @@ export const Route = createFileRoute('/intent/registry/$packageName/{$}.md')({ return new Response('Missing version', { status: 400 }) } + const packageName = decodePkgName(params.packageName) + + const versions = await getPackageVersions(packageName) + const versionRecord = versions.find((v) => v.version === version) + + if (versionRecord) { + const skills = await getSkillsForVersion(versionRecord.id) + const skill = skills.find((s) => s.name === skillName) + + if (skill?.skillPath) { + const urls = buildSkillContentUrls( + packageName, + version, + skill.skillPath, + ) + if (urls) { + return Response.redirect(urls.unpkg, 302) + } + } + } + + // Fallback: legacy records without skillPath. Serve from DB. const content = await getIntentSkillMarkdown({ - data: { - packageName: decodePkgName(params.packageName), - skillName, - version, - }, + data: { packageName, skillName, version }, }) if (!content) { diff --git a/src/routes/intent/registry/index.tsx b/src/routes/intent/registry/index.tsx index acfd715ad..ec9d43a0e 100644 --- a/src/routes/intent/registry/index.tsx +++ b/src/routes/intent/registry/index.tsx @@ -749,10 +749,10 @@ function SkillHitRow({ }) { return ( From 938cab47c41aa0681462f541a35605f25f51f650 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 07:11:13 +0000 Subject: [PATCH 3/3] ci: apply automated fixes --- src/routes/intent/registry/$packageName.{$}.tsx | 4 +--- src/routes/intent/registry/$packageName.{$}[.]md.tsx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/routes/intent/registry/$packageName.{$}.tsx b/src/routes/intent/registry/$packageName.{$}.tsx index d4e5659b8..189d1dd4b 100644 --- a/src/routes/intent/registry/$packageName.{$}.tsx +++ b/src/routes/intent/registry/$packageName.{$}.tsx @@ -26,9 +26,7 @@ const LazySkillSparkline = React.lazy(() => })), ) -export const Route = createFileRoute( - '/intent/registry/$packageName/{$}', -)({ +export const Route = createFileRoute('/intent/registry/$packageName/{$}')({ loaderDeps: ({ search }) => ({ version: search.version }), loader: async ({ params, deps, context: { queryClient } }) => { const name = decodePkgName(params.packageName) diff --git a/src/routes/intent/registry/$packageName.{$}[.]md.tsx b/src/routes/intent/registry/$packageName.{$}[.]md.tsx index 5c3a58bbb..9bf0d906d 100644 --- a/src/routes/intent/registry/$packageName.{$}[.]md.tsx +++ b/src/routes/intent/registry/$packageName.{$}[.]md.tsx @@ -7,9 +7,7 @@ import { import { buildSkillContentUrls } from '~/utils/intent-api.server' import { decodePkgName } from './$packageName' -export const Route = createFileRoute( - '/intent/registry/$packageName/{$}.md', -)({ +export const Route = createFileRoute('/intent/registry/$packageName/{$}.md')({ server: { handlers: { GET: async ({ request, params }) => {