Files
mesh-catalog/modules/letta/client.ts
T
jschoubben 515b1e79c7 Convert baserow, letta, invoicing to mesh catalog DB consumers
Mirror the postgres-consumer shape (keycloak/nextcloud): requires the
backing service(s), binds/secrets for the delivered credential, and a
server container that reads it from an interpolated env file.

- baserow: consumes postgres-database + redis-cache; tooled (dormant
  until an account is configured, like gitea's token).
- letta: consumes postgres-database; tooled, live via a mesh-minted
  server-password injected into both server and runtime.
- invoicing: consumes mongodb-database + s3-bucket; plain two-container
  service (app + api), no tools.

Digests pinned for baserow and letta; invoicing keeps private-registry
tags (DIGEST-UNRESOLVED). redis-cache and mongodb-database consumer
shapes are inferred (no prior consumer in the catalog).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-06 01:12:50 +02:00

86 lines
3.0 KiB
TypeScript

// The Letta API client — letta's own code, living in the module (novox/hq ADR 0039). Its tools
// import it; nothing outside letta does.
//
// Letta authenticates with a single server password, presented as a Bearer token. That password is
// a mesh own-secret, minted once and handed to both the server (LETTA_SERVER_PASSWORD) and this
// client (MESH_LETTA_PASSWORD) — so the module's tools are live without anything configured by hand.
// The runtime config file may still override the URL or password.
import { readFileSync } from "node:fs";
export interface LettaAgent {
id: string;
name: string;
}
export interface LettaMessage {
id?: string;
role?: string;
text?: string;
[key: string]: unknown;
}
function meshConfig(file?: string): Record<string, string> {
if (!file) return {};
try {
return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>;
} catch {
return {};
}
}
export class LettaClient {
readonly baseUrl: string;
constructor(
url: string,
private readonly password: string,
) {
this.baseUrl = url.replace(/\/+$/, "");
}
/**
* Build from the module's resolved environment. URL and password come from the runtime config
* file first (MESH_LETTA_CONFIG_FILE), then the mesh's own names, then the bare LETTA_* names. A
* password is required — Letta rejects unauthenticated calls when SECURE is on — so this throws
* rather than hand back a client that fails on first use.
*/
static fromEnv(env: NodeJS.ProcessEnv = process.env): LettaClient {
const cfg = meshConfig(env.MESH_LETTA_CONFIG_FILE);
const url = cfg.url ?? env.MESH_LETTA_URL ?? env.LETTA_URL ?? "http://127.0.0.1:8283";
const password = cfg.password ?? env.MESH_LETTA_PASSWORD ?? env.LETTA_SERVER_PASSWORD;
if (!password) throw new Error("no Letta password — set MESH_LETTA_PASSWORD");
return new LettaClient(url, password);
}
private async request<T = unknown>(path: string, options: RequestInit = {}): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.password}`,
...(options.headers as Record<string, string> | undefined),
},
});
if (!res.ok) throw new Error(`letta ${path}: ${res.status} ${await res.text()}`);
if (res.status === 204) return null as T;
const text = await res.text();
return (text ? JSON.parse(text) : null) as T;
}
async listAgents(): Promise<LettaAgent[]> {
return (await this.request<LettaAgent[]>(`/v1/agents/`)) ?? [];
}
async getMessages(agentId: string, limit = 20): Promise<LettaMessage[]> {
return (await this.request<LettaMessage[]>(`/v1/agents/${agentId}/messages?limit=${limit}`)) ?? [];
}
async sendMessage(agentId: string, text: string): Promise<unknown> {
return this.request(`/v1/agents/${agentId}/messages`, {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
}
}