SPB Git

Repository Public

registry.ts 2.9 KB 76 lines typescript

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
Files main
Menu

Theme