import { OpenBrainConfigError, OpenBrainHttpError, OpenBrainRequestError } from "./errors.js"; export type OpenBrainThoughtMetadata = Record & { sessionId?: string; turn?: number; role?: string; type?: string; }; export type OpenBrainThought = { id: string; content: string; source: string; metadata: OpenBrainThoughtMetadata | undefined; createdAt: string | undefined; updatedAt: string | undefined; score: number | undefined; [key: string]: unknown; }; export type OpenBrainThoughtInput = { content: string; source: string; metadata?: OpenBrainThoughtMetadata; }; export type OpenBrainSearchInput = { query: string; limit: number; source?: string; }; export type OpenBrainClientOptions = { baseUrl: string; apiKey: string; fetchImpl?: typeof fetch; }; export interface OpenBrainClientLike { createThought(input: OpenBrainThoughtInput): Promise; search(input: OpenBrainSearchInput): Promise; listRecent(input: { limit: number; source?: string }): Promise; updateThought( id: string, payload: { content?: string; metadata?: OpenBrainThoughtMetadata }, ): Promise; deleteThought(id: string): Promise; deleteThoughts(params: { source?: string; metadataId?: string }): Promise; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function readString(record: Record, key: string): string | undefined { const value = record[key]; return typeof value === "string" ? value : undefined; } function readNumber(record: Record, key: string): number | undefined { const value = record[key]; return typeof value === "number" ? value : undefined; } function normalizeBaseUrl(baseUrl: string): string { const normalized = baseUrl.trim().replace(/\/+$/, ""); if (normalized.length === 0) { throw new OpenBrainConfigError("Missing required OpenBrain config: baseUrl"); } return normalized; } function normalizeApiKey(apiKey: string): string { const normalized = apiKey.trim(); if (normalized.length === 0) { throw new OpenBrainConfigError("Missing required OpenBrain config: apiKey"); } return normalized; } function normalizeHeaders(headers: unknown): Record { if (headers === undefined) { return {}; } if (Array.isArray(headers)) { const normalized: Record = {}; for (const pair of headers) { if (!Array.isArray(pair) || pair.length < 2) { continue; } const key = pair[0]; const value = pair[1]; if (typeof key !== "string" || typeof value !== "string") { continue; } normalized[key] = value; } return normalized; } if (headers instanceof Headers) { const normalized: Record = {}; for (const [key, value] of headers.entries()) { normalized[key] = value; } return normalized; } if (!isRecord(headers)) { return {}; } const normalized: Record = {}; for (const [key, value] of Object.entries(headers)) { if (typeof value === "string") { normalized[key] = value; continue; } if (Array.isArray(value)) { normalized[key] = value.join(", "); } } return normalized; } async function readResponseBody(response: Response): Promise { try { const body = await response.text(); return body.length > 0 ? body : undefined; } catch { return undefined; } } export class OpenBrainClient implements OpenBrainClientLike { private readonly baseUrl: string; private readonly apiKey: string; private readonly fetchImpl: typeof fetch; constructor(options: OpenBrainClientOptions) { this.baseUrl = normalizeBaseUrl(options.baseUrl); this.apiKey = normalizeApiKey(options.apiKey); this.fetchImpl = options.fetchImpl ?? fetch; } async createThought(input: OpenBrainThoughtInput): Promise { const payload = await this.request("/v1/thoughts", { method: "POST", body: JSON.stringify(input), }); return this.extractThought(payload); } async search(input: OpenBrainSearchInput): Promise { const payload = await this.request("/v1/search", { method: "POST", body: JSON.stringify(input), }); return this.extractThoughtArray(payload); } async listRecent(input: { limit: number; source?: string }): Promise { const params = new URLSearchParams({ limit: String(input.limit), }); if (input.source !== undefined && input.source.length > 0) { params.set("source", input.source); } const payload = await this.request(`/v1/thoughts/recent?${params.toString()}`, { method: "GET", }); return this.extractThoughtArray(payload); } async updateThought( id: string, payload: { content?: string; metadata?: OpenBrainThoughtMetadata }, ): Promise { const responsePayload = await this.request(`/v1/thoughts/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(payload), }); return this.extractThought(responsePayload); } async deleteThought(id: string): Promise { await this.request(`/v1/thoughts/${encodeURIComponent(id)}`, { method: "DELETE", }); } async deleteThoughts(params: { source?: string; metadataId?: string }): Promise { const query = new URLSearchParams(); if (params.source !== undefined && params.source.length > 0) { query.set("source", params.source); } if (params.metadataId !== undefined && params.metadataId.length > 0) { query.set("metadata_id", params.metadataId); } const suffix = query.size > 0 ? `?${query.toString()}` : ""; await this.request(`/v1/thoughts${suffix}`, { method: "DELETE", }); } private async request(endpoint: string, init: RequestInit): Promise { const headers = normalizeHeaders(init.headers); headers.Authorization = `Bearer ${this.apiKey}`; if (init.body !== undefined && headers["Content-Type"] === undefined) { headers["Content-Type"] = "application/json"; } const url = `${this.baseUrl}${endpoint}`; let response: Response; try { response = await this.fetchImpl(url, { ...init, headers, }); } catch (error) { throw new OpenBrainRequestError({ endpoint, cause: error }); } if (!response.ok) { throw new OpenBrainHttpError({ endpoint, status: response.status, responseBody: await readResponseBody(response), }); } if (response.status === 204) { return undefined as T; } const contentType = response.headers.get("content-type") ?? ""; if (!contentType.toLowerCase().includes("application/json")) { return undefined as T; } return (await response.json()) as T; } private extractThoughtArray(payload: unknown): OpenBrainThought[] { if (Array.isArray(payload)) { return payload.map((item) => this.normalizeThought(item)); } if (!isRecord(payload)) { return []; } const candidates = [payload.thoughts, payload.data, payload.results, payload.items]; for (const candidate of candidates) { if (Array.isArray(candidate)) { return candidate.map((item) => this.normalizeThought(item)); } } return []; } private extractThought(payload: unknown): OpenBrainThought { if (isRecord(payload)) { const nested = payload.thought; if (nested !== undefined) { return this.normalizeThought(nested); } const data = payload.data; if (data !== undefined && !Array.isArray(data)) { return this.normalizeThought(data); } } return this.normalizeThought(payload); } private normalizeThought(value: unknown): OpenBrainThought { if (!isRecord(value)) { return { id: "", content: "", source: "", metadata: undefined, createdAt: undefined, updatedAt: undefined, score: undefined, }; } const metadataValue = value.metadata; const metadata = isRecord(metadataValue) ? ({ ...metadataValue } as OpenBrainThoughtMetadata) : undefined; const id = readString(value, "id") ?? readString(value, "thought_id") ?? ""; const content = readString(value, "content") ?? readString(value, "text") ?? (value.content === undefined ? "" : String(value.content)); const source = readString(value, "source") ?? ""; const createdAt = readString(value, "createdAt") ?? readString(value, "created_at"); const updatedAt = readString(value, "updatedAt") ?? readString(value, "updated_at"); const score = readNumber(value, "score"); return { ...value, id, content, source, metadata, createdAt, updatedAt, score, }; } } export { normalizeApiKey, normalizeBaseUrl };