Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/intent/SkillDependencyGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ export function SkillDependencyGraph({
return (
<Link
key={node.id}
to="/intent/registry/$packageName/$skillName"
params={{ packageName, skillName: node.id }}
to="/intent/registry/$packageName/{$}"
params={{ packageName, _splat: node.id }}
>
<g className="cursor-pointer group">
<circle
Expand Down
180 changes: 161 additions & 19 deletions src/routeTree.gen.ts

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions src/routes/api/v1/intent/packages.$name.ts
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,
)
},
},
},
})
41 changes: 41 additions & 0 deletions src/routes/api/v1/intent/packages.ts
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize negative page values to 0.

page=-1 currently survives parsing and is forwarded downstream; clamp to avoid invalid pagination input.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 parsedPage = parseInt(url.searchParams.get('page') ?? '0', 10)
const page = Number.isNaN(parsedPage) ? 0 : Math.max(parsedPage, 0)
const pageSize = Math.min(
Math.max(parseInt(url.searchParams.get('pageSize') ?? '24', 10) || 24, 1),
100,
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/api/v1/intent/packages.ts` around lines 27 - 31, The parsed page
value can be negative (e.g., page = -1) and is forwarded downstream; clamp it to
zero by normalizing the parsed value before use: after computing page from
parseInt(url.searchParams.get('page') ...), ensure you set page =
Math.max(parsedPage, 0) so the variable used later (page) cannot be negative;
update the pagination logic where the page variable is defined to use this
clamping (refer to the page variable in src/routes/api/v1/intent/packages.ts).


const result = await fns.getIntentDirectory({
data: { search, framework, sort, page, pageSize },
})

return intentJsonResponse(result, decision.rl)
},
},
},
})
53 changes: 53 additions & 0 deletions src/routes/api/v1/intent/search.ts
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)
},
},
},
})
8 changes: 4 additions & 4 deletions src/routes/intent/registry/$packageName.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ function SkillsList({
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<Link
to="/intent/registry/$packageName/$skillName"
params={{ packageName, skillName: skill.name }}
to="/intent/registry/$packageName/{$}"
params={{ packageName, _splat: skill.name }}
className="font-mono text-sm font-semibold text-gray-900 dark:text-gray-100 hover:text-sky-600 dark:hover:text-sky-400 transition-colors"
>
{skill.name}
Expand Down Expand Up @@ -607,8 +607,8 @@ function ChangelogSkillRow({
{statusConfig.prefix}
</span>
<Link
to="/intent/registry/$packageName/$skillName"
params={{ packageName, skillName: name }}
to="/intent/registry/$packageName/{$}"
params={{ packageName, _splat: name }}
className="font-mono text-sm text-gray-900 dark:text-gray-100 hover:text-sky-600 dark:hover:text-sky-400 transition-colors truncate"
>
{name}
Expand Down
8 changes: 4 additions & 4 deletions src/routes/intent/registry/$packageName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -410,8 +410,8 @@ function SkillsNav({
return (
<Link
key={s.name}
to="/intent/registry/$packageName/$skillName"
params={{ packageName, skillName: s.name }}
to="/intent/registry/$packageName/{$}"
params={{ packageName, _splat: s.name }}
onClick={onNavigate}
className={`flex flex-col items-start px-2 py-1.5 rounded-md text-sm font-mono transition-colors ${
isActive
Expand Down
Loading
Loading