Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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,
)
Comment on lines +27 to +31
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