Skip to content
← Public packages

@kody/integrations-sh

Client for the public integrations.sh registry: search, detect, cached surface, and live discover.

src/search.ts

83 lines · 2.3 KB · TypeScript
import { fetchRegistryJson } from './client.ts'
import { isRecord } from './is-record.ts'
import { mapSearchResult, type SearchResult } from './types.ts'
import {
	buildSearchUrl,
	isSurfaceKind,
	type SurfaceKind,
} from './urls.ts'

const SEARCH_FETCH_TIMEOUT_MS = 10_000

export type SearchInput = {
	/** Provider name, product, or domain to search for. */
	query: string
	/** Limit results to one catalog kind. */
	kind?: SurfaceKind
	/** Maximum number of results (1–100, default 20). */
	limit?: number
	/** Catalog offset for paging. */
	offset?: number
}

export type SearchOutput = {
	results: Array<SearchResult>
}

/**
 * Search the public integrations.sh catalog.
 * Use this first when you need a canonical hostname or connect-target slugs.
 *
 * @param input - Query plus optional kind, limit, and offset
 * @returns Untrusted catalog matches (domain, kinds, listing URL, surfaces)
 *
 * @example
 * import search from 'kody:@<username>/integrations-sh/search'
 * const { results } = await search({ query: 'github', kind: 'openapi' })
 */
export default async function search(input: SearchInput): Promise<SearchOutput> {
	const query = input.query.trim()
	if (query.length === 0) {
		throw new Error('query is required')
	}
	if (input.kind != null && !isSurfaceKind(input.kind)) {
		throw new Error('kind must be mcp, openapi, graphql, or cli')
	}
	const limit = input.limit ?? 20
	if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
		throw new Error('limit must be an integer between 1 and 100')
	}
	if (
		input.offset != null &&
		(!Number.isInteger(input.offset) || input.offset < 0)
	) {
		throw new Error('offset must be a non-negative integer')
	}

	const url = buildSearchUrl({
		query,
		kind: input.kind,
		limit,
		offset: input.offset,
	})
	const fetched = await fetchRegistryJson(url, SEARCH_FETCH_TIMEOUT_MS)
	if (fetched.outcome !== 'success') {
		const detail =
			fetched.outcome === 'not-found'
				? `HTTP 404 for ${url}`
				: fetched.message
		throw new Error(`integrations.sh search failed: ${detail}`)
	}
	if (!isRecord(fetched.json) || !Array.isArray(fetched.json.results)) {
		throw new Error(
			'integrations.sh search failed: unexpected response shape',
		)
	}

	const results = fetched.json.results
		.map(mapSearchResult)
		.filter((result): result is SearchResult => result != null)
		.slice(0, limit)

	return { results }
}