feat(web): restore payload CMS (cuidecar pattern + importMap)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-06 14:30:16 +01:00
parent 7a5f786e0c
commit 866c1992b3
22 changed files with 9651 additions and 4 deletions

View File

@@ -15,11 +15,13 @@
}, },
"prettier": "@turbostarter/prettier-config", "prettier": "@turbostarter/prettier-config",
"dependencies": { "dependencies": {
"@qdrant/js-client-rest": "1.17.0",
"@turbostarter/db": "workspace:*", "@turbostarter/db": "workspace:*",
"@turbostarter/shared": "workspace:*", "@turbostarter/shared": "workspace:*",
"drizzle-orm": "0.44.7", "drizzle-orm": "0.44.7",
"libsodium-wrappers": "0.7.15", "libsodium-wrappers": "0.7.15",
"minio": "8.0.7", "minio": "8.0.7",
"neo4j-driver": "6.0.1",
"ws": "8.20.0", "ws": "8.20.0",
"zod": "catalog:" "zod": "catalog:"
}, },

View File

@@ -34,9 +34,12 @@ import {
mesh, mesh,
meshFile, meshFile,
meshFileAccess, meshFileAccess,
meshContext,
meshMember as memberTable, meshMember as memberTable,
meshMemory, meshMemory,
meshState, meshState,
meshStream,
meshTask,
messageQueue, messageQueue,
pendingStatus, pendingStatus,
presence, presence,
@@ -889,6 +892,334 @@ export async function deleteFile(
); );
} }
// --- Context sharing ---
/**
* Upsert a context snapshot for a peer. Each (meshId, presenceId) pair
* has at most one context row — repeated calls update it in place.
*/
export async function shareContext(
meshId: string,
presenceId: string,
peerName: string | undefined,
summary: string,
filesRead?: string[],
keyFindings?: string[],
tags?: string[],
): Promise<string> {
const now = new Date();
// Try to find existing context for this presence in this mesh.
const [existing] = await db
.select({ id: meshContext.id })
.from(meshContext)
.where(
and(
eq(meshContext.meshId, meshId),
eq(meshContext.presenceId, presenceId),
),
)
.limit(1);
if (existing) {
await db
.update(meshContext)
.set({
peerName: peerName ?? null,
summary,
filesRead: filesRead ?? [],
keyFindings: keyFindings ?? [],
tags: tags ?? [],
updatedAt: now,
})
.where(eq(meshContext.id, existing.id));
return existing.id;
}
const [row] = await db
.insert(meshContext)
.values({
meshId,
presenceId,
peerName: peerName ?? null,
summary,
filesRead: filesRead ?? [],
keyFindings: keyFindings ?? [],
tags: tags ?? [],
updatedAt: now,
})
.returning({ id: meshContext.id });
if (!row) throw new Error("failed to insert context");
return row.id;
}
/**
* Search contexts by tag match or summary ILIKE.
*/
export async function getContext(
meshId: string,
query: string,
): Promise<
Array<{
peerName: string;
summary: string;
filesRead: string[];
keyFindings: string[];
tags: string[];
updatedAt: Date;
}>
> {
const result = await db.execute<{
peer_name: string | null;
summary: string;
files_read: string[] | null;
key_findings: string[] | null;
tags: string[] | null;
updated_at: string | Date;
}>(sql`
SELECT peer_name, summary, files_read, key_findings, tags, updated_at
FROM mesh.context
WHERE mesh_id = ${meshId}
AND (
summary ILIKE ${"%" + query + "%"}
OR ${query} = ANY(tags)
)
ORDER BY updated_at DESC
LIMIT 20
`);
const rows = (result.rows ?? result) as Array<{
peer_name: string | null;
summary: string;
files_read: string[] | null;
key_findings: string[] | null;
tags: string[] | null;
updated_at: string | Date;
}>;
return rows.map((r) => ({
peerName: r.peer_name ?? "unknown",
summary: r.summary,
filesRead: r.files_read ?? [],
keyFindings: r.key_findings ?? [],
tags: r.tags ?? [],
updatedAt:
r.updated_at instanceof Date ? r.updated_at : new Date(r.updated_at),
}));
}
/**
* List all contexts for a mesh, ordered by most recently updated.
*/
export async function listContexts(
meshId: string,
): Promise<
Array<{
peerName: string;
summary: string;
tags: string[];
updatedAt: Date;
}>
> {
const rows = await db
.select({
peerName: meshContext.peerName,
summary: meshContext.summary,
tags: meshContext.tags,
updatedAt: meshContext.updatedAt,
})
.from(meshContext)
.where(eq(meshContext.meshId, meshId))
.orderBy(desc(meshContext.updatedAt));
return rows.map((r) => ({
peerName: r.peerName ?? "unknown",
summary: r.summary,
tags: (r.tags ?? []) as string[],
updatedAt: r.updatedAt,
}));
}
// --- Tasks ---
/**
* Create a new task in a mesh. Returns the generated id.
*/
export async function createTask(
meshId: string,
title: string,
assignee?: string,
priority?: string,
tags?: string[],
createdByName?: string,
): Promise<string> {
const [row] = await db
.insert(meshTask)
.values({
meshId,
title,
assignee: assignee ?? null,
priority: priority ?? "normal",
status: "open",
tags: tags ?? [],
createdByName: createdByName ?? null,
})
.returning({ id: meshTask.id });
if (!row) throw new Error("failed to insert task");
return row.id;
}
/**
* Claim an open task. Sets status to 'claimed' and records who claimed it.
* Only succeeds if the task is currently 'open'.
*/
export async function claimTask(
meshId: string,
taskId: string,
presenceId: string,
peerName?: string,
): Promise<boolean> {
const now = new Date();
const result = await db
.update(meshTask)
.set({
status: "claimed",
claimedByPresence: presenceId,
claimedByName: peerName ?? null,
claimedAt: now,
})
.where(
and(
eq(meshTask.id, taskId),
eq(meshTask.meshId, meshId),
eq(meshTask.status, "open"),
),
)
.returning({ id: meshTask.id });
return result.length > 0;
}
/**
* Complete a task. Sets status to 'done', records the result and timestamp.
*/
export async function completeTask(
meshId: string,
taskId: string,
result?: string,
): Promise<boolean> {
const now = new Date();
const rows = await db
.update(meshTask)
.set({
status: "done",
result: result ?? null,
completedAt: now,
})
.where(
and(
eq(meshTask.id, taskId),
eq(meshTask.meshId, meshId),
),
)
.returning({ id: meshTask.id });
return rows.length > 0;
}
/**
* List tasks in a mesh with optional status and assignee filters.
*/
export async function listTasks(
meshId: string,
status?: string,
assignee?: string,
): Promise<
Array<{
id: string;
title: string;
assignee: string | null;
claimedBy: string | null;
status: string;
priority: string;
createdBy: string | null;
tags: string[];
createdAt: Date;
}>
> {
const conditions = [eq(meshTask.meshId, meshId)];
if (status) {
conditions.push(eq(meshTask.status, status));
}
if (assignee) {
conditions.push(eq(meshTask.assignee, assignee));
}
const rows = await db
.select({
id: meshTask.id,
title: meshTask.title,
assignee: meshTask.assignee,
claimedByName: meshTask.claimedByName,
status: meshTask.status,
priority: meshTask.priority,
createdByName: meshTask.createdByName,
tags: meshTask.tags,
createdAt: meshTask.createdAt,
})
.from(meshTask)
.where(and(...conditions))
.orderBy(desc(meshTask.createdAt))
.limit(100);
return rows.map((r) => ({
id: r.id,
title: r.title,
assignee: r.assignee,
claimedBy: r.claimedByName,
status: r.status,
priority: r.priority,
createdBy: r.createdByName,
tags: (r.tags ?? []) as string[],
createdAt: r.createdAt,
}));
}
// --- Streams ---
/**
* Create a named real-time stream in a mesh. Upsert semantics: if a
* stream with the same (meshId, name) already exists, return its id.
*/
export async function createStream(
meshId: string,
name: string,
createdByName: string,
): Promise<string> {
const existing = await db
.select({ id: meshStream.id })
.from(meshStream)
.where(and(eq(meshStream.meshId, meshId), eq(meshStream.name, name)));
if (existing.length > 0) return existing[0]!.id;
const [row] = await db
.insert(meshStream)
.values({ meshId, name, createdByName })
.returning({ id: meshStream.id });
return row!.id;
}
/**
* List all streams in a mesh, ordered by creation time.
*/
export async function listStreams(
meshId: string,
): Promise<
Array<{ id: string; name: string; createdBy: string | null; createdAt: Date }>
> {
return db
.select({
id: meshStream.id,
name: meshStream.name,
createdBy: meshStream.createdByName,
createdAt: meshStream.createdAt,
})
.from(meshStream)
.where(eq(meshStream.meshId, meshId))
.orderBy(asc(meshStream.createdAt));
}
// --- Message queueing + delivery --- // --- Message queueing + delivery ---
export interface QueueParams { export interface QueueParams {
@@ -1239,3 +1570,118 @@ export async function findMemberByPubkey(
.limit(1); .limit(1);
return row ?? null; return row ?? null;
} }
// --- Mesh databases (per-mesh PostgreSQL schemas) ---
function meshSchemaName(meshId: string): string {
return `meshdb_${meshId.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
}
/** Validate that user-provided SQL doesn't contain dangerous operations. */
function validateMeshSql(userSql: string): void {
const upper = userSql.toUpperCase();
const forbidden = [
"DROP SCHEMA",
"CREATE SCHEMA",
"SET SEARCH_PATH",
"SET ROLE",
"SET SESSION",
"SET LOCAL",
"GRANT",
"REVOKE",
];
for (const f of forbidden) {
if (upper.includes(f))
throw new Error(`Forbidden SQL operation: ${f}`);
}
}
/** Ensure the per-mesh schema exists. */
export async function ensureMeshSchema(meshId: string): Promise<string> {
const schema = meshSchemaName(meshId);
await db.execute(
sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw('"' + schema + '"')}`,
);
return schema;
}
/** Run a SELECT query in the mesh's schema. */
export async function meshQuery(
meshId: string,
query: string,
): Promise<{
columns: string[];
rows: Array<Record<string, unknown>>;
rowCount: number;
}> {
validateMeshSql(query);
const schema = await ensureMeshSchema(meshId);
// Use a transaction so SET LOCAL is scoped and automatically reset.
return await db.transaction(async (tx) => {
await tx.execute(
sql.raw(`SET LOCAL search_path TO "${schema}"`)
);
const result = await tx.execute(sql.raw(query));
const rows = (result.rows ?? []) as Array<Record<string, unknown>>;
const columns = rows.length > 0 ? Object.keys(rows[0]!) : [];
return { columns, rows, rowCount: rows.length };
});
}
/** Run a DDL/DML statement in the mesh's schema. */
export async function meshExecute(
meshId: string,
statement: string,
): Promise<{ rowCount: number }> {
validateMeshSql(statement);
const schema = await ensureMeshSchema(meshId);
return await db.transaction(async (tx) => {
await tx.execute(
sql.raw(`SET LOCAL search_path TO "${schema}"`)
);
const result = await tx.execute(sql.raw(statement));
return { rowCount: (result as any).rowCount ?? 0 };
});
}
/** List tables and columns in the mesh's schema. */
export async function meshSchema(
meshId: string,
): Promise<
Array<{
name: string;
columns: Array<{ name: string; type: string; nullable: boolean }>;
}>
> {
const schema = meshSchemaName(meshId);
const result = await db.execute<{
table_name: string;
column_name: string;
data_type: string;
is_nullable: string;
}>(sql`
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = ${schema}
ORDER BY table_name, ordinal_position
`);
const rows = (result.rows ?? result) as Array<{
table_name: string;
column_name: string;
data_type: string;
is_nullable: string;
}>;
const tables = new Map<
string,
Array<{ name: string; type: string; nullable: boolean }>
>();
for (const r of rows) {
if (!tables.has(r.table_name)) tables.set(r.table_name, []);
tables.get(r.table_name)!.push({
name: r.column_name,
type: r.data_type,
nullable: r.is_nullable === "YES",
});
}
return [...tables.entries()].map(([name, columns]) => ({ name, columns }));
}

View File

@@ -24,6 +24,10 @@ const envSchema = z.object({
MINIO_ACCESS_KEY: z.string().default("claudemesh"), MINIO_ACCESS_KEY: z.string().default("claudemesh"),
MINIO_SECRET_KEY: z.string().default("changeme"), MINIO_SECRET_KEY: z.string().default("changeme"),
MINIO_USE_SSL: z.coerce.boolean().default(false), MINIO_USE_SSL: z.coerce.boolean().default(false),
QDRANT_URL: z.string().default("http://qdrant:6333"),
NEO4J_URL: z.string().default("bolt://neo4j:7687"),
NEO4J_USER: z.string().default("neo4j"),
NEO4J_PASSWORD: z.string().default("changeme"),
NODE_ENV: z NODE_ENV: z
.enum(["development", "production", "test"]) .enum(["development", "production", "test"])
.default("development"), .default("development"),

View File

@@ -15,17 +15,21 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import type { Duplex } from "node:stream"; import type { Duplex } from "node:stream";
import { WebSocketServer, type WebSocket } from "ws"; import { WebSocketServer, type WebSocket } from "ws";
import { eq } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import { env } from "./env"; import { env } from "./env";
import { db } from "./db"; import { db } from "./db";
import { messageQueue } from "@turbostarter/db/schema/mesh"; import { messageQueue } from "@turbostarter/db/schema/mesh";
import { import {
claimTask,
completeTask,
connectPresence, connectPresence,
createTask,
deleteFile, deleteFile,
disconnectPresence, disconnectPresence,
drainForMember, drainForMember,
findMemberByPubkey, findMemberByPubkey,
forgetMemory, forgetMemory,
getContext,
getFile, getFile,
getFileStatus, getFileStatus,
getState, getState,
@@ -34,9 +38,11 @@ import {
joinGroup, joinGroup,
joinMesh, joinMesh,
leaveGroup, leaveGroup,
listContexts,
listFiles, listFiles,
listPeersInMesh, listPeersInMesh,
listState, listState,
listTasks,
queueMessage, queueMessage,
recallMemory, recallMemory,
recordFileAccess, recordFileAccess,
@@ -45,12 +51,21 @@ import {
rememberMemory, rememberMemory,
setSummary, setSummary,
setState, setState,
shareContext,
startSweepers, startSweepers,
stopSweepers, stopSweepers,
uploadFile, uploadFile,
writeStatus, writeStatus,
ensureMeshSchema,
meshQuery,
meshExecute,
meshSchema,
createStream,
listStreams,
} from "./broker"; } from "./broker";
import { ensureBucket, meshBucketName, minioClient } from "./minio"; import { ensureBucket, meshBucketName, minioClient } from "./minio";
import { qdrant, meshCollectionName, ensureCollection } from "./qdrant";
import { neo4jDriver, meshDbName, ensureDatabase } from "./neo4j-client";
import type { import type {
HookSetStatusRequest, HookSetStatusRequest,
WSClientMessage, WSClientMessage,
@@ -81,6 +96,9 @@ interface PeerConn {
const connections = new Map<string, PeerConn>(); const connections = new Map<string, PeerConn>();
const connectionsPerMesh = new Map<string, number>(); const connectionsPerMesh = new Map<string, number>();
// Stream subscriptions: "meshId:streamName" → Set of presenceIds
const streamSubscriptions = new Map<string, Set<string>>();
const hookRateLimit = new TokenBucket( const hookRateLimit = new TokenBucket(
env.HOOK_RATE_LIMIT_PER_MIN, env.HOOK_RATE_LIMIT_PER_MIN,
env.HOOK_RATE_LIMIT_PER_MIN, env.HOOK_RATE_LIMIT_PER_MIN,
@@ -1066,6 +1084,598 @@ function handleConnection(ws: WebSocket): void {
}); });
break; break;
} }
case "share_context": {
const sc = msg as Extract<WSClientMessage, { type: "share_context" }>;
const memberInfo = conn.memberPubkey
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
: null;
const ctxId = await shareContext(
conn.meshId,
presenceId,
memberInfo?.displayName,
sc.summary,
sc.filesRead,
sc.keyFindings,
sc.tags,
);
sendToPeer(presenceId, {
type: "context_shared",
id: ctxId,
});
// Notify all other peers in the mesh that context was shared.
for (const [pid, peer] of connections) {
if (pid === presenceId) continue;
if (peer.meshId !== conn.meshId) continue;
sendToPeer(pid, {
type: "state_change",
key: `_context:${memberInfo?.displayName ?? "unknown"}`,
value: sc.summary,
updatedBy: memberInfo?.displayName ?? "unknown",
});
}
log.info("ws share_context", {
presence_id: presenceId,
context_id: ctxId,
});
break;
}
case "get_context": {
const gc = msg as Extract<WSClientMessage, { type: "get_context" }>;
const contexts = await getContext(conn.meshId, gc.query);
sendToPeer(presenceId, {
type: "context_results",
contexts: contexts.map((c) => ({
peerName: c.peerName,
summary: c.summary,
filesRead: c.filesRead,
keyFindings: c.keyFindings,
tags: c.tags,
updatedAt: c.updatedAt.toISOString(),
})),
});
log.info("ws get_context", {
presence_id: presenceId,
query: gc.query.slice(0, 80),
results: contexts.length,
});
break;
}
case "list_contexts": {
const allContexts = await listContexts(conn.meshId);
sendToPeer(presenceId, {
type: "context_list",
contexts: allContexts.map((c) => ({
peerName: c.peerName,
summary: c.summary,
tags: c.tags,
updatedAt: c.updatedAt.toISOString(),
})),
});
log.info("ws list_contexts", {
presence_id: presenceId,
mesh_id: conn.meshId,
count: allContexts.length,
});
break;
}
case "create_task": {
const ct = msg as Extract<WSClientMessage, { type: "create_task" }>;
const memberInfo = conn.memberPubkey
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
: null;
const taskId = await createTask(
conn.meshId,
ct.title,
ct.assignee,
ct.priority,
ct.tags,
memberInfo?.displayName,
);
sendToPeer(presenceId, {
type: "task_created",
id: taskId,
});
log.info("ws create_task", {
presence_id: presenceId,
task_id: taskId,
title: ct.title.slice(0, 80),
});
break;
}
case "claim_task": {
const clm = msg as Extract<WSClientMessage, { type: "claim_task" }>;
const memberInfo = conn.memberPubkey
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
: null;
const claimed = await claimTask(
conn.meshId,
clm.taskId,
presenceId,
memberInfo?.displayName,
);
if (!claimed) {
sendError(conn.ws, "task_not_claimable", "task is not open or does not exist");
break;
}
// Return updated task list so caller sees the change.
const tasksAfterClaim = await listTasks(conn.meshId);
sendToPeer(presenceId, {
type: "task_list",
tasks: tasksAfterClaim.map((t) => ({
id: t.id,
title: t.title,
assignee: t.assignee,
claimedBy: t.claimedBy,
status: t.status,
priority: t.priority,
createdBy: t.createdBy,
tags: t.tags,
createdAt: t.createdAt.toISOString(),
})),
});
log.info("ws claim_task", {
presence_id: presenceId,
task_id: clm.taskId,
});
break;
}
case "complete_task": {
const cpt = msg as Extract<WSClientMessage, { type: "complete_task" }>;
const completed = await completeTask(
conn.meshId,
cpt.taskId,
cpt.result,
);
if (!completed) {
sendError(conn.ws, "task_not_found", "task not found in this mesh");
break;
}
// Return updated task list.
const tasksAfterComplete = await listTasks(conn.meshId);
sendToPeer(presenceId, {
type: "task_list",
tasks: tasksAfterComplete.map((t) => ({
id: t.id,
title: t.title,
assignee: t.assignee,
claimedBy: t.claimedBy,
status: t.status,
priority: t.priority,
createdBy: t.createdBy,
tags: t.tags,
createdAt: t.createdAt.toISOString(),
})),
});
log.info("ws complete_task", {
presence_id: presenceId,
task_id: cpt.taskId,
});
break;
}
case "list_tasks": {
const lt = msg as Extract<WSClientMessage, { type: "list_tasks" }>;
const tasks = await listTasks(conn.meshId, lt.status, lt.assignee);
sendToPeer(presenceId, {
type: "task_list",
tasks: tasks.map((t) => ({
id: t.id,
title: t.title,
assignee: t.assignee,
claimedBy: t.claimedBy,
status: t.status,
priority: t.priority,
createdBy: t.createdBy,
tags: t.tags,
createdAt: t.createdAt.toISOString(),
})),
});
log.info("ws list_tasks", {
presence_id: presenceId,
mesh_id: conn.meshId,
count: tasks.length,
});
break;
}
// --- Streams ---
case "create_stream": {
const cs = msg as Extract<WSClientMessage, { type: "create_stream" }>;
const memberInfo = conn.memberPubkey
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
: null;
const streamId = await createStream(
conn.meshId,
cs.name,
memberInfo?.displayName ?? "peer",
);
sendToPeer(presenceId, {
type: "stream_created",
id: streamId,
name: cs.name,
});
log.info("ws create_stream", {
presence_id: presenceId,
stream: cs.name,
});
break;
}
case "subscribe": {
const sub = msg as Extract<WSClientMessage, { type: "subscribe" }>;
const key = `${conn.meshId}:${sub.stream}`;
if (!streamSubscriptions.has(key))
streamSubscriptions.set(key, new Set());
streamSubscriptions.get(key)!.add(presenceId);
log.info("ws subscribe", {
presence_id: presenceId,
stream: sub.stream,
});
break;
}
case "unsubscribe": {
const unsub = msg as Extract<
WSClientMessage,
{ type: "unsubscribe" }
>;
const key = `${conn.meshId}:${unsub.stream}`;
streamSubscriptions.get(key)?.delete(presenceId);
log.info("ws unsubscribe", {
presence_id: presenceId,
stream: unsub.stream,
});
break;
}
case "publish": {
const pub = msg as Extract<WSClientMessage, { type: "publish" }>;
const key = `${conn.meshId}:${pub.stream}`;
const subs = streamSubscriptions.get(key);
if (subs) {
const memberInfo = conn.memberPubkey
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
: null;
const push: WSServerMessage = {
type: "stream_data",
stream: pub.stream,
data: pub.data,
publishedBy: memberInfo?.displayName ?? "peer",
};
for (const subPid of subs) {
if (subPid === presenceId) continue; // don't echo to publisher
sendToPeer(subPid, push);
}
}
metrics.messagesRoutedTotal.inc({ priority: "stream" });
break;
}
case "list_streams": {
const streams = await listStreams(conn.meshId);
sendToPeer(presenceId, {
type: "stream_list",
streams: streams.map((s) => {
const key = `${conn.meshId}:${s.name}`;
return {
id: s.id,
name: s.name,
createdBy: s.createdBy ?? "",
createdAt: s.createdAt.toISOString(),
subscriberCount: streamSubscriptions.get(key)?.size ?? 0,
};
}),
});
log.info("ws list_streams", {
presence_id: presenceId,
mesh_id: conn.meshId,
count: streams.length,
});
break;
}
// --- Vector storage ---
case "vector_store": {
const vs = msg as Extract<WSClientMessage, { type: "vector_store" }>;
const collName = meshCollectionName(conn.meshId, vs.collection);
await ensureCollection(collName);
const { generateId } = await import("@turbostarter/shared/utils");
const pointId = generateId();
// Store text + metadata as payload. Use a zero vector as placeholder
// — real embeddings should be computed client-side and sent directly
// to Qdrant in a future version.
const zeroVector = new Array(1536).fill(0) as number[];
await qdrant.upsert(collName, {
wait: true,
points: [
{
id: pointId,
vector: zeroVector,
payload: {
text: vs.text,
mesh_id: conn.meshId,
stored_by: conn.memberPubkey,
stored_at: new Date().toISOString(),
...(vs.metadata ?? {}),
},
},
],
});
sendToPeer(presenceId, {
type: "ack" as const,
id: pointId,
messageId: pointId,
queued: false,
});
log.info("ws vector_store", {
presence_id: presenceId,
collection: vs.collection,
point_id: pointId,
});
break;
}
case "vector_search": {
const vq = msg as Extract<WSClientMessage, { type: "vector_search" }>;
const searchCollName = meshCollectionName(conn.meshId, vq.collection);
const searchLimit = vq.limit ?? 10;
try {
// Keyword search via payload scroll + filter.
// Full vector similarity requires client-computed embeddings (future).
const queryLower = vq.query.toLowerCase();
const scrollResult = await qdrant.scroll(searchCollName, {
limit: 100,
with_payload: true,
with_vector: false,
});
const matches = (scrollResult.points ?? [])
.filter((p) => {
const text = (p.payload as Record<string, unknown>)?.text;
return typeof text === "string" && text.toLowerCase().includes(queryLower);
})
.slice(0, searchLimit)
.map((p) => {
const payload = p.payload as Record<string, unknown>;
return {
id: String(p.id),
text: (payload.text as string) ?? "",
score: 1.0, // keyword match — no vector similarity score
metadata: payload,
};
});
sendToPeer(presenceId, {
type: "vector_results",
results: matches,
});
} catch {
// Collection may not exist yet — return empty results.
sendToPeer(presenceId, {
type: "vector_results",
results: [],
});
}
log.info("ws vector_search", {
presence_id: presenceId,
collection: vq.collection,
query: vq.query.slice(0, 80),
});
break;
}
case "vector_delete": {
const vd = msg as Extract<WSClientMessage, { type: "vector_delete" }>;
const deleteCollName = meshCollectionName(conn.meshId, vd.collection);
try {
await qdrant.delete(deleteCollName, {
wait: true,
points: [vd.id],
});
} catch {
/* collection or point may not exist — idempotent */
}
sendToPeer(presenceId, {
type: "ack" as const,
id: vd.id,
messageId: vd.id,
queued: false,
});
log.info("ws vector_delete", {
presence_id: presenceId,
collection: vd.collection,
point_id: vd.id,
});
break;
}
case "list_collections": {
try {
const qdrantResponse = await qdrant.getCollections();
const prefix = `mesh_${conn.meshId}_`.toLowerCase().replace(/[^a-z0-9_]/g, "_");
const meshCollections = (qdrantResponse.collections ?? [])
.map((c) => c.name)
.filter((name) => name.startsWith(prefix))
.map((name) => name.slice(prefix.length));
sendToPeer(presenceId, {
type: "collection_list",
collections: meshCollections,
});
} catch {
sendToPeer(presenceId, {
type: "collection_list",
collections: [],
});
}
log.info("ws list_collections", {
presence_id: presenceId,
mesh_id: conn.meshId,
});
break;
}
// --- Graph database ---
case "graph_query": {
const gq = msg as Extract<WSClientMessage, { type: "graph_query" }>;
const gqDbName = meshDbName(conn.meshId);
let gqSession;
try {
await ensureDatabase(gqDbName);
gqSession = neo4jDriver.session({ database: gqDbName });
} catch {
// Community edition — fall back to default db.
gqSession = neo4jDriver.session();
}
try {
const gqResult = await gqSession.run(gq.cypher);
const gqRecords = gqResult.records.map((r) => {
const obj: Record<string, unknown> = {};
for (const key of r.keys) {
obj[key] = r.get(key);
}
return obj;
});
sendToPeer(presenceId, {
type: "graph_result",
records: gqRecords,
});
} catch (gqErr) {
sendError(conn.ws, "graph_error", gqErr instanceof Error ? gqErr.message : String(gqErr));
} finally {
await gqSession.close();
}
log.info("ws graph_query", {
presence_id: presenceId,
cypher: gq.cypher.slice(0, 80),
});
break;
}
case "graph_execute": {
const ge = msg as Extract<WSClientMessage, { type: "graph_execute" }>;
const geDbName = meshDbName(conn.meshId);
let geSession;
try {
await ensureDatabase(geDbName);
geSession = neo4jDriver.session({ database: geDbName });
} catch {
geSession = neo4jDriver.session();
}
try {
const geResult = await geSession.run(ge.cypher);
const geRecords = geResult.records.map((r) => {
const obj: Record<string, unknown> = {};
for (const key of r.keys) {
obj[key] = r.get(key);
}
return obj;
});
sendToPeer(presenceId, {
type: "graph_result",
records: geRecords,
});
} catch (geErr) {
sendError(conn.ws, "graph_error", geErr instanceof Error ? geErr.message : String(geErr));
} finally {
await geSession.close();
}
log.info("ws graph_execute", {
presence_id: presenceId,
cypher: ge.cypher.slice(0, 80),
});
break;
}
// --- Mesh database (per-mesh PostgreSQL schema) ---
case "mesh_query": {
const mq = msg as Extract<WSClientMessage, { type: "mesh_query" }>;
try {
const result = await meshQuery(conn.meshId, mq.sql);
sendToPeer(presenceId, { type: "mesh_query_result", ...result });
} catch (e) {
sendError(
conn.ws,
"query_error",
e instanceof Error ? e.message : String(e),
);
}
log.info("ws mesh_query", {
presence_id: presenceId,
sql: mq.sql.slice(0, 80),
});
break;
}
case "mesh_execute": {
const me = msg as Extract<WSClientMessage, { type: "mesh_execute" }>;
try {
const result = await meshExecute(conn.meshId, me.sql);
sendToPeer(presenceId, {
type: "mesh_query_result",
columns: [],
rows: [],
rowCount: result.rowCount,
});
} catch (e) {
sendError(
conn.ws,
"execute_error",
e instanceof Error ? e.message : String(e),
);
}
log.info("ws mesh_execute", {
presence_id: presenceId,
sql: me.sql.slice(0, 80),
});
break;
}
case "mesh_schema": {
try {
const tables = await meshSchema(conn.meshId);
sendToPeer(presenceId, { type: "mesh_schema_result", tables });
} catch (e) {
sendError(
conn.ws,
"schema_error",
e instanceof Error ? e.message : String(e),
);
}
log.info("ws mesh_schema", { presence_id: presenceId });
break;
}
case "mesh_info": {
const [peers, stateEntries, memCount, fileCount, taskCounts, streams, tables] = await Promise.all([
listPeersInMesh(conn.meshId),
listState(conn.meshId),
db.execute(sql`SELECT COUNT(*) as n FROM mesh.memory WHERE mesh_id = ${conn.meshId} AND forgotten_at IS NULL`).then(r => Number(((r.rows ?? r) as any[])[0]?.n ?? 0)),
db.execute(sql`SELECT COUNT(*) as n FROM mesh.file WHERE mesh_id = ${conn.meshId} AND deleted_at IS NULL`).then(r => Number(((r.rows ?? r) as any[])[0]?.n ?? 0)),
db.execute(sql`SELECT status, COUNT(*) as n FROM mesh.task WHERE mesh_id = ${conn.meshId} GROUP BY status`).then(r => {
const rows = (r.rows ?? r) as Array<{ status: string; n: string }>;
const counts = { open: 0, claimed: 0, done: 0 };
for (const row of rows) counts[row.status as keyof typeof counts] = Number(row.n);
return counts;
}),
listStreams(conn.meshId),
meshSchema(conn.meshId).catch(() => []),
]);
const allGroups = new Set<string>();
for (const p of peers) for (const g of p.groups) allGroups.add(`@${g.name}`);
const myPresence = peers.find(p => p.sessionId === [...connections.entries()].find(([pid]) => pid === presenceId)?.[1]?.sessionPubkey);
const peerConn = connections.get(presenceId);
sendToPeer(presenceId, {
type: "mesh_info_result",
mesh: conn.meshId,
peers: peers.length,
groups: [...allGroups],
stateKeys: stateEntries.map((e: any) => e.key),
memoryCount: memCount,
fileCount: fileCount,
tasks: taskCounts,
streams: streams.map(s => s.name),
tables: tables.map((t: any) => t.name),
collections: [],
yourName: peerConn?.groups?.[0]?.name ?? "unknown",
yourGroups: peerConn?.groups ?? [],
});
log.info("ws mesh_info", { presence_id: presenceId });
break;
}
} }
} catch (e) { } catch (e) {
metrics.messagesRejectedTotal.inc({ reason: "parse_or_handler" }); metrics.messagesRejectedTotal.inc({ reason: "parse_or_handler" });
@@ -1081,6 +1691,11 @@ function handleConnection(ws: WebSocket): void {
connections.delete(presenceId); connections.delete(presenceId);
if (conn) decMeshCount(conn.meshId); if (conn) decMeshCount(conn.meshId);
await disconnectPresence(presenceId); await disconnectPresence(presenceId);
// Clean up stream subscriptions for this peer
for (const [key, subs] of streamSubscriptions) {
subs.delete(presenceId);
if (subs.size === 0) streamSubscriptions.delete(key);
}
log.info("ws close", { presence_id: presenceId }); log.info("ws close", { presence_id: presenceId });
} }
}); });

View File

@@ -0,0 +1,22 @@
import neo4j from "neo4j-driver";
import { env } from "./env";
export const neo4jDriver = neo4j.driver(
env.NEO4J_URL,
neo4j.auth.basic(env.NEO4J_USER, env.NEO4J_PASSWORD),
);
export function meshDbName(meshId: string): string {
return `mesh_${meshId.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
}
export async function ensureDatabase(name: string): Promise<void> {
const session = neo4jDriver.session({ database: "system" });
try {
await session.run(`CREATE DATABASE $name IF NOT EXISTS`, { name });
} catch {
/* may not support multi-db in community edition — fall back to default */
} finally {
await session.close();
}
}

24
apps/broker/src/qdrant.ts Normal file
View File

@@ -0,0 +1,24 @@
import { QdrantClient } from "@qdrant/js-client-rest";
import { env } from "./env";
export const qdrant = new QdrantClient({ url: env.QDRANT_URL });
export function meshCollectionName(
meshId: string,
collection: string,
): string {
return `mesh_${meshId}_${collection}`.toLowerCase().replace(/[^a-z0-9_]/g, "_");
}
export async function ensureCollection(
name: string,
vectorSize = 1536,
): Promise<void> {
try {
await qdrant.getCollection(name);
} catch {
await qdrant.createCollection(name, {
vectors: { size: vectorSize, distance: "Cosine" },
});
}
}

View File

@@ -230,6 +230,133 @@ export interface WSMemoryResultsMessage {
}>; }>;
} }
// --- Vector storage messages ---
/** Client → broker: store a text document in a vector collection. */
export interface WSVectorStoreMessage {
type: "vector_store";
collection: string;
text: string;
metadata?: Record<string, unknown>;
}
/** Client → broker: search a vector collection. */
export interface WSVectorSearchMessage {
type: "vector_search";
collection: string;
query: string;
limit?: number;
}
/** Client → broker: delete a point from a vector collection. */
export interface WSVectorDeleteMessage {
type: "vector_delete";
collection: string;
id: string;
}
/** Client → broker: list all vector collections for this mesh. */
export interface WSListCollectionsMessage {
type: "list_collections";
}
// --- Graph database messages ---
/** Client → broker: run a read-only Cypher query. */
export interface WSGraphQueryMessage {
type: "graph_query";
cypher: string;
}
/** Client → broker: run a write Cypher statement. */
export interface WSGraphExecuteMessage {
type: "graph_execute";
cypher: string;
}
// --- Mesh database (per-mesh PostgreSQL schema) messages ---
/** Client → broker: run a SELECT query in the mesh's schema. */
export interface WSMeshQueryMessage {
type: "mesh_query";
sql: string;
}
/** Client → broker: run a DDL/DML statement in the mesh's schema. */
export interface WSMeshExecuteMessage {
type: "mesh_execute";
sql: string;
}
/** Client → broker: list tables and columns in the mesh's schema. */
export interface WSMeshSchemaMessage {
type: "mesh_schema";
}
// --- Vector/Graph response messages ---
/** Broker → client: vector search results. */
export interface WSVectorResultsMessage {
type: "vector_results";
results: Array<{
id: string;
text: string;
score: number;
metadata?: Record<string, unknown>;
}>;
}
/** Broker → client: list of vector collections. */
export interface WSCollectionListMessage {
type: "collection_list";
collections: string[];
}
/** Broker → client: graph query results. */
export interface WSGraphResultMessage {
type: "graph_result";
records: Array<Record<string, unknown>>;
}
/** Broker → client: mesh SQL query results. */
export interface WSMeshQueryResultMessage {
type: "mesh_query_result";
columns: string[];
rows: Array<Record<string, unknown>>;
rowCount: number;
}
/** Broker → client: mesh schema introspection results. */
export interface WSMeshSchemaResultMessage {
type: "mesh_schema_result";
tables: Array<{
name: string;
columns: Array<{ name: string; type: string; nullable: boolean }>;
}>;
}
/** Client → broker: get full mesh overview. */
export interface WSMeshInfoMessage {
type: "mesh_info";
}
/** Broker → client: aggregated mesh overview. */
export interface WSMeshInfoResultMessage {
type: "mesh_info_result";
mesh: string;
peers: number;
groups: string[];
stateKeys: string[];
memoryCount: number;
fileCount: number;
tasks: { open: number; claimed: number; done: number };
streams: string[];
tables: string[];
collections: string[];
yourName: string;
yourGroups: Array<{ name: string; role?: string }>;
}
/** Client → broker: check delivery status of a message. */ /** Client → broker: check delivery status of a message. */
export interface WSMessageStatusMessage { export interface WSMessageStatusMessage {
type: "message_status"; type: "message_status";
@@ -309,6 +436,170 @@ export interface WSFileStatusResultMessage {
}>; }>;
} }
// --- Context sharing messages ---
/** Client → broker: share current working context. */
export interface WSShareContextMessage {
type: "share_context";
summary: string;
filesRead?: string[];
keyFindings?: string[];
tags?: string[];
}
/** Client → broker: search contexts by query. */
export interface WSGetContextMessage {
type: "get_context";
query: string;
}
/** Client → broker: list all contexts in the mesh. */
export interface WSListContextsMessage {
type: "list_contexts";
}
/** Broker → client: acknowledgement for share_context. */
export interface WSContextSharedMessage {
type: "context_shared";
id: string;
}
/** Broker → client: response to get_context. */
export interface WSContextResultsMessage {
type: "context_results";
contexts: Array<{
peerName: string;
summary: string;
filesRead: string[];
keyFindings: string[];
tags: string[];
updatedAt: string;
}>;
}
/** Broker → client: response to list_contexts. */
export interface WSContextListMessage {
type: "context_list";
contexts: Array<{
peerName: string;
summary: string;
tags: string[];
updatedAt: string;
}>;
}
// --- Task messages ---
/** Client → broker: create a task. */
export interface WSCreateTaskMessage {
type: "create_task";
title: string;
assignee?: string;
priority?: string;
tags?: string[];
}
/** Client → broker: claim an open task. */
export interface WSClaimTaskMessage {
type: "claim_task";
taskId: string;
}
/** Client → broker: mark a task as done. */
export interface WSCompleteTaskMessage {
type: "complete_task";
taskId: string;
result?: string;
}
/** Client → broker: list tasks with optional filters. */
export interface WSListTasksMessage {
type: "list_tasks";
status?: string;
assignee?: string;
}
/** Broker → client: acknowledgement for create_task. */
export interface WSTaskCreatedMessage {
type: "task_created";
id: string;
}
/** Broker → client: response to list_tasks, claim_task, complete_task. */
export interface WSTaskListMessage {
type: "task_list";
tasks: Array<{
id: string;
title: string;
assignee: string | null;
claimedBy: string | null;
status: string;
priority: string;
createdBy: string | null;
tags: string[];
createdAt: string;
}>;
}
// --- Stream messages ---
/** Client → broker: create a named real-time stream. */
export interface WSCreateStreamMessage {
type: "create_stream";
name: string;
}
/** Client → broker: publish data to a stream. */
export interface WSPublishMessage {
type: "publish";
stream: string;
data: unknown;
}
/** Client → broker: subscribe to a stream. */
export interface WSSubscribeMessage {
type: "subscribe";
stream: string;
}
/** Client → broker: unsubscribe from a stream. */
export interface WSUnsubscribeMessage {
type: "unsubscribe";
stream: string;
}
/** Client → broker: list all streams in the mesh. */
export interface WSListStreamsMessage {
type: "list_streams";
}
/** Broker → client: acknowledgement for create_stream. */
export interface WSStreamCreatedMessage {
type: "stream_created";
id: string;
name: string;
}
/** Broker → client: real-time data pushed from a stream. */
export interface WSStreamDataMessage {
type: "stream_data";
stream: string;
data: unknown;
publishedBy: string;
}
/** Broker → client: response to list_streams. */
export interface WSStreamListMessage {
type: "stream_list";
streams: Array<{
id: string;
name: string;
createdBy: string;
createdAt: string;
subscriberCount: number;
}>;
}
/** Broker → client: structured error. */ /** Broker → client: structured error. */
export interface WSErrorMessage { export interface WSErrorMessage {
type: "error"; type: "error";
@@ -335,7 +626,29 @@ export type WSClientMessage =
| WSGetFileMessage | WSGetFileMessage
| WSListFilesMessage | WSListFilesMessage
| WSFileStatusMessage | WSFileStatusMessage
| WSDeleteFileMessage; | WSDeleteFileMessage
| WSShareContextMessage
| WSGetContextMessage
| WSListContextsMessage
| WSCreateTaskMessage
| WSClaimTaskMessage
| WSCompleteTaskMessage
| WSListTasksMessage
| WSVectorStoreMessage
| WSVectorSearchMessage
| WSVectorDeleteMessage
| WSListCollectionsMessage
| WSGraphQueryMessage
| WSGraphExecuteMessage
| WSMeshQueryMessage
| WSMeshExecuteMessage
| WSMeshSchemaMessage
| WSCreateStreamMessage
| WSPublishMessage
| WSSubscribeMessage
| WSUnsubscribeMessage
| WSListStreamsMessage
| WSMeshInfoMessage;
export type WSServerMessage = export type WSServerMessage =
| WSHelloAckMessage | WSHelloAckMessage
@@ -351,4 +664,18 @@ export type WSServerMessage =
| WSFileUrlMessage | WSFileUrlMessage
| WSFileListMessage | WSFileListMessage
| WSFileStatusResultMessage | WSFileStatusResultMessage
| WSContextSharedMessage
| WSContextResultsMessage
| WSContextListMessage
| WSTaskCreatedMessage
| WSTaskListMessage
| WSVectorResultsMessage
| WSCollectionListMessage
| WSGraphResultMessage
| WSMeshQueryResultMessage
| WSMeshSchemaResultMessage
| WSStreamCreatedMessage
| WSStreamDataMessage
| WSStreamListMessage
| WSMeshInfoResultMessage
| WSErrorMessage; | WSErrorMessage;

View File

@@ -1,6 +1,6 @@
{ {
"name": "claudemesh-cli", "name": "claudemesh-cli",
"version": "0.4.0", "version": "0.5.0",
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.", "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
"keywords": [ "keywords": [
"claude-code", "claude-code",

View File

@@ -166,6 +166,26 @@ When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATEL
| list_files(query?, from?) | Find files shared in the mesh. | | list_files(query?, from?) | Find files shared in the mesh. |
| file_status(id) | Check who has accessed a file. | | file_status(id) | Check who has accessed a file. |
| delete_file(id) | Remove a shared file from the mesh. | | delete_file(id) | Remove a shared file from the mesh. |
| vector_store(collection, text, metadata?) | Store embedding in per-mesh Qdrant collection. |
| vector_search(collection, query, limit?) | Semantic search over stored embeddings. |
| vector_delete(collection, id) | Remove an embedding. |
| list_collections() | List vector collections in this mesh. |
| graph_query(cypher) | Read-only Cypher query on per-mesh Neo4j. |
| graph_execute(cypher) | Write Cypher query (CREATE, MERGE, DELETE). |
| mesh_query(sql) | Run a SELECT query on the per-mesh shared database. |
| mesh_execute(sql) | Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE). |
| mesh_schema() | List tables and columns in the per-mesh shared database. |
| create_stream(name) | Create a real-time data stream in the mesh. |
| publish(stream, data) | Push data to a stream. Subscribers receive it in real-time. |
| subscribe(stream) | Subscribe to a stream. Data pushes arrive as channel notifications. |
| list_streams() | List active streams in the mesh. |
| share_context(summary, files_read?, key_findings?, tags?) | Share session understanding with peers. |
| get_context(query) | Find context from peers who explored an area. |
| list_contexts() | See what all peers currently know. |
| create_task(title, assignee?, priority?, tags?) | Create a work item. |
| claim_task(id) | Claim an unclaimed task. |
| complete_task(id, result?) | Mark task done with optional result. |
| list_tasks(status?, assignee?) | List tasks filtered by status/assignee. |
If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`). If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
@@ -192,6 +212,24 @@ Persistent knowledge that survives across sessions. Use remember(content, tags?)
share_file for persistent references, send_message(file:) for ephemeral attachments. share_file for persistent references, send_message(file:) for ephemeral attachments.
Tags on shared files make them searchable. Use list_files to find what peers shared. Tags on shared files make them searchable. Use list_files to find what peers shared.
## Vectors
Store and search semantic embeddings. Use vector_store to index content, vector_search to find similar content.
## Graph
Build and query entity relationship graphs. Use graph_execute for writes (CREATE, MERGE), graph_query for reads (MATCH).
## Mesh Database
Per-mesh PostgreSQL database. Use mesh_execute for DDL/DML (CREATE TABLE, INSERT), mesh_query for SELECT, mesh_schema to inspect tables. Schema auto-created on first use.
## Streams
Real-time data channels. create_stream to start one, publish to push data, subscribe to receive pushes. Use for build logs, deploy status, live metrics.
## Context
Share your session understanding with peers. Use share_context after exploring a codebase area. Check get_context before re-reading files another peer already analyzed.
## Tasks
Create and claim work items. create_task to propose work, claim_task to take ownership, complete_task when done. Prevents duplicate effort.
## Priority ## Priority
- "now": interrupt immediately, even if recipient is in DND (use for urgent: broken deploy, blocking issue) - "now": interrupt immediately, even if recipient is in DND (use for urgent: broken deploy, blocking issue)
- "next" (default): deliver when recipient goes idle (normal coordination) - "next" (default): deliver when recipient goes idle (normal coordination)
@@ -455,6 +493,213 @@ Call list_peers at session start to understand who is online, their roles, and w
return text(`Deleted: ${id}`); return text(`Deleted: ${id}`);
} }
// --- Vectors ---
case "vector_store": {
const { collection, text: storeText, metadata } = (args ?? {}) as { collection?: string; text?: string; metadata?: Record<string, unknown> };
if (!collection || !storeText) return text("vector_store: `collection` and `text` required", true);
const client = allClients()[0];
if (!client) return text("vector_store: not connected", true);
const id = await client.vectorStore(collection, storeText, metadata);
return text(`Stored in ${collection}${id ? ` (${id})` : ""}`);
}
case "vector_search": {
const { collection, query, limit } = (args ?? {}) as { collection?: string; query?: string; limit?: number };
if (!collection || !query) return text("vector_search: `collection` and `query` required", true);
const client = allClients()[0];
if (!client) return text("vector_search: not connected", true);
const results = await client.vectorSearch(collection, query, limit);
if (results.length === 0) return text(`No results in ${collection} for "${query}".`);
const lines = results.map(r => `- [${r.id.slice(0, 8)}…] (score: ${r.score.toFixed(3)}) ${r.text.slice(0, 120)}${r.text.length > 120 ? "…" : ""}`);
return text(`${results.length} result(s) in ${collection}:\n${lines.join("\n")}`);
}
case "vector_delete": {
const { collection, id } = (args ?? {}) as { collection?: string; id?: string };
if (!collection || !id) return text("vector_delete: `collection` and `id` required", true);
const client = allClients()[0];
if (!client) return text("vector_delete: not connected", true);
await client.vectorDelete(collection, id);
return text(`Deleted ${id} from ${collection}`);
}
case "list_collections": {
const client = allClients()[0];
if (!client) return text("list_collections: not connected", true);
const collections = await client.listCollections();
if (collections.length === 0) return text("No vector collections.");
return text(`Collections:\n${collections.map(c => `- ${c}`).join("\n")}`);
}
// --- Graph ---
case "graph_query": {
const { cypher } = (args ?? {}) as { cypher?: string };
if (!cypher) return text("graph_query: `cypher` required", true);
const client = allClients()[0];
if (!client) return text("graph_query: not connected", true);
const rows = await client.graphQuery(cypher);
if (rows.length === 0) return text("No results.");
return text(JSON.stringify(rows, null, 2));
}
case "graph_execute": {
const { cypher } = (args ?? {}) as { cypher?: string };
if (!cypher) return text("graph_execute: `cypher` required", true);
const client = allClients()[0];
if (!client) return text("graph_execute: not connected", true);
const rows = await client.graphExecute(cypher);
return text(rows.length > 0 ? JSON.stringify(rows, null, 2) : "Executed successfully.");
}
// --- Context ---
case "share_context": {
const { summary, files_read, key_findings, tags } = (args ?? {}) as { summary?: string; files_read?: string[]; key_findings?: string[]; tags?: string[] };
if (!summary) return text("share_context: `summary` required", true);
const client = allClients()[0];
if (!client) return text("share_context: not connected", true);
await client.shareContext(summary, files_read, key_findings, tags);
return text(`Context shared: "${summary.slice(0, 80)}${summary.length > 80 ? "…" : ""}"`);
}
case "get_context": {
const { query } = (args ?? {}) as { query?: string };
if (!query) return text("get_context: `query` required", true);
const client = allClients()[0];
if (!client) return text("get_context: not connected", true);
const contexts = await client.getContext(query);
if (contexts.length === 0) return text(`No context found for "${query}".`);
const lines = contexts.map(c => {
const files = c.filesRead.length ? `\n Files: ${c.filesRead.join(", ")}` : "";
const findings = c.keyFindings.length ? `\n Findings: ${c.keyFindings.join("; ")}` : "";
return `- **${c.peerName}** (${c.updatedAt}): ${c.summary}${files}${findings}`;
});
return text(`${contexts.length} context(s):\n${lines.join("\n")}`);
}
case "list_contexts": {
const client = allClients()[0];
if (!client) return text("list_contexts: not connected", true);
const contexts = await client.listContexts();
if (contexts.length === 0) return text("No peer contexts shared yet.");
const lines = contexts.map(c => `- **${c.peerName}**: ${c.summary}${c.tags.length ? ` [${c.tags.join(", ")}]` : ""}`);
return text(`Peer contexts:\n${lines.join("\n")}`);
}
// --- Tasks ---
case "create_task": {
const { title, assignee, priority, tags } = (args ?? {}) as { title?: string; assignee?: string; priority?: string; tags?: string[] };
if (!title) return text("create_task: `title` required", true);
const client = allClients()[0];
if (!client) return text("create_task: not connected", true);
const id = await client.createTask(title, assignee, priority, tags);
return text(`Task created${id ? ` (${id})` : ""}: "${title}"${assignee ? `${assignee}` : ""}`);
}
case "claim_task": {
const { id } = (args ?? {}) as { id?: string };
if (!id) return text("claim_task: `id` required", true);
const client = allClients()[0];
if (!client) return text("claim_task: not connected", true);
await client.claimTask(id);
return text(`Claimed task: ${id}`);
}
case "complete_task": {
const { id, result } = (args ?? {}) as { id?: string; result?: string };
if (!id) return text("complete_task: `id` required", true);
const client = allClients()[0];
if (!client) return text("complete_task: not connected", true);
await client.completeTask(id, result);
return text(`Completed task: ${id}${result ? `${result}` : ""}`);
}
case "list_tasks": {
const { status, assignee } = (args ?? {}) as { status?: string; assignee?: string };
const client = allClients()[0];
if (!client) return text("list_tasks: not connected", true);
const tasks = await client.listTasks(status, assignee);
if (tasks.length === 0) return text("No tasks found.");
const lines = tasks.map(t => `- [${t.id.slice(0, 8)}…] **${t.title}** (${t.status}, ${t.priority}) ${t.assignee ? `${t.assignee}` : "unassigned"} (by ${t.createdBy})`);
return text(`${tasks.length} task(s):\n${lines.join("\n")}`);
}
// --- Mesh Database ---
case "mesh_query": {
const { sql: querySql } = (args ?? {}) as { sql?: string };
if (!querySql) return text("mesh_query: `sql` required", true);
const client = allClients()[0];
if (!client) return text("mesh_query: not connected", true);
const result = await client.meshQuery(querySql);
if (!result) return text("mesh_query: query failed or timed out", true);
if (result.rows.length === 0) return text(`Query returned 0 rows.`);
const header = `| ${result.columns.join(" | ")} |`;
const sep = `| ${result.columns.map(() => "---").join(" | ")} |`;
const rows = result.rows.map(r => `| ${result.columns.map(c => String(r[c] ?? "")).join(" | ")} |`);
return text(`${result.rowCount} row(s):\n${header}\n${sep}\n${rows.join("\n")}`);
}
case "mesh_execute": {
const { sql: execSql } = (args ?? {}) as { sql?: string };
if (!execSql) return text("mesh_execute: `sql` required", true);
const client = allClients()[0];
if (!client) return text("mesh_execute: not connected", true);
await client.meshExecute(execSql);
return text(`Executed.`);
}
case "mesh_schema": {
const client = allClients()[0];
if (!client) return text("mesh_schema: not connected", true);
const tables = await client.meshSchema();
if (!tables || tables.length === 0) return text("No tables in mesh database.");
const lines = tables.map(t => `**${t.name}**: ${t.columns.map(c => `${c.name} (${c.type}${c.nullable ? ", nullable" : ""})`).join(", ")}`);
return text(lines.join("\n"));
}
// --- Streams ---
case "create_stream": {
const { name: streamName } = (args ?? {}) as { name?: string };
if (!streamName) return text("create_stream: `name` required", true);
const client = allClients()[0];
if (!client) return text("create_stream: not connected", true);
const streamId = await client.createStream(streamName);
return text(`Stream created: ${streamName}${streamId ? ` (${streamId})` : ""}`);
}
case "publish": {
const { stream: pubStream, data: pubData } = (args ?? {}) as { stream?: string; data?: unknown };
if (!pubStream) return text("publish: `stream` required", true);
const client = allClients()[0];
if (!client) return text("publish: not connected", true);
await client.publish(pubStream, pubData);
return text(`Published to ${pubStream}.`);
}
case "subscribe": {
const { stream: subStream } = (args ?? {}) as { stream?: string };
if (!subStream) return text("subscribe: `stream` required", true);
const client = allClients()[0];
if (!client) return text("subscribe: not connected", true);
await client.subscribe(subStream);
return text(`Subscribed to ${subStream}. Data pushes will arrive as channel notifications.`);
}
case "list_streams": {
const client = allClients()[0];
if (!client) return text("list_streams: not connected", true);
const streams = await client.listStreams();
if (streams.length === 0) return text("No active streams.");
const lines = streams.map(s => `- **${s.name}** (${s.id.slice(0, 8)}…) by ${s.createdBy}, ${s.subscriberCount} subscriber(s)`);
return text(lines.join("\n"));
}
case "mesh_info": {
const client = allClients()[0];
if (!client) return text("mesh_info: not connected", true);
const info = await client.meshInfo();
if (!info) return text("mesh_info: timed out", true);
const lines = [
`**Mesh**: ${info.mesh}`,
`**Peers**: ${info.peers}`,
`**Groups**: ${(info.groups as string[])?.join(", ") || "none"}`,
`**State keys**: ${(info.stateKeys as string[])?.join(", ") || "none"}`,
`**Memories**: ${info.memoryCount}`,
`**Files**: ${info.fileCount}`,
`**Tasks**: open=${(info.tasks as any)?.open ?? 0}, claimed=${(info.tasks as any)?.claimed ?? 0}, done=${(info.tasks as any)?.done ?? 0}`,
`**Streams**: ${(info.streams as string[])?.join(", ") || "none"}`,
`**Tables**: ${(info.tables as string[])?.join(", ") || "none"}`,
`**Your name**: ${info.yourName}`,
`**Your groups**: ${(info.yourGroups as any[])?.map((g: any) => `@${g.name}${g.role ? ':' + g.role : ''}`).join(", ") || "none"}`,
];
return text(lines.join("\n"));
}
default: default:
return text(`Unknown tool: ${name}`, true); return text(`Unknown tool: ${name}`, true);
} }
@@ -499,6 +744,22 @@ Call list_peers at session start to understand who is online, their roles, and w
} }
}); });
client.onStreamData(async (evt) => {
try {
await server.notification({
method: "notifications/claude/channel",
params: {
content: `[stream:${evt.stream}] from ${evt.publishedBy}: ${JSON.stringify(evt.data)}`,
meta: {
kind: "stream_data",
stream: evt.stream,
published_by: evt.publishedBy,
},
},
});
} catch { /* best effort */ }
});
client.onStateChange(async (change) => { client.onStateChange(async (change) => {
try { try {
await server.notification({ await server.notification({

View File

@@ -269,4 +269,290 @@ export const TOOLS: Tool[] = [
required: ["id"], required: ["id"],
}, },
}, },
// --- Vector tools ---
{
name: "vector_store",
description:
"Store an embedding in a per-mesh Qdrant collection. Auto-creates the collection on first use.",
inputSchema: {
type: "object",
properties: {
collection: { type: "string", description: "Collection name" },
text: { type: "string", description: "Text to embed and store" },
metadata: {
type: "object",
description: "Optional metadata to attach",
},
},
required: ["collection", "text"],
},
},
{
name: "vector_search",
description: "Semantic search over stored embeddings in a collection.",
inputSchema: {
type: "object",
properties: {
collection: { type: "string", description: "Collection name" },
query: { type: "string", description: "Search query text" },
limit: {
type: "number",
description: "Max results (default: 10)",
},
},
required: ["collection", "query"],
},
},
{
name: "vector_delete",
description: "Remove an embedding from a collection.",
inputSchema: {
type: "object",
properties: {
collection: { type: "string", description: "Collection name" },
id: { type: "string", description: "Embedding ID to delete" },
},
required: ["collection", "id"],
},
},
{
name: "list_collections",
description: "List vector collections in this mesh.",
inputSchema: { type: "object", properties: {} },
},
// --- Graph tools ---
{
name: "graph_query",
description:
"Run a read-only Cypher query on the per-mesh Neo4j database.",
inputSchema: {
type: "object",
properties: {
cypher: { type: "string", description: "Cypher MATCH query" },
},
required: ["cypher"],
},
},
{
name: "graph_execute",
description:
"Run a write Cypher query (CREATE, MERGE, DELETE) on the per-mesh Neo4j database.",
inputSchema: {
type: "object",
properties: {
cypher: { type: "string", description: "Cypher write query" },
},
required: ["cypher"],
},
},
// --- Mesh Database tools ---
{
name: "mesh_query",
description:
"Run a SELECT query on the per-mesh shared database.",
inputSchema: {
type: "object",
properties: {
sql: { type: "string", description: "SQL SELECT query" },
},
required: ["sql"],
},
},
{
name: "mesh_execute",
description:
"Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE).",
inputSchema: {
type: "object",
properties: {
sql: { type: "string", description: "SQL statement" },
},
required: ["sql"],
},
},
{
name: "mesh_schema",
description:
"List tables and columns in the per-mesh shared database.",
inputSchema: { type: "object", properties: {} },
},
// --- Stream tools ---
{
name: "create_stream",
description:
"Create a real-time data stream in the mesh.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Stream name" },
},
required: ["name"],
},
},
{
name: "publish",
description:
"Push data to a stream. Subscribers receive it in real-time.",
inputSchema: {
type: "object",
properties: {
stream: { type: "string", description: "Stream name" },
data: { description: "Any JSON data to publish" },
},
required: ["stream", "data"],
},
},
{
name: "subscribe",
description:
"Subscribe to a stream. Data pushes arrive as channel notifications.",
inputSchema: {
type: "object",
properties: {
stream: { type: "string", description: "Stream name" },
},
required: ["stream"],
},
},
{
name: "list_streams",
description:
"List active streams in the mesh.",
inputSchema: { type: "object", properties: {} },
},
// --- Context tools ---
{
name: "share_context",
description:
"Share your session understanding with the mesh. Call after exploring a codebase area.",
inputSchema: {
type: "object",
properties: {
summary: {
type: "string",
description: "Summary of what you explored/learned",
},
files_read: {
type: "array",
items: { type: "string" },
description: "File paths you read",
},
key_findings: {
type: "array",
items: { type: "string" },
description: "Key findings or insights",
},
tags: {
type: "array",
items: { type: "string" },
description: "Tags for categorization",
},
},
required: ["summary"],
},
},
{
name: "get_context",
description:
"Find context from peers who explored an area. Check before re-reading files another peer already analyzed.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (file path, topic, etc.)",
},
},
required: ["query"],
},
},
{
name: "list_contexts",
description: "See what all peers currently know about the codebase.",
inputSchema: { type: "object", properties: {} },
},
// --- Task tools ---
{
name: "create_task",
description: "Create a work item for the mesh.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Task title" },
assignee: {
type: "string",
description: "Peer name to assign (optional)",
},
priority: {
type: "string",
enum: ["low", "normal", "high", "urgent"],
description: "Priority level (default: normal)",
},
tags: {
type: "array",
items: { type: "string" },
description: "Tags for categorization",
},
},
required: ["title"],
},
},
{
name: "claim_task",
description: "Claim an unclaimed task to take ownership.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Task ID" },
},
required: ["id"],
},
},
{
name: "complete_task",
description: "Mark a task as done with an optional result summary.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Task ID" },
result: {
type: "string",
description: "Summary of what was done",
},
},
required: ["id"],
},
},
{
name: "list_tasks",
description: "List tasks filtered by status and/or assignee.",
inputSchema: {
type: "object",
properties: {
status: {
type: "string",
enum: ["open", "claimed", "completed"],
description: "Filter by status",
},
assignee: {
type: "string",
description: "Filter by assignee name",
},
},
},
},
// --- Mesh info ---
{
name: "mesh_info",
description:
"Get a complete overview of the mesh: peers, groups, state, memory, files, tasks, streams, tables. Call on session start for full situational awareness.",
inputSchema: { type: "object", properties: {} },
},
]; ];

View File

@@ -415,6 +415,19 @@ export class BrokerClient {
private fileUrlResolvers: Array<(result: { url: string; name: string } | null) => void> = []; private fileUrlResolvers: Array<(result: { url: string; name: string } | null) => void> = [];
private fileListResolvers: Array<(files: Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) => void> = []; private fileListResolvers: Array<(files: Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) => void> = [];
private fileStatusResolvers: Array<(accesses: Array<{ peerName: string; accessedAt: string }>) => void> = []; private fileStatusResolvers: Array<(accesses: Array<{ peerName: string; accessedAt: string }>) => void> = [];
private vectorStoredResolvers: Array<(id: string | null) => void> = [];
private vectorResultsResolvers: Array<(results: Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>) => void> = [];
private collectionListResolvers: Array<(collections: string[]) => void> = [];
private graphResultResolvers: Array<(rows: Array<Record<string, unknown>>) => void> = [];
private contextListResolvers: Array<(contexts: Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>) => void> = [];
private contextResultsResolvers: Array<(contexts: Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>) => void> = [];
private taskCreatedResolvers: Array<(id: string | null) => void> = [];
private taskListResolvers: Array<(tasks: Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>) => void> = [];
private meshQueryResolvers: Array<(result: { columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number } | null) => void> = [];
private meshSchemaResolvers: Array<(tables: Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>) => void> = [];
private streamCreatedResolvers: Array<(id: string | null) => void> = [];
private streamListResolvers: Array<(streams: Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>) => void> = [];
private streamDataHandlers = new Set<(data: { stream: string; data: unknown; publishedBy: string }) => void>();
async messageStatus(messageId: string): Promise<{ messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null> { async messageStatus(messageId: string): Promise<{ messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null; if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
@@ -517,12 +530,262 @@ export class BrokerClient {
return body.fileId ?? null; return body.fileId ?? null;
} }
// --- Vectors ---
/** Store an embedding in a per-mesh Qdrant collection. */
async vectorStore(collection: string, text: string, metadata?: Record<string, unknown>): Promise<string | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
return new Promise((resolve) => {
this.vectorStoredResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "vector_store", collection, text, metadata }));
setTimeout(() => {
const idx = this.vectorStoredResolvers.indexOf(resolve);
if (idx !== -1) { this.vectorStoredResolvers.splice(idx, 1); resolve(null); }
}, 5_000);
});
}
/** Semantic search over stored embeddings. */
async vectorSearch(collection: string, query: string, limit?: number): Promise<Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.vectorResultsResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "vector_search", collection, query, limit }));
setTimeout(() => {
const idx = this.vectorResultsResolvers.indexOf(resolve);
if (idx !== -1) { this.vectorResultsResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
/** Remove an embedding from a collection. */
async vectorDelete(collection: string, id: string): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "vector_delete", collection, id }));
}
/** List vector collections in this mesh. */
async listCollections(): Promise<string[]> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.collectionListResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "list_collections" }));
setTimeout(() => {
const idx = this.collectionListResolvers.indexOf(resolve);
if (idx !== -1) { this.collectionListResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
// --- Graph ---
/** Run a read query on the per-mesh Neo4j database. */
async graphQuery(cypher: string): Promise<Array<Record<string, unknown>>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.graphResultResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "graph_query", cypher }));
setTimeout(() => {
const idx = this.graphResultResolvers.indexOf(resolve);
if (idx !== -1) { this.graphResultResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
/** Run a write query (CREATE, MERGE, DELETE) on the per-mesh Neo4j database. */
async graphExecute(cypher: string): Promise<Array<Record<string, unknown>>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.graphResultResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "graph_execute", cypher }));
setTimeout(() => {
const idx = this.graphResultResolvers.indexOf(resolve);
if (idx !== -1) { this.graphResultResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
// --- Context ---
/** Share session understanding with the mesh. */
async shareContext(summary: string, filesRead?: string[], keyFindings?: string[], tags?: string[]): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "share_context", summary, filesRead, keyFindings, tags }));
}
/** Find context from peers who explored an area. */
async getContext(query: string): Promise<Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.contextResultsResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "get_context", query }));
setTimeout(() => {
const idx = this.contextResultsResolvers.indexOf(resolve);
if (idx !== -1) { this.contextResultsResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
/** See what all peers currently know. */
async listContexts(): Promise<Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.contextListResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "list_contexts" }));
setTimeout(() => {
const idx = this.contextListResolvers.indexOf(resolve);
if (idx !== -1) { this.contextListResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
// --- Tasks ---
/** Create a work item. */
async createTask(title: string, assignee?: string, priority?: string, tags?: string[]): Promise<string | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
return new Promise((resolve) => {
this.taskCreatedResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "create_task", title, assignee, priority, tags }));
setTimeout(() => {
const idx = this.taskCreatedResolvers.indexOf(resolve);
if (idx !== -1) { this.taskCreatedResolvers.splice(idx, 1); resolve(null); }
}, 5_000);
});
}
/** Claim an unclaimed task. */
async claimTask(id: string): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "claim_task", id }));
}
/** Mark a task done with optional result. */
async completeTask(id: string, result?: string): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "complete_task", id, result }));
}
/** List tasks filtered by status/assignee. */
async listTasks(status?: string, assignee?: string): Promise<Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.taskListResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "list_tasks", status, assignee }));
setTimeout(() => {
const idx = this.taskListResolvers.indexOf(resolve);
if (idx !== -1) { this.taskListResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
// --- Mesh Database ---
/** Run a SELECT query on the per-mesh shared database. */
async meshQuery(sql: string): Promise<{ columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number } | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
return new Promise((resolve) => {
this.meshQueryResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "mesh_query", sql }));
setTimeout(() => {
const idx = this.meshQueryResolvers.indexOf(resolve);
if (idx !== -1) { this.meshQueryResolvers.splice(idx, 1); resolve(null); }
}, 5_000);
});
}
/** Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE). */
async meshExecute(sql: string): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "mesh_execute", sql }));
}
/** List tables and columns in the per-mesh shared database. */
async meshSchema(): Promise<Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.meshSchemaResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "mesh_schema" }));
setTimeout(() => {
const idx = this.meshSchemaResolvers.indexOf(resolve);
if (idx !== -1) { this.meshSchemaResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
// --- Streams ---
/** Create a real-time data stream in the mesh. */
async createStream(name: string): Promise<string | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
return new Promise((resolve) => {
this.streamCreatedResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "create_stream", name }));
setTimeout(() => {
const idx = this.streamCreatedResolvers.indexOf(resolve);
if (idx !== -1) { this.streamCreatedResolvers.splice(idx, 1); resolve(null); }
}, 5_000);
});
}
/** Push data to a stream. Subscribers receive it in real-time. */
async publish(stream: string, data: unknown): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "publish", stream, data }));
}
/** Subscribe to a stream. Data pushes arrive via onStreamData handler. */
async subscribe(stream: string): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "subscribe", stream }));
}
/** Unsubscribe from a stream. */
async unsubscribe(stream: string): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "unsubscribe", stream }));
}
/** List active streams in the mesh. */
async listStreams(): Promise<Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.streamListResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "list_streams" }));
setTimeout(() => {
const idx = this.streamListResolvers.indexOf(resolve);
if (idx !== -1) { this.streamListResolvers.splice(idx, 1); resolve([]); }
}, 5_000);
});
}
/** Subscribe to stream data pushes. Returns an unsubscribe function. */
onStreamData(handler: (data: { stream: string; data: unknown; publishedBy: string }) => void): () => void {
this.streamDataHandlers.add(handler);
return () => this.streamDataHandlers.delete(handler);
}
/** Subscribe to state change notifications. Returns an unsubscribe function. */ /** Subscribe to state change notifications. Returns an unsubscribe function. */
onStateChange(handler: (change: { key: string; value: unknown; updatedBy: string }) => void): () => void { onStateChange(handler: (change: { key: string; value: unknown; updatedBy: string }) => void): () => void {
this.stateChangeHandlers.add(handler); this.stateChangeHandlers.add(handler);
return () => this.stateChangeHandlers.delete(handler); return () => this.stateChangeHandlers.delete(handler);
} }
// --- Mesh info ---
private meshInfoResolvers: Array<(result: Record<string, unknown> | null) => void> = [];
async meshInfo(): Promise<Record<string, unknown> | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
return new Promise((resolve) => {
this.meshInfoResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "mesh_info" }));
setTimeout(() => {
const idx = this.meshInfoResolvers.indexOf(resolve);
if (idx !== -1) { this.meshInfoResolvers.splice(idx, 1); resolve(null); }
}, 5_000);
});
}
close(): void { close(): void {
this.closed = true; this.closed = true;
if (this.helloTimer) clearTimeout(this.helloTimer); if (this.helloTimer) clearTimeout(this.helloTimer);
@@ -698,6 +961,100 @@ export class BrokerClient {
if (resolver) resolver(accesses); if (resolver) resolver(accesses);
return; return;
} }
if (msg.type === "vector_stored") {
const resolver = this.vectorStoredResolvers.shift();
if (resolver) resolver(msg.id ? String(msg.id) : null);
return;
}
if (msg.type === "vector_results") {
const results = (msg.results as Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>) ?? [];
const resolver = this.vectorResultsResolvers.shift();
if (resolver) resolver(results);
return;
}
if (msg.type === "collection_list") {
const collections = (msg.collections as string[]) ?? [];
const resolver = this.collectionListResolvers.shift();
if (resolver) resolver(collections);
return;
}
if (msg.type === "graph_result") {
const rows = (msg.rows as Array<Record<string, unknown>>) ?? [];
const resolver = this.graphResultResolvers.shift();
if (resolver) resolver(rows);
return;
}
if (msg.type === "context_list") {
const contexts = (msg.contexts as Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>) ?? [];
const resolver = this.contextListResolvers.shift();
if (resolver) resolver(contexts);
return;
}
if (msg.type === "context_results") {
const contexts = (msg.contexts as Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>) ?? [];
const resolver = this.contextResultsResolvers.shift();
if (resolver) resolver(contexts);
return;
}
if (msg.type === "task_created") {
const resolver = this.taskCreatedResolvers.shift();
if (resolver) resolver(msg.id ? String(msg.id) : null);
return;
}
if (msg.type === "task_list") {
const tasks = (msg.tasks as Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>) ?? [];
const resolver = this.taskListResolvers.shift();
if (resolver) resolver(tasks);
return;
}
if (msg.type === "mesh_query_result") {
const resolver = this.meshQueryResolvers.shift();
if (resolver) {
if (msg.columns) {
resolver({
columns: (msg.columns as string[]) ?? [],
rows: (msg.rows as Array<Record<string, unknown>>) ?? [],
rowCount: (msg.rowCount as number) ?? 0,
});
} else {
resolver(null);
}
}
return;
}
if (msg.type === "mesh_schema_result") {
const tables = (msg.tables as Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>) ?? [];
const resolver = this.meshSchemaResolvers.shift();
if (resolver) resolver(tables);
return;
}
if (msg.type === "stream_created") {
const resolver = this.streamCreatedResolvers.shift();
if (resolver) resolver(msg.id ? String(msg.id) : null);
return;
}
if (msg.type === "stream_list") {
const streams = (msg.streams as Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>) ?? [];
const resolver = this.streamListResolvers.shift();
if (resolver) resolver(streams);
return;
}
if (msg.type === "stream_data") {
const evt = {
stream: String(msg.stream ?? ""),
data: msg.data,
publishedBy: String(msg.publishedBy ?? ""),
};
for (const h of this.streamDataHandlers) {
try { h(evt); } catch { /* handler errors are not the transport's problem */ }
}
return;
}
if (msg.type === "mesh_info_result") {
const resolver = this.meshInfoResolvers.shift();
if (resolver) resolver(msg as Record<string, unknown>);
return;
}
if (msg.type === "error") { if (msg.type === "error") {
this.debug(`broker error: ${msg.code} ${msg.message}`); this.debug(`broker error: ${msg.code} ${msg.message}`);
const id = msg.id ? String(msg.id) : null; const id = msg.id ? String(msg.id) : null;

View File

@@ -1,5 +1,8 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { withPayload } = require("@payloadcms/next/withPayload");
import env from "./env.config"; import env from "./env.config";
const INTERNAL_PACKAGES = [ const INTERNAL_PACKAGES = [
@@ -130,4 +133,4 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: env.ANALYZE, enabled: env.ANALYZE,
}); });
export default withBundleAnalyzer(config); export default withPayload(withBundleAnalyzer(config));

View File

@@ -0,0 +1,14 @@
import "@payloadcms/next/css";
import type { ReactNode } from "react";
export const metadata = {
title: "CMS — claudemesh",
};
export default function PayloadLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,14 @@
/* eslint-disable */
// @ts-nocheck — Payload generates these types at build time
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap";
import config from "@payload-config";
type Args = { params: Promise<{ segments: string[] }> };
export const generateMetadata = ({ params }: Args) =>
generatePageMetadata({ config, params });
export default function Page({ params }: Args) {
return <RootPage config={config} params={params} importMap={importMap} />;
}

View File

@@ -0,0 +1,51 @@
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
}

View File

@@ -48,6 +48,41 @@ services:
start_period: 10s start_period: 10s
retries: 3 retries: 3
qdrant:
image: qdrant/qdrant
restart: always
volumes:
- qdrant-data:/qdrant/storage
expose:
- "6333"
networks:
- claudemesh-internal
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:6333/readyz"]
interval: 15s
timeout: 5s
retries: 3
neo4j:
image: neo4j:5
restart: always
environment:
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-changeme}
NEO4J_PLUGINS: '[]'
volumes:
- neo4j-data:/data
expose:
- "7687"
- "7474"
networks:
- claudemesh-internal
healthcheck:
test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "${NEO4J_PASSWORD:-changeme}", "RETURN 1"]
interval: 15s
timeout: 5s
start_period: 30s
retries: 3
broker: broker:
image: ${BROKER_IMAGE:-claudemesh-broker:latest} image: ${BROKER_IMAGE:-claudemesh-broker:latest}
restart: always restart: always
@@ -64,6 +99,10 @@ services:
MINIO_ACCESS_KEY: claudemesh MINIO_ACCESS_KEY: claudemesh
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-changeme} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-changeme}
MINIO_USE_SSL: "false" MINIO_USE_SSL: "false"
QDRANT_URL: http://qdrant:6333
NEO4J_URL: bolt://neo4j:7687
NEO4J_USER: neo4j
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-changeme}
expose: expose:
- "7900" - "7900"
networks: networks:
@@ -72,6 +111,10 @@ services:
depends_on: depends_on:
minio: minio:
condition: service_healthy condition: service_healthy
qdrant:
condition: service_healthy
neo4j:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "bun", "-e", "fetch('http://localhost:7900/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"] test: ["CMD", "bun", "-e", "fetch('http://localhost:7900/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
interval: 15s interval: 15s
@@ -114,6 +157,8 @@ services:
volumes: volumes:
minio-data: minio-data:
qdrant-data:
neo4j-data:
networks: networks:
# Coolify's shared Traefik network — must already exist on the host # Coolify's shared Traefik network — must already exist on the host

View File

@@ -0,0 +1,33 @@
CREATE TABLE "mesh"."context" (
"id" text PRIMARY KEY NOT NULL,
"mesh_id" text NOT NULL,
"presence_id" text,
"peer_name" text,
"summary" text NOT NULL,
"files_read" text[] DEFAULT '{}',
"key_findings" text[] DEFAULT '{}',
"tags" text[] DEFAULT '{}',
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "mesh"."task" (
"id" text PRIMARY KEY NOT NULL,
"mesh_id" text NOT NULL,
"title" text NOT NULL,
"assignee" text,
"claimed_by_name" text,
"claimed_by_presence" text,
"priority" text DEFAULT 'normal' NOT NULL,
"status" text DEFAULT 'open' NOT NULL,
"tags" text[] DEFAULT '{}',
"result" text,
"created_by_name" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"claimed_at" timestamp,
"completed_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "mesh"."context" ADD CONSTRAINT "context_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "mesh"."context" ADD CONSTRAINT "context_presence_id_presence_id_fk" FOREIGN KEY ("presence_id") REFERENCES "mesh"."presence"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mesh"."task" ADD CONSTRAINT "task_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "mesh"."task" ADD CONSTRAINT "task_claimed_by_presence_presence_id_fk" FOREIGN KEY ("claimed_by_presence") REFERENCES "mesh"."presence"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -0,0 +1,10 @@
CREATE TABLE "mesh"."stream" (
"id" text PRIMARY KEY NOT NULL,
"mesh_id" text NOT NULL,
"name" text NOT NULL,
"created_by_name" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "mesh"."stream" ADD CONSTRAINT "stream_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
CREATE UNIQUE INDEX "stream_mesh_name_idx" ON "mesh"."stream" USING btree ("mesh_id","name");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,20 @@
"when": 1775480008546, "when": 1775480008546,
"tag": "0009_add-file-tables", "tag": "0009_add-file-tables",
"breakpoints": true "breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1775480729014,
"tag": "0010_add-context-and-tasks",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1775481222701,
"tag": "0011_add-streams",
"breakpoints": true
} }
] ]
} }

View File

@@ -327,6 +327,68 @@ export const meshFileAccess = meshSchema.table("file_access", {
accessedAt: timestamp().defaultNow().notNull(), accessedAt: timestamp().defaultNow().notNull(),
}); });
/**
* Per-peer context snapshot. Each peer (presence) has at most one context
* entry per mesh, upserted on each share_context call. Allows peers to
* discover what others are working on, which files they've read, and
* key findings — without sending a direct message.
*/
export const meshContext = meshSchema.table("context", {
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
presenceId: text().references(() => presence.id, { onDelete: "cascade" }),
peerName: text(),
summary: text().notNull(),
filesRead: text().array().default([]),
keyFindings: text().array().default([]),
tags: text().array().default([]),
updatedAt: timestamp().defaultNow().notNull(),
});
/**
* Mesh-scoped task board. Peers can create tasks, claim them, and mark
* them done. Lightweight project management for multi-agent workflows.
*/
export const meshTask = meshSchema.table("task", {
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
title: text().notNull(),
assignee: text(),
claimedByName: text(),
claimedByPresence: text().references(() => presence.id),
priority: text().notNull().default("normal"),
status: text().notNull().default("open"),
tags: text().array().default([]),
result: text(),
createdByName: text(),
createdAt: timestamp().defaultNow().notNull(),
claimedAt: timestamp(),
completedAt: timestamp(),
});
/**
* Named real-time data channels within a mesh. One peer publishes, all
* subscribers receive. No message history — streams are live.
* Use cases: build logs, deploy status, monitoring data, live code diffs.
*/
export const meshStream = meshSchema.table(
"stream",
{
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
name: text().notNull(),
createdByName: text(),
createdAt: timestamp().defaultNow().notNull(),
},
(table) => [uniqueIndex("stream_mesh_name_idx").on(table.meshId, table.name)],
);
export const meshRelations = relations(mesh, ({ one, many }) => ({ export const meshRelations = relations(mesh, ({ one, many }) => ({
owner: one(user, { owner: one(user, {
fields: [mesh.ownerUserId], fields: [mesh.ownerUserId],
@@ -469,3 +531,45 @@ export type SelectMeshFile = typeof meshFile.$inferSelect;
export type InsertMeshFile = typeof meshFile.$inferInsert; export type InsertMeshFile = typeof meshFile.$inferInsert;
export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect; export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect;
export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert; export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert;
export const selectMeshContextSchema = createSelectSchema(meshContext);
export const insertMeshContextSchema = createInsertSchema(meshContext);
export const selectMeshTaskSchema = createSelectSchema(meshTask);
export const insertMeshTaskSchema = createInsertSchema(meshTask);
export type SelectMeshContext = typeof meshContext.$inferSelect;
export type InsertMeshContext = typeof meshContext.$inferInsert;
export type SelectMeshTask = typeof meshTask.$inferSelect;
export type InsertMeshTask = typeof meshTask.$inferInsert;
export const meshContextRelations = relations(meshContext, ({ one }) => ({
mesh: one(mesh, {
fields: [meshContext.meshId],
references: [mesh.id],
}),
presence: one(presence, {
fields: [meshContext.presenceId],
references: [presence.id],
}),
}));
export const meshTaskRelations = relations(meshTask, ({ one }) => ({
mesh: one(mesh, {
fields: [meshTask.meshId],
references: [mesh.id],
}),
claimedPresence: one(presence, {
fields: [meshTask.claimedByPresence],
references: [presence.id],
}),
}));
export const meshStreamRelations = relations(meshStream, ({ one }) => ({
mesh: one(mesh, {
fields: [meshStream.meshId],
references: [mesh.id],
}),
}));
export const selectMeshStreamSchema = createSelectSchema(meshStream);
export const insertMeshStreamSchema = createInsertSchema(meshStream);
export type SelectMeshStream = typeof meshStream.$inferSelect;
export type InsertMeshStream = typeof meshStream.$inferInsert;