openclaw/extensions/memory-powermem/client.ts

158 lines
4.3 KiB
TypeScript

/**
* PowerMem HTTP API client.
* Calls POST /api/v1/memories, POST /api/v1/memories/search, DELETE /api/v1/memories/:id, GET /api/v1/system/health.
*/
import type { PowerMemConfig } from "./config.js";
export type PowerMemSearchResult = {
memory_id: number;
content: string;
score: number;
metadata?: Record<string, unknown>;
};
export type PowerMemAddResult = {
memory_id: number;
content: string;
user_id?: string;
agent_id?: string;
metadata?: Record<string, unknown>;
};
function buildUrl(baseUrl: string, path: string): string {
const base = baseUrl.replace(/\/+$/, "");
const p = path.startsWith("/") ? path : `/${path}`;
return `${base}${p}`;
}
function buildHeaders(apiKey?: string): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (apiKey) {
headers["X-API-Key"] = apiKey;
}
return headers;
}
async function handleResponse<T>(res: Response, parseJson = true): Promise<T> {
const text = await res.text();
if (!res.ok) {
let message = `PowerMem API ${res.status}: ${res.statusText}`;
try {
const body = text ? JSON.parse(text) : null;
if (body?.message) message = body.message;
else if (body?.detail) message = Array.isArray(body.detail) ? body.detail.map((d: { msg?: string }) => d.msg ?? String(d)).join("; ") : String(body.detail);
} catch {
if (text) message = text.slice(0, 200);
}
throw new Error(message);
}
if (!parseJson) return undefined as T;
if (!text) return undefined as T;
return JSON.parse(text) as T;
}
export type PowerMemClientOptions = {
baseUrl: string;
apiKey?: string;
userId?: string;
agentId?: string;
};
export class PowerMemClient {
private readonly baseUrl: string;
private readonly apiKey?: string;
private readonly userId: string;
private readonly agentId: string;
constructor(options: PowerMemClientOptions) {
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
this.apiKey = options.apiKey;
this.userId = options.userId ?? "moltbot-user";
this.agentId = options.agentId ?? "moltbot-agent";
}
static fromConfig(cfg: PowerMemConfig, userId: string, agentId: string): PowerMemClient {
return new PowerMemClient({
baseUrl: cfg.baseUrl,
apiKey: cfg.apiKey,
userId,
agentId,
});
}
private async request<T>(
method: string,
path: string,
body?: unknown,
parseJson = true,
): Promise<T> {
const url = buildUrl(this.baseUrl, path);
const res = await fetch(url, {
method,
headers: buildHeaders(this.apiKey),
body: body !== undefined ? JSON.stringify(body) : undefined,
});
return handleResponse<T>(res, parseJson);
}
/** GET /api/v1/system/health */
async health(): Promise<{ status: string }> {
const data = await this.request<{ data?: { status?: string } }>(
"GET",
"/api/v1/system/health",
undefined,
);
return { status: data?.data?.status ?? "unknown" };
}
/** POST /api/v1/memories */
async add(
content: string,
options: { infer?: boolean; metadata?: Record<string, unknown> } = {},
): Promise<PowerMemAddResult[]> {
const body = {
content,
user_id: this.userId,
agent_id: this.agentId,
infer: options.infer ?? true,
...(options.metadata && { metadata: options.metadata }),
};
const res = await this.request<{ success: boolean; data?: PowerMemAddResult[] }>(
"POST",
"/api/v1/memories",
body,
);
if (!res?.data) return [];
return res.data;
}
/** POST /api/v1/memories/search */
async search(query: string, limit = 5): Promise<PowerMemSearchResult[]> {
const body = {
query,
user_id: this.userId,
agent_id: this.agentId,
limit,
};
const res = await this.request<{
success: boolean;
data?: { results?: PowerMemSearchResult[] };
}>("POST", "/api/v1/memories/search", body);
return res?.data?.results ?? [];
}
/** DELETE /api/v1/memories/:memory_id */
async delete(memoryId: number | string): Promise<void> {
const id = typeof memoryId === "string" ? memoryId : String(memoryId);
await this.request(
"DELETE",
`/api/v1/memories/${id}?user_id=${encodeURIComponent(this.userId)}&agent_id=${encodeURIComponent(this.agentId)}`,
undefined,
false,
);
}
}