// vyrable.ts — single-file TypeScript SDK for the Vyrable REST API. // Version 1.0.0. // // Usage: // import { VyrableClient } from "./vyrable"; // const vy = new VyrableClient({ apiKey: process.env.VYRABLE_API_KEY! }); // const personas = await vy.listPersonas(); // // All methods return parsed JSON. Errors are thrown as VyrableApiError // with .status and .message preserved from the response. export interface VyrableClientOptions { apiKey: string; /// Override the API base URL — useful for staging. baseUrl?: string; } export class VyrableApiError extends Error { constructor(public status: number, public code: string, message: string) { super(message); this.name = "VyrableApiError"; } } // ── Response types ──────────────────────────────────────────────── export type ContentStatus = | "DRAFT" | "GENERATING" | "APPROVED" | "SCHEDULED" | "PUBLISHED" | "FAILED"; export type ContentType = | "POST" | "THREAD" | "ARTICLE" | "CAROUSEL" | "VIDEO_SCRIPT"; export interface ContentSummary { id: string; topic: string; headline: string | null; status: ContentStatus; contentType: ContentType; hashtags: string[]; targetPlatforms: string[]; scheduledFor: string | null; publishedAt: string | null; createdAt: string; persona: { id: string; name: string }; } export interface ContentDetail extends ContentSummary { body: string; seoKeywords: string[]; variants: Array<{ id: string; variantLabel: string; body: string; headline: string | null; overallScore: number | null; isWinner: boolean; }>; } export interface Persona { id: string; name: string; title: string | null; company: string | null; bio: string | null; expertiseAreas: string[]; contentPillars: string[]; language: string; visibility: string; } export interface Idea { id: string; title: string; notes: string | null; status: string; priority: string; tags: string[]; sourceUrl: string | null; sourceName: string | null; dueDate: string | null; createdAt: string; personaId: string | null; contentId: string | null; } export interface Campaign { id: string; name: string; goal: string; status: string; messagingAngle: string | null; contentPillars: string[]; startDate: string | null; endDate: string | null; createdAt: string; } export interface Pagination { total: number; limit: number; offset: number; } // ── Client ───────────────────────────────────────────────────────── export class VyrableClient { private readonly apiKey: string; private readonly baseUrl: string; constructor(opts: VyrableClientOptions) { if (!opts.apiKey) throw new Error("VyrableClient: apiKey is required"); this.apiKey = opts.apiKey; this.baseUrl = opts.baseUrl ?? "https://vyrable.ai"; } // ── Content ───────────────────────────────────────────────────── async listContent(params?: { status?: ContentStatus; limit?: number; offset?: number }) { return this.request<{ data: ContentSummary[]; pagination: Pagination }>( `/api/v1/content${this.qs(params)}`, ); } async getContent(id: string) { return this.request<{ data: ContentDetail }>(`/api/v1/content/${encodeURIComponent(id)}`); } async searchContent(q: string, limit = 10) { return this.request<{ data: ContentSummary[]; query: string }>( `/api/v1/content/search${this.qs({ q, limit })}`, ); } async generateContent(input: { personaId: string; topic: string; contentType?: ContentType; brief?: string; }) { return this.request<{ data: { id: string; status: ContentStatus } }>( `/api/v1/content/generate`, { method: "POST", body: input }, ); } async publishContent(id: string) { return this.request<{ data: { contentId: string; topic: string; succeededPlatforms: string[]; failedPlatforms: { platform: string; error: string }[]; }; message: string; }>(`/api/v1/content/${encodeURIComponent(id)}/publish`, { method: "POST" }); } async scheduleContent(id: string, scheduledFor: string | Date) { const iso = scheduledFor instanceof Date ? scheduledFor.toISOString() : scheduledFor; return this.request<{ data: { id: string; status: ContentStatus; scheduledFor: string }; message: string; }>(`/api/v1/content/${encodeURIComponent(id)}/schedule`, { method: "POST", body: { scheduledFor: iso }, }); } // ── Personas ──────────────────────────────────────────────────── async listPersonas() { return this.request<{ data: Persona[] }>(`/api/v1/personas`); } // ── Ideas ─────────────────────────────────────────────────────── async listIdeas(params?: { status?: string; priority?: string; personaId?: string; limit?: number; offset?: number; }) { return this.request<{ data: Idea[]; pagination: Pagination }>( `/api/v1/ideas${this.qs(params)}`, ); } async captureIdea(input: { title: string; notes?: string; sourceUrl?: string; sourceName?: string; personaId?: string; tags?: string[]; }) { return this.request<{ data: { id: string; title: string; status: string }; message: string }>( `/api/v1/ideas`, { method: "POST", body: input }, ); } // ── Campaigns ─────────────────────────────────────────────────── async listCampaigns(params?: { status?: string; limit?: number }) { return this.request<{ data: Campaign[] }>( `/api/v1/campaigns${this.qs(params)}`, ); } // ── Internal ──────────────────────────────────────────────────── private async request( path: string, init: { method?: string; body?: unknown } = {}, ): Promise { const headers: Record = { Authorization: `Bearer ${this.apiKey}`, }; if (init.body !== undefined) headers["Content-Type"] = "application/json"; const res = await fetch(`${this.baseUrl}${path}`, { method: init.method ?? "GET", headers, body: init.body !== undefined ? JSON.stringify(init.body) : undefined, }); const text = await res.text(); let parsed: unknown = null; try { parsed = text ? JSON.parse(text) : null; } catch { // Non-JSON response; preserve text. } if (!res.ok) { const errBody = parsed && typeof parsed === "object" && parsed !== null ? (parsed as { error?: string; message?: string }) : null; throw new VyrableApiError( res.status, errBody?.error ?? "HTTPError", errBody?.message ?? text || res.statusText, ); } return parsed as T; } private qs(params: Record | undefined): string { if (!params) return ""; const entries = Object.entries(params) .filter(([, v]) => v !== undefined && v !== null && v !== "") .map(([k, v]) => [k, String(v)] as [string, string]); if (entries.length === 0) return ""; return "?" + new URLSearchParams(entries).toString(); } }