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
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// 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 }] }),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user