diff --git a/src/components/intent/SkillDependencyGraph.tsx b/src/components/intent/SkillDependencyGraph.tsx index bda77d3a6..2db693adf 100644 --- a/src/components/intent/SkillDependencyGraph.tsx +++ b/src/components/intent/SkillDependencyGraph.tsx @@ -268,8 +268,8 @@ export function SkillDependencyGraph({ const datum = point?.datum if (!datum || datum.kind !== 'node') return void navigate({ - to: '/intent/registry/$packageName/$skillName', - params: { packageName, skillName: datum.id }, + to: '/intent/registry/$packageName/{$}', + params: { packageName, _splat: datum.id }, }) }} /> diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index c09771b52..6c65d993f 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -265,8 +265,10 @@ import { Route as ApiChatSectionsIdRouteImport } from './routes/api/chat/section import { Route as ApiChatSkillsIdRouteImport } from './routes/api/chat/skills/$id' import { Route as ApiChatWorkspaceIndexActivityRouteImport } from './routes/api/chat/workspace-index.activity' import { Route as ApiChatWorkspaceSyncOperationRouteImport } from './routes/api/chat/workspace-sync/$operation' +import { Route as ApiV1IntentPackagesRouteImport } from './routes/api/v1/intent/packages' +import { Route as ApiV1IntentSearchRouteImport } from './routes/api/v1/intent/search' import { Route as IntentRegistryPackageNameIndexRouteImport } from './routes/intent/registry/$packageName.index' -import { Route as IntentRegistryPackageNameSkillNameRouteImport } from './routes/intent/registry/$packageName.$skillName' +import { Route as IntentRegistryPackageNameChar123Char125RouteImport } from './routes/intent/registry/$packageName.{$}' import { Route as IntentRegistryPackageNameChar123Char125DotmdRouteImport } from './routes/intent/registry/$packageName.{$}[.]md' import { Route as LibraryLibraryIdVersionDocsIndexRouteImport } from './routes/_library/$libraryId/$version.docs.index' import { Route as LibraryLibraryIdVersionDocsSplatRouteImport } from './routes/_library/$libraryId/$version.docs.$' @@ -298,6 +300,7 @@ import { Route as ApiChatReferencesKodyInspectRouteImport } from './routes/api/c import { Route as ApiChatReferencesKodyRefreshRouteImport } from './routes/api/chat/references/kody/refresh' import { Route as ApiChatReferencesToolsRefreshRouteImport } from './routes/api/chat/references/tools/refresh' import { Route as ApiChatRetriesIdPrepareRouteImport } from './routes/api/chat/retries/$id.prepare' +import { Route as ApiV1IntentPackagesNameRouteImport } from './routes/api/v1/intent/packages.$name' import { Route as ChartsCatalogPreviewsRevisionChar123caseIdChar125DotsvgRouteImport } from './routes/charts.catalog_.previews.$revision.{$caseId}[.]svg' import { Route as ChatCConversationIdMMessageIdRouteImport } from './routes/chat.c.$conversationId.m.$messageId' import { Route as ChatWWorkspaceIdBBotIdRouteImport } from './routes/chat.w.$workspaceId.b.$botId' @@ -331,6 +334,8 @@ import { Route as ApiChatConversationsIdExecutionSnapshotsSnapshotIdRouteImport import { Route as ApiChatConversationsIdFilesFileIdContentRouteImport } from './routes/api/chat/conversations/$id.files.$fileId.content' import { Route as ApiChatConversationsIdWorkflowRunsRunIdCancelRouteImport } from './routes/api/chat/conversations/$id.workflow-runs.$runId.cancel' import { Route as LibraryLibraryIdVersionDocsFrameworkFrameworkExamplesSplatRouteImport } from './routes/_library/$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 IndexRoute = IndexRouteImport.update({ id: '/', @@ -1659,16 +1664,26 @@ const ApiChatWorkspaceSyncOperationRoute = path: '/api/chat/workspace-sync/$operation', getParentRoute: () => rootRouteImport, } as any) +const ApiV1IntentPackagesRoute = ApiV1IntentPackagesRouteImport.update({ + id: '/api/v1/intent/packages', + path: '/api/v1/intent/packages', + getParentRoute: () => rootRouteImport, +} as any) +const ApiV1IntentSearchRoute = ApiV1IntentSearchRouteImport.update({ + id: '/api/v1/intent/search', + path: '/api/v1/intent/search', + getParentRoute: () => rootRouteImport, +} as any) const IntentRegistryPackageNameIndexRoute = IntentRegistryPackageNameIndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => IntentRegistryPackageNameRoute, } as any) -const IntentRegistryPackageNameSkillNameRoute = - IntentRegistryPackageNameSkillNameRouteImport.update({ - id: '/$skillName', - path: '/$skillName', +const IntentRegistryPackageNameChar123Char125Route = + IntentRegistryPackageNameChar123Char125RouteImport.update({ + id: '/{$}', + path: '/{$}', getParentRoute: () => IntentRegistryPackageNameRoute, } as any) const IntentRegistryPackageNameChar123Char125DotmdRoute = @@ -1849,6 +1864,11 @@ const ApiChatRetriesIdPrepareRoute = ApiChatRetriesIdPrepareRouteImport.update({ path: '/prepare', getParentRoute: () => ApiChatRetriesIdRoute, } as any) +const ApiV1IntentPackagesNameRoute = ApiV1IntentPackagesNameRouteImport.update({ + id: '/$name', + path: '/$name', + getParentRoute: () => ApiV1IntentPackagesRoute, +} as any) const ChartsCatalogPreviewsRevisionChar123caseIdChar125DotsvgRoute = ChartsCatalogPreviewsRevisionChar123caseIdChar125DotsvgRouteImport.update({ id: '/charts/catalog_/previews/$revision/{$caseId}.svg', @@ -2048,6 +2068,18 @@ const LibraryLibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute = path: '/framework/$framework/examples/$', getParentRoute: () => LibraryLibraryIdVersionDocsRoute, } 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 @@ -2282,7 +2314,9 @@ export interface FileRoutesByFullPath { '/api/chat/skills/$id': typeof ApiChatSkillsIdRoute '/api/chat/workspace-index/activity': typeof ApiChatWorkspaceIndexActivityRoute '/api/chat/workspace-sync/$operation': typeof ApiChatWorkspaceSyncOperationRoute - '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute + '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren + '/api/v1/intent/search': typeof ApiV1IntentSearchRoute + '/intent/registry/$packageName/{$}': typeof IntentRegistryPackageNameChar123Char125Route '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version/': typeof LibraryLibraryIdVersionIndexRoute '/ai/$version/': typeof LibraryAiVersionIndexRoute @@ -2337,6 +2371,7 @@ export interface FileRoutesByFullPath { '/api/chat/references/kody/refresh': typeof ApiChatReferencesKodyRefreshRoute '/api/chat/references/tools/refresh': typeof ApiChatReferencesToolsRefreshRoute '/api/chat/retries/$id/prepare': typeof ApiChatRetriesIdPrepareRoute + '/api/v1/intent/packages/$name': typeof ApiV1IntentPackagesNameRouteWithChildren '/charts/catalog/previews/$revision/{$caseId}.svg': typeof ChartsCatalogPreviewsRevisionChar123caseIdChar125DotsvgRoute '/chat/c/$conversationId/m/$messageId': typeof ChatCConversationIdMMessageIdRoute '/chat/w/$workspaceId/b/$botId': typeof ChatWWorkspaceIdBBotIdRoute @@ -2371,6 +2406,8 @@ export interface FileRoutesByFullPath { '/api/chat/conversations/$id/workflow-runs/$runId/cancel': typeof ApiChatConversationsIdWorkflowRunsRunIdCancelRoute '/$libraryId/$version/docs/framework/$framework/': typeof LibraryLibraryIdVersionDocsFrameworkFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/examples/$': typeof LibraryLibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute + '/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 @@ -2592,7 +2629,9 @@ export interface FileRoutesByTo { '/api/chat/skills/$id': typeof ApiChatSkillsIdRoute '/api/chat/workspace-index/activity': typeof ApiChatWorkspaceIndexActivityRoute '/api/chat/workspace-sync/$operation': typeof ApiChatWorkspaceSyncOperationRoute - '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute + '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren + '/api/v1/intent/search': typeof ApiV1IntentSearchRoute + '/intent/registry/$packageName/{$}': typeof IntentRegistryPackageNameChar123Char125Route '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/$libraryId/$version': typeof LibraryLibraryIdVersionIndexRoute '/ai/$version': typeof LibraryAiVersionIndexRoute @@ -2647,6 +2686,7 @@ export interface FileRoutesByTo { '/api/chat/references/kody/refresh': typeof ApiChatReferencesKodyRefreshRoute '/api/chat/references/tools/refresh': typeof ApiChatReferencesToolsRefreshRoute '/api/chat/retries/$id/prepare': typeof ApiChatRetriesIdPrepareRoute + '/api/v1/intent/packages/$name': typeof ApiV1IntentPackagesNameRouteWithChildren '/charts/catalog/previews/$revision/{$caseId}.svg': typeof ChartsCatalogPreviewsRevisionChar123caseIdChar125DotsvgRoute '/chat/c/$conversationId/m/$messageId': typeof ChatCConversationIdMMessageIdRoute '/chat/w/$workspaceId/b/$botId': typeof ChatWWorkspaceIdBBotIdRoute @@ -2681,6 +2721,8 @@ export interface FileRoutesByTo { '/api/chat/conversations/$id/workflow-runs/$runId/cancel': typeof ApiChatConversationsIdWorkflowRunsRunIdCancelRoute '/$libraryId/$version/docs/framework/$framework': typeof LibraryLibraryIdVersionDocsFrameworkFrameworkIndexRoute '/$libraryId/$version/docs/framework/$framework/examples/$': typeof LibraryLibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute + '/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 @@ -2917,7 +2959,9 @@ export interface FileRoutesById { '/api/chat/skills/$id': typeof ApiChatSkillsIdRoute '/api/chat/workspace-index/activity': typeof ApiChatWorkspaceIndexActivityRoute '/api/chat/workspace-sync/$operation': typeof ApiChatWorkspaceSyncOperationRoute - '/intent/registry/$packageName/$skillName': typeof IntentRegistryPackageNameSkillNameRoute + '/api/v1/intent/packages': typeof ApiV1IntentPackagesRouteWithChildren + '/api/v1/intent/search': typeof ApiV1IntentSearchRoute + '/intent/registry/$packageName/{$}': typeof IntentRegistryPackageNameChar123Char125Route '/intent/registry/$packageName/{$}.md': typeof IntentRegistryPackageNameChar123Char125DotmdRoute '/_library/$libraryId/$version/': typeof LibraryLibraryIdVersionIndexRoute '/_library/ai/$version/': typeof LibraryAiVersionIndexRoute @@ -2972,6 +3016,7 @@ export interface FileRoutesById { '/api/chat/references/kody/refresh': typeof ApiChatReferencesKodyRefreshRoute '/api/chat/references/tools/refresh': typeof ApiChatReferencesToolsRefreshRoute '/api/chat/retries/$id/prepare': typeof ApiChatRetriesIdPrepareRoute + '/api/v1/intent/packages/$name': typeof ApiV1IntentPackagesNameRouteWithChildren '/charts/catalog_/previews/$revision/{$caseId}.svg': typeof ChartsCatalogPreviewsRevisionChar123caseIdChar125DotsvgRoute '/chat/c/$conversationId/m/$messageId': typeof ChatCConversationIdMMessageIdRoute '/chat/w/$workspaceId/b/$botId': typeof ChatWWorkspaceIdBBotIdRoute @@ -3006,6 +3051,8 @@ export interface FileRoutesById { '/api/chat/conversations/$id/workflow-runs/$runId/cancel': typeof ApiChatConversationsIdWorkflowRunsRunIdCancelRoute '/_library/$libraryId/$version/docs/framework/$framework/': typeof LibraryLibraryIdVersionDocsFrameworkFrameworkIndexRoute '/_library/$libraryId/$version/docs/framework/$framework/examples/$': typeof LibraryLibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute + '/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 @@ -3242,7 +3289,9 @@ export interface FileRouteTypes { | '/api/chat/skills/$id' | '/api/chat/workspace-index/activity' | '/api/chat/workspace-sync/$operation' - | '/intent/registry/$packageName/$skillName' + | '/api/v1/intent/packages' + | '/api/v1/intent/search' + | '/intent/registry/$packageName/{$}' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version/' | '/ai/$version/' @@ -3297,6 +3346,7 @@ export interface FileRouteTypes { | '/api/chat/references/kody/refresh' | '/api/chat/references/tools/refresh' | '/api/chat/retries/$id/prepare' + | '/api/v1/intent/packages/$name' | '/charts/catalog/previews/$revision/{$caseId}.svg' | '/chat/c/$conversationId/m/$messageId' | '/chat/w/$workspaceId/b/$botId' @@ -3331,6 +3381,8 @@ export interface FileRouteTypes { | '/api/chat/conversations/$id/workflow-runs/$runId/cancel' | '/$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: | '/' @@ -3552,7 +3604,9 @@ export interface FileRouteTypes { | '/api/chat/skills/$id' | '/api/chat/workspace-index/activity' | '/api/chat/workspace-sync/$operation' - | '/intent/registry/$packageName/$skillName' + | '/api/v1/intent/packages' + | '/api/v1/intent/search' + | '/intent/registry/$packageName/{$}' | '/intent/registry/$packageName/{$}.md' | '/$libraryId/$version' | '/ai/$version' @@ -3607,6 +3661,7 @@ export interface FileRouteTypes { | '/api/chat/references/kody/refresh' | '/api/chat/references/tools/refresh' | '/api/chat/retries/$id/prepare' + | '/api/v1/intent/packages/$name' | '/charts/catalog/previews/$revision/{$caseId}.svg' | '/chat/c/$conversationId/m/$messageId' | '/chat/w/$workspaceId/b/$botId' @@ -3641,6 +3696,8 @@ export interface FileRouteTypes { | '/api/chat/conversations/$id/workflow-runs/$runId/cancel' | '/$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__' | '/' @@ -3876,7 +3933,9 @@ export interface FileRouteTypes { | '/api/chat/skills/$id' | '/api/chat/workspace-index/activity' | '/api/chat/workspace-sync/$operation' - | '/intent/registry/$packageName/$skillName' + | '/api/v1/intent/packages' + | '/api/v1/intent/search' + | '/intent/registry/$packageName/{$}' | '/intent/registry/$packageName/{$}.md' | '/_library/$libraryId/$version/' | '/_library/ai/$version/' @@ -3931,6 +3990,7 @@ export interface FileRouteTypes { | '/api/chat/references/kody/refresh' | '/api/chat/references/tools/refresh' | '/api/chat/retries/$id/prepare' + | '/api/v1/intent/packages/$name' | '/charts/catalog_/previews/$revision/{$caseId}.svg' | '/chat/c/$conversationId/m/$messageId' | '/chat/w/$workspaceId/b/$botId' @@ -3965,6 +4025,8 @@ export interface FileRouteTypes { | '/api/chat/conversations/$id/workflow-runs/$runId/cancel' | '/_library/$libraryId/$version/docs/framework/$framework/' | '/_library/$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 { @@ -4105,6 +4167,8 @@ export interface RootRouteChildren { ApiChatOpenrouterCallbackRoute: typeof ApiChatOpenrouterCallbackRoute ApiChatRetriesIdRoute: typeof ApiChatRetriesIdRouteWithChildren ApiChatWorkspaceSyncOperationRoute: typeof ApiChatWorkspaceSyncOperationRoute + ApiV1IntentPackagesRoute: typeof ApiV1IntentPackagesRouteWithChildren + ApiV1IntentSearchRoute: typeof ApiV1IntentSearchRoute ApiAuthCliStatusTicketIdRoute: typeof ApiAuthCliStatusTicketIdRoute ApiChatConversationsIdOperationRoute: typeof ApiChatConversationsIdOperationRoute ApiChatConversationsIdFilesRoute: typeof ApiChatConversationsIdFilesRouteWithChildren @@ -5916,6 +5980,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiChatWorkspaceSyncOperationRouteImport 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/v1/intent/search': { + id: '/api/v1/intent/search' + path: '/api/v1/intent/search' + fullPath: '/api/v1/intent/search' + preLoaderRoute: typeof ApiV1IntentSearchRouteImport + parentRoute: typeof rootRouteImport + } '/intent/registry/$packageName/': { id: '/intent/registry/$packageName/' path: '/' @@ -5923,11 +6001,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IntentRegistryPackageNameIndexRouteImport 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 } '/intent/registry/$packageName/{$}.md': { @@ -6147,6 +6225,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiChatRetriesIdPrepareRouteImport parentRoute: typeof ApiChatRetriesIdRoute } + '/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 + } '/charts/catalog_/previews/$revision/{$caseId}.svg': { id: '/charts/catalog_/previews/$revision/{$caseId}.svg' path: '/charts/catalog/previews/$revision/{$caseId}.svg' @@ -6378,6 +6463,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibraryLibraryIdVersionDocsFrameworkFrameworkExamplesSplatRouteImport parentRoute: typeof LibraryLibraryIdVersionDocsRoute } + '/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 + } } } @@ -6987,15 +7086,15 @@ const ApiChatWorkspaceIndexRouteWithChildren = ) 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, @@ -7143,6 +7242,47 @@ const ApiChatRetriesIdRouteChildren: ApiChatRetriesIdRouteChildren = { const ApiChatRetriesIdRouteWithChildren = ApiChatRetriesIdRoute._addFileChildren(ApiChatRetriesIdRouteChildren) +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) + interface ApiChatConversationsIdFilesFileIdRouteChildren { ApiChatConversationsIdFilesFileIdContentRoute: typeof ApiChatConversationsIdFilesFileIdContentRoute } @@ -7367,6 +7507,8 @@ const rootRouteChildren: RootRouteChildren = { ApiChatOpenrouterCallbackRoute: ApiChatOpenrouterCallbackRoute, ApiChatRetriesIdRoute: ApiChatRetriesIdRouteWithChildren, ApiChatWorkspaceSyncOperationRoute: ApiChatWorkspaceSyncOperationRoute, + ApiV1IntentPackagesRoute: ApiV1IntentPackagesRouteWithChildren, + ApiV1IntentSearchRoute: ApiV1IntentSearchRoute, ApiAuthCliStatusTicketIdRoute: ApiAuthCliStatusTicketIdRoute, ApiChatConversationsIdOperationRoute: ApiChatConversationsIdOperationRoute, ApiChatConversationsIdFilesRoute: 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..aa43e32c4 --- /dev/null +++ b/src/routes/api/v1/intent/packages.ts @@ -0,0 +1,57 @@ +import { createFileRoute } from '@tanstack/react-router' +import { safeParse } from 'valibot' + +export const Route = createFileRoute('/api/v1/intent/packages')({ + server: { + handlers: { + GET: async ({ request }: { request: Request }) => { + 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 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 = Math.max(0, parseInt(url.searchParams.get('page') ?? '0', 10) || 0) + const pageSize = Math.min( + Math.max(parseInt(url.searchParams.get('pageSize') ?? '24', 10) || 24, 1), + 48, + ) + + const parsed = safeParse(fns.intentDirectorySchema, { + search, + framework, + sort, + page, + pageSize, + }) + if (!parsed.success) { + return intentErrorResponse( + parsed.issues[0].message, + 'INVALID_REQUEST', + 400, + decision.rl, + ) + } + const result = await fns.getIntentDirectory({ data: parsed.output }) + + 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/routes/intent/registry/$packageName.index.tsx b/src/routes/intent/registry/$packageName.index.tsx index c56ba0c52..5a3eac3bf 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 1e88adcf3..e4e140f7f 100644 --- a/src/routes/intent/registry/$packageName.tsx +++ b/src/routes/intent/registry/$packageName.tsx @@ -149,10 +149,8 @@ function PackageLayoutInner({ readonly activeVersion: string readonly setVersion: (v: string) => void }) { - const { packageName, skillName } = useParams({ strict: false }) as { - packageName: string - skillName?: string - } + const { packageName } = Route.useParams() + const { _splat: skillName } = useParams({ strict: false }) const skillsQuery = useSuspenseQuery( intentVersionSkillsQueryOptions({ @@ -431,8 +429,8 @@ function SkillsNav({ return ( ({ version: search.version }), loader: async ({ params, deps, context: { queryClient } }) => { const name = decodePkgName(params.packageName) + const skillName = params._splat ?? '' const detail = await queryClient.ensureQueryData( intentPackageDetailQueryOptions(name), ) const latestVersion = detail?.versions[0]?.version ?? '' const activeVersion = deps.version ?? latestVersion - if (activeVersion) { + if (activeVersion && skillName) { await queryClient.ensureQueryData( intentVersionSkillsQueryOptions({ packageName: name, @@ -69,7 +68,7 @@ export const Route = createFileRoute( return getIntentSkillPage({ data: { packageName: name, - skillName: params.skillName, + skillName, version: activeVersion, }, }) @@ -79,10 +78,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}.`, }), } }, @@ -90,7 +90,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() @@ -236,8 +236,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..9bf0d906d 100644 --- a/src/routes/intent/registry/$packageName.{$}[.]md.tsx +++ b/src/routes/intent/registry/$packageName.{$}[.]md.tsx @@ -1,5 +1,10 @@ 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')({ @@ -14,12 +19,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 bd6c3f9c8..8604d023e 100644 --- a/src/routes/intent/registry/index.tsx +++ b/src/routes/intent/registry/index.tsx @@ -760,10 +760,10 @@ function SkillHitRow({ }) { return ( diff --git a/src/utils/intent-api.server.ts b/src/utils/intent-api.server.ts new file mode 100644 index 000000000..3cc36a5f3 --- /dev/null +++ b/src/utils/intent-api.server.ts @@ -0,0 +1,144 @@ +/** + * 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, + headers: { 'Cache-Control': 'no-store' }, + }, + ), + } + } + + 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 responses containing per-client quota headers. + */ +export function intentJsonResponse( + body: unknown, + rl: RateLimitResult, + init?: { status?: number }, +): Response { + const headers = new Headers(rl.headers) + headers.set('Content-Type', 'application/json') + headers.set('Cache-Control', 'no-store') + 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 encodedSkillPath = skillPath + .split('/') + .map(encodeURIComponent) + .join('/') + const path = `${packageName}@${version}/skills/${encodedSkillPath}/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('Cache-Control', 'no-store') + 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 afbee287c..17d89df4f 100644 --- a/src/utils/intent-db.server.ts +++ b/src/utils/intent-db.server.ts @@ -342,6 +342,7 @@ export interface SkillSearchResult { description: string | null type: string | null framework: string | null + skillPath: string | null packageName: string version: string versionId: number @@ -365,6 +366,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/intent.functions.ts b/src/utils/intent.functions.ts index 39bbd5819..6cc044cf3 100644 --- a/src/utils/intent.functions.ts +++ b/src/utils/intent.functions.ts @@ -155,16 +155,16 @@ export const getIntentStats = createServerFn({ method: 'GET' }).handler( // Directory listing // --------------------------------------------------------------------------- +export const intentDirectorySchema = v.object({ + search: v.optional(intentSearchSchema), + framework: v.optional(intentFrameworkSchema), + sort: v.optional(v.picklist(['downloads', 'name', 'skills', 'newest'])), + page: v.optional(intentPageSchema), + pageSize: v.optional(intentPageSizeSchema), +}) + export const getIntentDirectory = createServerFn({ method: 'GET' }) - .validator( - v.object({ - search: v.optional(intentSearchSchema), - framework: v.optional(intentFrameworkSchema), - sort: v.optional(v.picklist(['downloads', 'name', 'skills', 'newest'])), - page: v.optional(intentPageSchema), - pageSize: v.optional(intentPageSizeSchema), - }), - ) + .validator(intentDirectorySchema) .handler(async ({ data }) => { const { search, diff --git a/src/utils/rateLimit.server.ts b/src/utils/rateLimit.server.ts index 9805044db..b4fcd6228 100644 --- a/src/utils/rateLimit.server.ts +++ b/src/utils/rateLimit.server.ts @@ -33,20 +33,33 @@ export async function checkIpRateLimit( const result = await checkRateLimit(identifier, 'ip', options.limitPerMinute) - const headers = new Headers() - headers.set('X-RateLimit-Limit', options.limitPerMinute.toString()) - headers.set('X-RateLimit-Remaining', result.remaining.toString()) - headers.set( - 'X-RateLimit-Reset', - Math.floor(result.resetAt.getTime() / 1000).toString(), + 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, ) - if (!result.allowed) { - headers.set( - 'Retry-After', - Math.ceil((result.resetAt.getTime() - Date.now()) / 1000).toString(), - ) - } + return buildRateLimitResult(result, options.limitPerMinute) +} + +function buildRateLimitResult( + result: { allowed: boolean; remaining: number; resetAt: Date }, + limitPerMinute: number, +): RateLimitResult { + const headers = createRateLimitHeaders(limitPerMinute, result) return { allowed: result.allowed, @@ -232,6 +245,10 @@ export const RATE_LIMITS = { }, // 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' }, // CLI auth ticket creation: public endpoint polled by local tools cliAuthTicket: { limitPerMinute: 20, keyPrefix: 'cli-auth-ticket' }, builderProjectWrite: {