Repository navigation
feat(intent): public read-only registry API at /api/v1/intent #885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
c4aa7ec
2b7da1b
938cab4
58dc8ce
a523837
b658842
0d69ada
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| }, | ||
| }, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| ) | ||
| }, | ||
| }, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| ) | ||
| }, | ||
| }, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize negative
Suggested patch- const page = parseInt(url.searchParams.get('page') ?? '0', 10) || 0
+ const parsedPage = parseInt(url.searchParams.get('page') ?? '0', 10)
+ const page = Number.isNaN(parsedPage) ? 0 : Math.max(parsedPage, 0)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const result = await fns.getIntentDirectory({ | ||||||||||||||||||||||||
| data: { search, framework, sort, page, pageSize }, | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return intentJsonResponse(result, decision.rl) | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| }, | ||
| }, | ||
| }, | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.