Files
homelab-mcp/src/lib/projects.ts
adam 94198d3897 Add structured project tools with rich relationships
Replace chunk-based project tools with dedicated structured project
tools that store projects as single Qdrant points with rich JSON
payloads. Adds list_projects, get_project, upsert_project,
update_project, and delete_project tools with Zod discriminated
union validation for per-category relationship operations.
2026-03-01 18:14:37 +00:00

103 lines
2.5 KiB
TypeScript

import { createHash } from "crypto";
import { embed } from "./embeddings.js";
import { getClient, COLLECTION } from "./qdrant.js";
// --- Relationship types ---
export interface HostRelationship {
host: string;
role: string;
description?: string;
}
export interface SoftwareRelationship {
name: string;
role: string;
version?: string;
description?: string;
}
export interface GitRepoRelationship {
repo: string;
path?: string;
role: string;
description?: string;
}
export interface LocalDirRelationship {
path: string;
role: string;
description?: string;
}
export interface ProjectRelationships {
hosts: HostRelationship[];
software: SoftwareRelationship[];
git_repos: GitRepoRelationship[];
local_dirs: LocalDirRelationship[];
}
// --- Project payload (stored as Qdrant point payload) ---
export interface ProjectPayload {
slug: string;
name: string;
status: string;
summary: string;
tags: string[];
current_state: string;
blockers: string[];
decisions_pending: string[];
relationships: ProjectRelationships;
key_facts: string[];
history: string[];
updated: string;
type: "project";
}
// --- Helpers ---
export function projectPointId(slug: string): string {
const hash = createHash("sha256").update(`project\0${slug}`).digest("hex");
return [
hash.slice(0, 8),
hash.slice(8, 12),
hash.slice(12, 16),
hash.slice(16, 20),
hash.slice(20, 32),
].join("-");
}
export function buildEmbeddingText(payload: ProjectPayload): string {
const parts = [payload.summary, payload.current_state, ...payload.key_facts, ...payload.tags];
return parts.filter(Boolean).join(" ");
}
export function emptyRelationships(): ProjectRelationships {
return { hosts: [], software: [], git_repos: [], local_dirs: [] };
}
// --- Data access ---
export async function getProjectPoint(slug: string): Promise<ProjectPayload | null> {
const client = getClient();
const id = projectPointId(slug);
try {
const points = await client.retrieve(COLLECTION, { ids: [id], with_payload: true, with_vector: false });
if (points.length === 0) return null;
return points[0].payload as unknown as ProjectPayload;
} catch {
return null;
}
}
export async function upsertProjectPoint(payload: ProjectPayload): Promise<string> {
const client = getClient();
const id = projectPointId(payload.slug);
const vector = await embed(buildEmbeddingText(payload));
await client.upsert(COLLECTION, {
points: [{ id, vector, payload: payload as unknown as Record<string, unknown> }],
});
return id;
}