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
86 lines
3.0 KiB
TypeScript
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 }] }),
|
|
});
|
|
}
|
|
}
|