feat(wave2): @mosaic/openclaw-context plugin migrated to monorepo (#3)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #3.
This commit is contained in:
333
plugins/openclaw-context/src/openbrain-client.ts
Normal file
333
plugins/openclaw-context/src/openbrain-client.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import { OpenBrainConfigError, OpenBrainHttpError, OpenBrainRequestError } from "./errors.js";
|
||||
|
||||
export type OpenBrainThoughtMetadata = Record<string, unknown> & {
|
||||
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<OpenBrainThought>;
|
||||
search(input: OpenBrainSearchInput): Promise<OpenBrainThought[]>;
|
||||
listRecent(input: { limit: number; source?: string }): Promise<OpenBrainThought[]>;
|
||||
updateThought(
|
||||
id: string,
|
||||
payload: { content?: string; metadata?: OpenBrainThoughtMetadata },
|
||||
): Promise<OpenBrainThought>;
|
||||
deleteThought(id: string): Promise<void>;
|
||||
deleteThoughts(params: { source?: string; metadataId?: string }): Promise<void>;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function readString(record: Record<string, unknown>, key: string): string | undefined {
|
||||
const value = record[key];
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function readNumber(record: Record<string, unknown>, 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<string, string> {
|
||||
if (headers === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
const normalized: Record<string, string> = {};
|
||||
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<string, string> = {};
|
||||
for (const [key, value] of headers.entries()) {
|
||||
normalized[key] = value;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (!isRecord(headers)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalized: Record<string, string> = {};
|
||||
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<string | undefined> {
|
||||
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<OpenBrainThought> {
|
||||
const payload = await this.request<unknown>("/v1/thoughts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return this.extractThought(payload);
|
||||
}
|
||||
|
||||
async search(input: OpenBrainSearchInput): Promise<OpenBrainThought[]> {
|
||||
const payload = await this.request<unknown>("/v1/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return this.extractThoughtArray(payload);
|
||||
}
|
||||
|
||||
async listRecent(input: { limit: number; source?: string }): Promise<OpenBrainThought[]> {
|
||||
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<unknown>(`/v1/thoughts/recent?${params.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return this.extractThoughtArray(payload);
|
||||
}
|
||||
|
||||
async updateThought(
|
||||
id: string,
|
||||
payload: { content?: string; metadata?: OpenBrainThoughtMetadata },
|
||||
): Promise<OpenBrainThought> {
|
||||
const responsePayload = await this.request<unknown>(`/v1/thoughts/${encodeURIComponent(id)}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
return this.extractThought(responsePayload);
|
||||
}
|
||||
|
||||
async deleteThought(id: string): Promise<void> {
|
||||
await this.request<unknown>(`/v1/thoughts/${encodeURIComponent(id)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
async deleteThoughts(params: { source?: string; metadataId?: string }): Promise<void> {
|
||||
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<unknown>(`/v1/thoughts${suffix}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string, init: RequestInit): Promise<T> {
|
||||
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 };
|
||||
Reference in New Issue
Block a user