Repository Public
1/**2 * Skills registry adapter (skills.spboucher.ai). Anonymous, READ-ONLY search of the public REST3 * API (`GET {base}/api/v1/artifacts?q=…&sort=relevance` → `{items:[Artifact]}`; see the registry's4 * SPEC §9 and its SDK). No token, no publishing, and nothing is downloaded or executed:5 * installing from the registry is not implemented in this milestone.6 */7import { z } from 'zod';89export const RegistryResult = z.object({10 name: z.string(),11 type: z.string().nullable(),12 summary: z.string(),13 latestVersion: z.string().nullable(),14 license: z.string().nullable(),15 url: z.string().nullable(),16 publisher: z.string().nullable(),17 verified: z.boolean().nullable(),18});19export type RegistryResult = z.infer<typeof RegistryResult>;20export const RegistrySearch = z.object({ connected: z.boolean(), results: z.array(RegistryResult) });2122export interface SkillsRegistry {23 search(query: string, limit?: number): Promise<RegistryResult[]>;24 getManifest(name: string): Promise<unknown>;25}2627const s = (v: unknown, max = 300): string | null => (typeof v === 'string' && v ? v.slice(0, max) : null);2829export class HttpSkillsRegistry implements SkillsRegistry {30 constructor(31 private readonly baseUrl: string,32 private readonly fetchImpl: typeof fetch,33 ) {}3435 private async get(path: string): Promise<unknown> {36 const res = await this.fetchImpl(`${this.baseUrl}/api/v1${path}`, {37 headers: { accept: 'application/json', 'user-agent': 'claude-remote' },38 signal: AbortSignal.timeout(6000),39 redirect: 'error',40 });41 if (!res.ok) throw new Error(`registry responded ${res.status}`);42 return res.json();43 }4445 async search(query: string, limit = 20): Promise<RegistryResult[]> {46 const q = new URLSearchParams({ q: query, sort: 'relevance', per_page: String(limit) });47 const page = (await this.get(`/artifacts?${q.toString()}`)) as { items?: unknown };48 const items = Array.isArray(page?.items) ? page.items : [];49 const out: RegistryResult[] = [];50 for (const raw of items.slice(0, limit)) {51 const a = raw as Record<string, unknown>;52 const name = s(a.name) ?? (s(a.namespace) && s(a.slug) ? `@${s(a.namespace)}/${s(a.slug)}` : null);53 if (!name) continue;54 const pub = (a.publisher ?? {}) as Record<string, unknown>;55 const url = s(a.url, 500);56 out.push({57 name,58 type: s(a.type, 40),59 summary: s(a.summary, 500) ?? '',60 latestVersion: s(a.latestVersion, 40),61 license: s(a.license, 80),62 url: url && /^https:\/\//.test(url) ? url : null,63 publisher: s(pub.displayName) ?? s(pub.namespace),64 verified: typeof a.sourceVerified === 'boolean' ? a.sourceVerified : null,65 });66 }67 return out;68 }6970 async getManifest(name: string): Promise<unknown> {71 const m = /^@?([a-z0-9-]+)\/([a-z0-9-]+)$/.exec(name);72 if (!m) throw new Error('invalid artifact name');73 return this.get(`/artifacts/${m[1]}/${m[2]}`);74 }75}76