Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82cfee315c | ||
|
|
ab08be04a5 | ||
|
|
ee585a8370 | ||
|
|
1f078bf0c8 | ||
|
|
2372032a68 | ||
|
|
a70c5fd124 | ||
|
|
5c62d287cf | ||
|
|
9ae378c2e3 | ||
|
|
7381738f0b | ||
|
|
8c6b0c0e07 | ||
|
|
ec9626503c | ||
|
|
820ec085b2 | ||
|
|
9e6f6d7bc9 | ||
|
|
7194e7d28e | ||
|
|
0b4e389f2b | ||
|
|
7a5f786e0c | ||
|
|
10e5fdcfd1 | ||
|
|
cc6e56aef9 | ||
|
|
1aaa483d60 | ||
|
|
99d9d19079 | ||
|
|
888078876a |
@@ -15,10 +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",
|
||||||
|
"neo4j-driver": "6.0.1",
|
||||||
"ws": "8.20.0",
|
"ws": "8.20.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ import { db } from "./db";
|
|||||||
import {
|
import {
|
||||||
invite as inviteTable,
|
invite as inviteTable,
|
||||||
mesh,
|
mesh,
|
||||||
|
meshFile,
|
||||||
|
meshFileAccess,
|
||||||
|
meshContext,
|
||||||
meshMember as memberTable,
|
meshMember as memberTable,
|
||||||
|
meshMemory,
|
||||||
|
meshState,
|
||||||
|
meshStream,
|
||||||
|
meshTask,
|
||||||
messageQueue,
|
messageQueue,
|
||||||
pendingStatus,
|
pendingStatus,
|
||||||
presence,
|
presence,
|
||||||
@@ -489,6 +496,730 @@ export async function leaveGroup(
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Shared state ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert a key-value pair in the mesh's shared state.
|
||||||
|
* Returns the upserted row.
|
||||||
|
*/
|
||||||
|
export async function setState(
|
||||||
|
meshId: string,
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
presenceId?: string,
|
||||||
|
presenceName?: string,
|
||||||
|
): Promise<{
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
updatedBy: string;
|
||||||
|
updatedAt: Date;
|
||||||
|
}> {
|
||||||
|
const now = new Date();
|
||||||
|
const [row] = await db
|
||||||
|
.insert(meshState)
|
||||||
|
.values({
|
||||||
|
meshId,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
updatedByPresence: presenceId ?? null,
|
||||||
|
updatedByName: presenceName ?? null,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [meshState.meshId, meshState.key],
|
||||||
|
set: {
|
||||||
|
value,
|
||||||
|
updatedByPresence: presenceId ?? null,
|
||||||
|
updatedByName: presenceName ?? null,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.returning({
|
||||||
|
key: meshState.key,
|
||||||
|
value: meshState.value,
|
||||||
|
updatedByName: meshState.updatedByName,
|
||||||
|
updatedAt: meshState.updatedAt,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
key: row!.key,
|
||||||
|
value: row!.value,
|
||||||
|
updatedBy: row!.updatedByName ?? "unknown",
|
||||||
|
updatedAt: row!.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a single state key for a mesh. Returns null if not found.
|
||||||
|
*/
|
||||||
|
export async function getState(
|
||||||
|
meshId: string,
|
||||||
|
key: string,
|
||||||
|
): Promise<{
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
updatedBy: string;
|
||||||
|
updatedAt: Date;
|
||||||
|
} | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select({
|
||||||
|
key: meshState.key,
|
||||||
|
value: meshState.value,
|
||||||
|
updatedByName: meshState.updatedByName,
|
||||||
|
updatedAt: meshState.updatedAt,
|
||||||
|
})
|
||||||
|
.from(meshState)
|
||||||
|
.where(and(eq(meshState.meshId, meshId), eq(meshState.key, key)))
|
||||||
|
.limit(1);
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
key: row.key,
|
||||||
|
value: row.value,
|
||||||
|
updatedBy: row.updatedByName ?? "unknown",
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all state entries for a mesh.
|
||||||
|
*/
|
||||||
|
export async function listState(
|
||||||
|
meshId: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{ key: string; value: unknown; updatedBy: string; updatedAt: Date }>
|
||||||
|
> {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
key: meshState.key,
|
||||||
|
value: meshState.value,
|
||||||
|
updatedByName: meshState.updatedByName,
|
||||||
|
updatedAt: meshState.updatedAt,
|
||||||
|
})
|
||||||
|
.from(meshState)
|
||||||
|
.where(eq(meshState.meshId, meshId))
|
||||||
|
.orderBy(asc(meshState.key));
|
||||||
|
return rows.map((r) => ({
|
||||||
|
key: r.key,
|
||||||
|
value: r.value,
|
||||||
|
updatedBy: r.updatedByName ?? "unknown",
|
||||||
|
updatedAt: r.updatedAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Memory ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new memory for a mesh. Returns the generated id.
|
||||||
|
*/
|
||||||
|
export async function rememberMemory(
|
||||||
|
meshId: string,
|
||||||
|
content: string,
|
||||||
|
tags: string[],
|
||||||
|
memberId?: string,
|
||||||
|
memberName?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const [row] = await db
|
||||||
|
.insert(meshMemory)
|
||||||
|
.values({
|
||||||
|
meshId,
|
||||||
|
content,
|
||||||
|
tags,
|
||||||
|
rememberedBy: memberId ?? null,
|
||||||
|
rememberedByName: memberName ?? null,
|
||||||
|
})
|
||||||
|
.returning({ id: meshMemory.id });
|
||||||
|
if (!row) throw new Error("failed to insert memory");
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-text search memories in a mesh. Uses the search_vector tsvector
|
||||||
|
* column with plainto_tsquery for ranked results.
|
||||||
|
*/
|
||||||
|
export async function recallMemory(
|
||||||
|
meshId: string,
|
||||||
|
query: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
rememberedBy: string;
|
||||||
|
rememberedAt: Date;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const result = await db.execute<{
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
remembered_by_name: string | null;
|
||||||
|
remembered_at: string | Date;
|
||||||
|
}>(sql`
|
||||||
|
SELECT id, content, tags, remembered_by_name, remembered_at
|
||||||
|
FROM mesh.memory
|
||||||
|
WHERE mesh_id = ${meshId}
|
||||||
|
AND forgotten_at IS NULL
|
||||||
|
AND search_vector @@ plainto_tsquery('english', ${query})
|
||||||
|
ORDER BY ts_rank(search_vector, plainto_tsquery('english', ${query})) DESC
|
||||||
|
LIMIT 20
|
||||||
|
`);
|
||||||
|
const rows = (result.rows ?? result) as Array<{
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
remembered_by_name: string | null;
|
||||||
|
remembered_at: string | Date;
|
||||||
|
}>;
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
content: r.content,
|
||||||
|
tags: r.tags ?? [],
|
||||||
|
rememberedBy: r.remembered_by_name ?? "unknown",
|
||||||
|
rememberedAt:
|
||||||
|
r.remembered_at instanceof Date
|
||||||
|
? r.remembered_at
|
||||||
|
: new Date(r.remembered_at),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-delete a memory by setting forgotten_at.
|
||||||
|
*/
|
||||||
|
export async function forgetMemory(
|
||||||
|
meshId: string,
|
||||||
|
memoryId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await db
|
||||||
|
.update(meshMemory)
|
||||||
|
.set({ forgottenAt: new Date() })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(meshMemory.id, memoryId),
|
||||||
|
eq(meshMemory.meshId, meshId),
|
||||||
|
isNull(meshMemory.forgottenAt),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- File sharing ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a file metadata row after upload to MinIO.
|
||||||
|
*/
|
||||||
|
export async function uploadFile(args: {
|
||||||
|
meshId: string;
|
||||||
|
name: string;
|
||||||
|
sizeBytes: number;
|
||||||
|
mimeType?: string;
|
||||||
|
minioKey: string;
|
||||||
|
tags?: string[];
|
||||||
|
persistent?: boolean;
|
||||||
|
uploadedByName?: string;
|
||||||
|
uploadedByMember?: string;
|
||||||
|
targetSpec?: string;
|
||||||
|
expiresAt?: Date;
|
||||||
|
}): Promise<string> {
|
||||||
|
const [row] = await db
|
||||||
|
.insert(meshFile)
|
||||||
|
.values({
|
||||||
|
meshId: args.meshId,
|
||||||
|
name: args.name,
|
||||||
|
sizeBytes: args.sizeBytes,
|
||||||
|
mimeType: args.mimeType ?? null,
|
||||||
|
minioKey: args.minioKey,
|
||||||
|
tags: args.tags ?? [],
|
||||||
|
persistent: args.persistent ?? true,
|
||||||
|
uploadedByName: args.uploadedByName ?? null,
|
||||||
|
uploadedByMember: args.uploadedByMember ?? null,
|
||||||
|
targetSpec: args.targetSpec ?? null,
|
||||||
|
expiresAt: args.expiresAt ?? null,
|
||||||
|
})
|
||||||
|
.returning({ id: meshFile.id });
|
||||||
|
if (!row) throw new Error("failed to insert file row");
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single file by id, check it belongs to the mesh and is not deleted.
|
||||||
|
*/
|
||||||
|
export async function getFile(
|
||||||
|
meshId: string,
|
||||||
|
fileId: string,
|
||||||
|
): Promise<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sizeBytes: number;
|
||||||
|
mimeType: string | null;
|
||||||
|
minioKey: string;
|
||||||
|
tags: string[];
|
||||||
|
persistent: boolean;
|
||||||
|
uploadedByName: string | null;
|
||||||
|
targetSpec: string | null;
|
||||||
|
uploadedAt: Date;
|
||||||
|
} | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select({
|
||||||
|
id: meshFile.id,
|
||||||
|
name: meshFile.name,
|
||||||
|
sizeBytes: meshFile.sizeBytes,
|
||||||
|
mimeType: meshFile.mimeType,
|
||||||
|
minioKey: meshFile.minioKey,
|
||||||
|
tags: meshFile.tags,
|
||||||
|
persistent: meshFile.persistent,
|
||||||
|
uploadedByName: meshFile.uploadedByName,
|
||||||
|
targetSpec: meshFile.targetSpec,
|
||||||
|
uploadedAt: meshFile.uploadedAt,
|
||||||
|
})
|
||||||
|
.from(meshFile)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(meshFile.id, fileId),
|
||||||
|
eq(meshFile.meshId, meshId),
|
||||||
|
isNull(meshFile.deletedAt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
tags: (row.tags ?? []) as string[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List files in a mesh. Optionally filter by query (name ILIKE) or uploader.
|
||||||
|
*/
|
||||||
|
export async function listFiles(
|
||||||
|
meshId: string,
|
||||||
|
query?: string,
|
||||||
|
from?: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sizeBytes: number;
|
||||||
|
tags: string[];
|
||||||
|
uploadedBy: string;
|
||||||
|
uploadedAt: Date;
|
||||||
|
persistent: boolean;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const conditions = [
|
||||||
|
eq(meshFile.meshId, meshId),
|
||||||
|
isNull(meshFile.deletedAt),
|
||||||
|
];
|
||||||
|
if (query) {
|
||||||
|
conditions.push(sql`${meshFile.name} ILIKE ${"%" + query + "%"}`);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
conditions.push(eq(meshFile.uploadedByName, from));
|
||||||
|
}
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: meshFile.id,
|
||||||
|
name: meshFile.name,
|
||||||
|
sizeBytes: meshFile.sizeBytes,
|
||||||
|
tags: meshFile.tags,
|
||||||
|
uploadedByName: meshFile.uploadedByName,
|
||||||
|
uploadedAt: meshFile.uploadedAt,
|
||||||
|
persistent: meshFile.persistent,
|
||||||
|
})
|
||||||
|
.from(meshFile)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(desc(meshFile.uploadedAt))
|
||||||
|
.limit(100);
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
sizeBytes: r.sizeBytes,
|
||||||
|
tags: (r.tags ?? []) as string[],
|
||||||
|
uploadedBy: r.uploadedByName ?? "unknown",
|
||||||
|
uploadedAt: r.uploadedAt,
|
||||||
|
persistent: r.persistent,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a file access event (download/presigned URL generation).
|
||||||
|
*/
|
||||||
|
export async function recordFileAccess(
|
||||||
|
fileId: string,
|
||||||
|
sessionPubkey?: string,
|
||||||
|
peerName?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await db.insert(meshFileAccess).values({
|
||||||
|
fileId,
|
||||||
|
peerSessionPubkey: sessionPubkey ?? null,
|
||||||
|
peerName: peerName ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access log for a file.
|
||||||
|
*/
|
||||||
|
export async function getFileStatus(
|
||||||
|
fileId: string,
|
||||||
|
): Promise<Array<{ peerName: string; accessedAt: Date }>> {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
peerName: meshFileAccess.peerName,
|
||||||
|
accessedAt: meshFileAccess.accessedAt,
|
||||||
|
})
|
||||||
|
.from(meshFileAccess)
|
||||||
|
.where(eq(meshFileAccess.fileId, fileId))
|
||||||
|
.orderBy(desc(meshFileAccess.accessedAt));
|
||||||
|
return rows.map((r) => ({
|
||||||
|
peerName: r.peerName ?? "unknown",
|
||||||
|
accessedAt: r.accessedAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-delete a file by setting deleted_at.
|
||||||
|
*/
|
||||||
|
export async function deleteFile(
|
||||||
|
meshId: string,
|
||||||
|
fileId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await db
|
||||||
|
.update(meshFile)
|
||||||
|
.set({ deletedAt: new Date() })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(meshFile.id, fileId),
|
||||||
|
eq(meshFile.meshId, meshId),
|
||||||
|
isNull(meshFile.deletedAt),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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 {
|
||||||
@@ -839,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 }));
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ const envSchema = z.object({
|
|||||||
MAX_CONNECTIONS_PER_MESH: z.coerce.number().int().positive().default(100),
|
MAX_CONNECTIONS_PER_MESH: z.coerce.number().int().positive().default(100),
|
||||||
MAX_MESSAGE_BYTES: z.coerce.number().int().positive().default(65_536),
|
MAX_MESSAGE_BYTES: z.coerce.number().int().positive().default(65_536),
|
||||||
HOOK_RATE_LIMIT_PER_MIN: z.coerce.number().int().positive().default(30),
|
HOOK_RATE_LIMIT_PER_MIN: z.coerce.number().int().positive().default(30),
|
||||||
|
MINIO_ENDPOINT: z.string().default("minio:9000"),
|
||||||
|
MINIO_ACCESS_KEY: z.string().default("claudemesh"),
|
||||||
|
MINIO_SECRET_KEY: z.string().default("changeme"),
|
||||||
|
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"),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
28
apps/broker/src/minio.ts
Normal file
28
apps/broker/src/minio.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* MinIO client for file storage.
|
||||||
|
*
|
||||||
|
* Each mesh gets its own bucket (mesh-{meshId}). Files are stored under
|
||||||
|
* a key path that encodes persistence and origin:
|
||||||
|
* - persistent: shared/{fileId}/{originalName}
|
||||||
|
* - ephemeral: ephemeral/{YYYY-MM-DD}/{fileId}/{originalName}
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Client } from "minio";
|
||||||
|
import { env } from "./env";
|
||||||
|
|
||||||
|
export const minioClient = new Client({
|
||||||
|
endPoint: env.MINIO_ENDPOINT.split(":")[0]!,
|
||||||
|
port: parseInt(env.MINIO_ENDPOINT.split(":")[1] || "9000"),
|
||||||
|
useSSL: env.MINIO_USE_SSL,
|
||||||
|
accessKey: env.MINIO_ACCESS_KEY,
|
||||||
|
secretKey: env.MINIO_SECRET_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function ensureBucket(name: string): Promise<void> {
|
||||||
|
const exists = await minioClient.bucketExists(name);
|
||||||
|
if (!exists) await minioClient.makeBucket(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function meshBucketName(meshId: string): string {
|
||||||
|
return `mesh-${meshId.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
||||||
|
}
|
||||||
22
apps/broker/src/neo4j-client.ts
Normal file
22
apps/broker/src/neo4j-client.ts
Normal 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
24
apps/broker/src/qdrant.ts
Normal 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" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,6 +118,43 @@ export interface WSLeaveGroupMessage {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Client → broker: set a shared state key-value. */
|
||||||
|
export interface WSSetStateMessage {
|
||||||
|
type: "set_state";
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: read a shared state key. */
|
||||||
|
export interface WSGetStateMessage {
|
||||||
|
type: "get_state";
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: list all shared state entries. */
|
||||||
|
export interface WSListStateMessage {
|
||||||
|
type: "list_state";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: store a memory. */
|
||||||
|
export interface WSRememberMessage {
|
||||||
|
type: "remember";
|
||||||
|
content: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: full-text search memories. */
|
||||||
|
export interface WSRecallMessage {
|
||||||
|
type: "recall";
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: soft-delete a memory. */
|
||||||
|
export interface WSForgetMessage {
|
||||||
|
type: "forget";
|
||||||
|
memoryId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Broker → client: acknowledgement for a send. */
|
/** Broker → client: acknowledgement for a send. */
|
||||||
export interface WSAckMessage {
|
export interface WSAckMessage {
|
||||||
type: "ack";
|
type: "ack";
|
||||||
@@ -147,6 +184,422 @@ export interface WSPeersListMessage {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Broker → client: a state key was changed by another peer. */
|
||||||
|
export interface WSStateChangeMessage {
|
||||||
|
type: "state_change";
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: response to get_state. */
|
||||||
|
export interface WSStateResultMessage {
|
||||||
|
type: "state_result";
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
updatedAt: string;
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: response to list_state. */
|
||||||
|
export interface WSStateListMessage {
|
||||||
|
type: "state_list";
|
||||||
|
entries: Array<{
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
updatedBy: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: acknowledgement for a remember. */
|
||||||
|
export interface WSMemoryStoredMessage {
|
||||||
|
type: "memory_stored";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: response to recall. */
|
||||||
|
export interface WSMemoryResultsMessage {
|
||||||
|
type: "memory_results";
|
||||||
|
memories: Array<{
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
tags: string[];
|
||||||
|
rememberedBy: string;
|
||||||
|
rememberedAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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. */
|
||||||
|
export interface WSMessageStatusMessage {
|
||||||
|
type: "message_status";
|
||||||
|
messageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: delivery status with per-recipient detail. */
|
||||||
|
export interface WSMessageStatusResultMessage {
|
||||||
|
type: "message_status_result";
|
||||||
|
messageId: string;
|
||||||
|
targetSpec: string;
|
||||||
|
delivered: boolean;
|
||||||
|
deliveredAt: string | null;
|
||||||
|
recipients: Array<{
|
||||||
|
name: string;
|
||||||
|
pubkey: string;
|
||||||
|
status: "delivered" | "held" | "disconnected";
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- File sharing messages ---
|
||||||
|
|
||||||
|
/** Client → broker: get a presigned download URL for a file. */
|
||||||
|
export interface WSGetFileMessage {
|
||||||
|
type: "get_file";
|
||||||
|
fileId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: list files in the mesh. */
|
||||||
|
export interface WSListFilesMessage {
|
||||||
|
type: "list_files";
|
||||||
|
query?: string;
|
||||||
|
from?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: get access log for a file. */
|
||||||
|
export interface WSFileStatusMessage {
|
||||||
|
type: "file_status";
|
||||||
|
fileId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: soft-delete a file. */
|
||||||
|
export interface WSDeleteFileMessage {
|
||||||
|
type: "delete_file";
|
||||||
|
fileId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: presigned URL for downloading a file. */
|
||||||
|
export interface WSFileUrlMessage {
|
||||||
|
type: "file_url";
|
||||||
|
fileId: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: list of files in the mesh. */
|
||||||
|
export interface WSFileListMessage {
|
||||||
|
type: "file_list";
|
||||||
|
files: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
tags: string[];
|
||||||
|
uploadedBy: string;
|
||||||
|
uploadedAt: string;
|
||||||
|
persistent: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: access log for a file. */
|
||||||
|
export interface WSFileStatusResultMessage {
|
||||||
|
type: "file_status_result";
|
||||||
|
fileId: string;
|
||||||
|
accesses: Array<{
|
||||||
|
peerName: string;
|
||||||
|
accessedAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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";
|
||||||
@@ -162,11 +615,67 @@ export type WSClientMessage =
|
|||||||
| WSListPeersMessage
|
| WSListPeersMessage
|
||||||
| WSSetSummaryMessage
|
| WSSetSummaryMessage
|
||||||
| WSJoinGroupMessage
|
| WSJoinGroupMessage
|
||||||
| WSLeaveGroupMessage;
|
| WSLeaveGroupMessage
|
||||||
|
| WSSetStateMessage
|
||||||
|
| WSGetStateMessage
|
||||||
|
| WSListStateMessage
|
||||||
|
| WSRememberMessage
|
||||||
|
| WSRecallMessage
|
||||||
|
| WSForgetMessage
|
||||||
|
| WSMessageStatusMessage
|
||||||
|
| WSGetFileMessage
|
||||||
|
| WSListFilesMessage
|
||||||
|
| WSFileStatusMessage
|
||||||
|
| 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
|
||||||
| WSPushMessage
|
| WSPushMessage
|
||||||
| WSAckMessage
|
| WSAckMessage
|
||||||
| WSPeersListMessage
|
| WSPeersListMessage
|
||||||
|
| WSStateChangeMessage
|
||||||
|
| WSStateResultMessage
|
||||||
|
| WSStateListMessage
|
||||||
|
| WSMemoryStoredMessage
|
||||||
|
| WSMemoryResultsMessage
|
||||||
|
| WSMessageStatusResultMessage
|
||||||
|
| WSFileUrlMessage
|
||||||
|
| WSFileListMessage
|
||||||
|
| WSFileStatusResultMessage
|
||||||
|
| WSContextSharedMessage
|
||||||
|
| WSContextResultsMessage
|
||||||
|
| WSContextListMessage
|
||||||
|
| WSTaskCreatedMessage
|
||||||
|
| WSTaskListMessage
|
||||||
|
| WSVectorResultsMessage
|
||||||
|
| WSCollectionListMessage
|
||||||
|
| WSGraphResultMessage
|
||||||
|
| WSMeshQueryResultMessage
|
||||||
|
| WSMeshSchemaResultMessage
|
||||||
|
| WSStreamCreatedMessage
|
||||||
|
| WSStreamDataMessage
|
||||||
|
| WSStreamListMessage
|
||||||
|
| WSMeshInfoResultMessage
|
||||||
| WSErrorMessage;
|
| WSErrorMessage;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "0.2.0",
|
"version": "0.5.9",
|
||||||
"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",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface LaunchArgs {
|
|||||||
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
|
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
|
||||||
joinLink: string | null;
|
joinLink: string | null;
|
||||||
meshSlug: string | null;
|
meshSlug: string | null;
|
||||||
|
messageMode: "push" | "inbox" | "off" | null;
|
||||||
quiet: boolean;
|
quiet: boolean;
|
||||||
skipPermConfirm: boolean;
|
skipPermConfirm: boolean;
|
||||||
claudeArgs: string[];
|
claudeArgs: string[];
|
||||||
@@ -38,6 +39,7 @@ function parseArgs(argv: string[]): LaunchArgs {
|
|||||||
groups: null,
|
groups: null,
|
||||||
joinLink: null,
|
joinLink: null,
|
||||||
meshSlug: null,
|
meshSlug: null,
|
||||||
|
messageMode: null,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
skipPermConfirm: false,
|
skipPermConfirm: false,
|
||||||
claudeArgs: [],
|
claudeArgs: [],
|
||||||
@@ -66,6 +68,10 @@ function parseArgs(argv: string[]): LaunchArgs {
|
|||||||
result.meshSlug = argv[++i]!;
|
result.meshSlug = argv[++i]!;
|
||||||
} else if (arg.startsWith("--mesh=")) {
|
} else if (arg.startsWith("--mesh=")) {
|
||||||
result.meshSlug = arg.slice("--mesh=".length);
|
result.meshSlug = arg.slice("--mesh=".length);
|
||||||
|
} else if (arg === "--inbox") {
|
||||||
|
result.messageMode = "inbox";
|
||||||
|
} else if (arg === "--no-messages") {
|
||||||
|
result.messageMode = "off";
|
||||||
} else if (arg === "--quiet") {
|
} else if (arg === "--quiet") {
|
||||||
result.quiet = true;
|
result.quiet = true;
|
||||||
} else if (arg === "-y" || arg === "--yes") {
|
} else if (arg === "-y" || arg === "--yes") {
|
||||||
@@ -171,7 +177,7 @@ async function confirmPermissions(): Promise<void> {
|
|||||||
|
|
||||||
// --- Banner ---
|
// --- Banner ---
|
||||||
|
|
||||||
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[]): void {
|
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[], messageMode: "push" | "inbox" | "off"): void {
|
||||||
const useColor =
|
const useColor =
|
||||||
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||||
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||||
@@ -183,9 +189,15 @@ function printBanner(name: string, meshSlug: string, role: string | null, groups
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const rule = "─".repeat(60);
|
const rule = "─".repeat(60);
|
||||||
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags}`));
|
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags} [${messageMode}]`));
|
||||||
console.log(rule);
|
console.log(rule);
|
||||||
|
if (messageMode === "push") {
|
||||||
console.log("Peer messages arrive as <channel> reminders in real-time.");
|
console.log("Peer messages arrive as <channel> reminders in real-time.");
|
||||||
|
} else if (messageMode === "inbox") {
|
||||||
|
console.log("Peer messages held in inbox. Use check_messages to read.");
|
||||||
|
} else {
|
||||||
|
console.log("Messages off. Use check_messages to poll manually.");
|
||||||
|
}
|
||||||
console.log("Peers send text only — they cannot call tools or read files.");
|
console.log("Peers send text only — they cannot call tools or read files.");
|
||||||
console.log(dim(`Config: ${getConfigPath()}`));
|
console.log(dim(`Config: ${getConfigPath()}`));
|
||||||
console.log(rule);
|
console.log(rule);
|
||||||
@@ -263,6 +275,8 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
let role: string | null = args.role;
|
let role: string | null = args.role;
|
||||||
let parsedGroups: GroupEntry[] = args.groups ? parseGroupsString(args.groups) : [];
|
let parsedGroups: GroupEntry[] = args.groups ? parseGroupsString(args.groups) : [];
|
||||||
|
|
||||||
|
let messageMode: "push" | "inbox" | "off" = args.messageMode ?? "push";
|
||||||
|
|
||||||
if (!args.quiet) {
|
if (!args.quiet) {
|
||||||
if (role === null) {
|
if (role === null) {
|
||||||
const answer = await askLine(" Role (optional): ");
|
const answer = await askLine(" Role (optional): ");
|
||||||
@@ -272,6 +286,18 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
const answer = await askLine(" Groups (comma-separated, optional): ");
|
const answer = await askLine(" Groups (comma-separated, optional): ");
|
||||||
if (answer) parsedGroups = parseGroupsString(answer);
|
if (answer) parsedGroups = parseGroupsString(answer);
|
||||||
}
|
}
|
||||||
|
if (args.messageMode === null) {
|
||||||
|
console.log("\n Message mode:");
|
||||||
|
console.log(" 1) Push (real-time, peers can interrupt your work)");
|
||||||
|
console.log(" 2) Inbox (held until you check, notification only)");
|
||||||
|
console.log(" 3) Off (tools only, no messages)");
|
||||||
|
console.log("");
|
||||||
|
const answer = await askLine(" Choice [1]: ");
|
||||||
|
const choice = parseInt(answer || "1", 10);
|
||||||
|
if (choice === 2) messageMode = "inbox";
|
||||||
|
else if (choice === 3) messageMode = "off";
|
||||||
|
else messageMode = "push";
|
||||||
|
}
|
||||||
if (role || parsedGroups.length) console.log("");
|
if (role || parsedGroups.length) console.log("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +319,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
meshes: [mesh],
|
meshes: [mesh],
|
||||||
displayName,
|
displayName,
|
||||||
...(parsedGroups.length > 0 ? { groups: parsedGroups } : {}),
|
...(parsedGroups.length > 0 ? { groups: parsedGroups } : {}),
|
||||||
|
messageMode,
|
||||||
};
|
};
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(tmpDir, "config.json"),
|
join(tmpDir, "config.json"),
|
||||||
@@ -302,7 +329,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
|
|
||||||
// 5. Banner + permission confirmation.
|
// 5. Banner + permission confirmation.
|
||||||
if (!args.quiet) {
|
if (!args.quiet) {
|
||||||
printBanner(displayName, mesh.slug, role, parsedGroups);
|
printBanner(displayName, mesh.slug, role, parsedGroups, messageMode);
|
||||||
// Auto-permissions confirmation — needed for autonomous peer messaging.
|
// Auto-permissions confirmation — needed for autonomous peer messaging.
|
||||||
if (!args.skipPermConfirm) {
|
if (!args.skipPermConfirm) {
|
||||||
await confirmPermissions();
|
await confirmPermissions();
|
||||||
|
|||||||
@@ -129,34 +129,121 @@ function formatPush(p: InboundPush, meshSlug: string): string {
|
|||||||
export async function startMcpServer(): Promise<void> {
|
export async function startMcpServer(): Promise<void> {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const myName = config.displayName ?? "unnamed";
|
||||||
|
const myGroups = (config.groups ?? []).map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ') || "none";
|
||||||
|
const messageMode = config.messageMode ?? "push";
|
||||||
|
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
{ name: "claudemesh", version: "0.1.4" },
|
{ name: "claudemesh", version: "0.3.0" },
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
experimental: { "claude/channel": {} },
|
experimental: { "claude/channel": {} },
|
||||||
tools: {},
|
tools: {},
|
||||||
},
|
},
|
||||||
instructions: `You are connected to claudemesh — a peer mesh for Claude Code sessions on this machine and elsewhere.
|
instructions: `## Identity
|
||||||
|
You are "${myName}" — a peer in the claudemesh network. Your groups: ${myGroups}. You are one of several Claude Code sessions connected to the same mesh. No orchestrator exists — peers are equals. Your identity comes from your name and group roles, not from a central authority.
|
||||||
|
|
||||||
IMPORTANT: When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Do not wait until your current task is finished. Pause what you are doing, reply to the message using send_message, then resume your work. Treat incoming peer messages like a coworker tapping you on the shoulder — answer right away, even if you're in the middle of something.
|
## Responding to messages
|
||||||
|
When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Pause your current task, reply via send_message, then resume. Read from_name, mesh_slug, and priority from the channel attributes. Reply by setting \`to\` to the sender's from_name (display name). Stay in character per your system prompt. Do not ignore low-priority messages — acknowledge them briefly even if you defer action.
|
||||||
|
|
||||||
Read the from_id, from_name, mesh_slug, and priority attributes to understand context. Reply by calling send_message with to set to the from_name (display name) of the sender. The \`to\` field can be a peer name, pubkey, @group, or * for broadcast.
|
## Tools
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| send_message(to, message, priority?) | Send to peer name, @group, or * broadcast. \`to\` accepts display name, pubkey hex, @groupname, or *. |
|
||||||
|
| list_peers(mesh_slug?) | List connected peers with status, summary, groups, and roles. |
|
||||||
|
| check_messages() | Drain buffered inbound messages (auto-pushed in most cases, use as fallback). |
|
||||||
|
| set_summary(summary) | Set 1-2 sentence description of your current work, visible to all peers. |
|
||||||
|
| set_status(status) | Override status: idle, working, or dnd. |
|
||||||
|
| join_group(name, role?) | Join a @group with optional role (lead, member, observer, or any string). |
|
||||||
|
| leave_group(name) | Leave a @group. |
|
||||||
|
| set_state(key, value) | Write shared state; pushes change to all peers. |
|
||||||
|
| get_state(key) | Read a shared state value. |
|
||||||
|
| list_state() | List all state keys with values, authors, and timestamps. |
|
||||||
|
| remember(content, tags?) | Store persistent knowledge with optional tags. |
|
||||||
|
| recall(query) | Full-text search over mesh memory. |
|
||||||
|
| forget(id) | Soft-delete a memory entry. |
|
||||||
|
| share_file(path, name?, tags?) | Share a persistent file with the mesh. |
|
||||||
|
| get_file(id, save_to) | Download a shared file to a local path. |
|
||||||
|
| list_files(query?, from?) | Find files shared in the mesh. |
|
||||||
|
| file_status(id) | Check who has accessed a file. |
|
||||||
|
| 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. |
|
||||||
|
|
||||||
Available tools:
|
If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
|
||||||
- list_peers: see joined meshes + their connection status
|
|
||||||
- send_message: send to a peer by display name, pubkey, @group, #channel, or * broadcast (priority: now/next/low)
|
|
||||||
- check_messages: drain buffered inbound messages (usually auto-pushed)
|
|
||||||
- set_summary: 1-2 sentence summary of what you're working on
|
|
||||||
- set_status: manually override your status (idle/working/dnd)
|
|
||||||
- join_group: join a @group with optional role
|
|
||||||
- leave_group: leave a @group
|
|
||||||
|
|
||||||
Message priority:
|
Multi-target: send_message accepts an array of targets for the 'to' field.
|
||||||
- "now": delivered immediately regardless of recipient status (use sparingly)
|
send_message(to: ["Alice", "@backend"], message: "sprint starts")
|
||||||
- "next" (default): delivered when recipient is idle
|
Targets are deduplicated — each peer receives the message once.
|
||||||
- "low": pull-only (check_messages)
|
|
||||||
|
|
||||||
If you have multiple joined meshes, prefix the \`to\` argument of send_message with \`<mesh-slug>:\` to disambiguate. Otherwise claudemesh picks the single joined mesh.`,
|
Targeted views: when different audiences need different details about the same event,
|
||||||
|
send tailored messages instead of one generic broadcast:
|
||||||
|
send_message(to: "@frontend", message: "Auth v2: useAuth hook changed, see src/auth/")
|
||||||
|
send_message(to: "@backend", message: "Auth v2: new /api/auth/v2 endpoints, v1 deprecated")
|
||||||
|
send_message(to: "@pm", message: "Auth v2 done. 3 points, no blockers.")
|
||||||
|
|
||||||
|
## Groups
|
||||||
|
Groups are routing labels. Send to @groupname to multicast to all members. Roles are metadata that peers interpret: a "lead" gathers input before synthesizing a response, a "member" contributes when asked, an "observer" watches silently. Join and leave groups dynamically with join_group/leave_group. Check list_peers to see who belongs to which groups and their roles.
|
||||||
|
|
||||||
|
## State
|
||||||
|
Shared key-value store scoped to the mesh. Use get_state/set_state for live coordination facts (deploy frozen? current sprint? PR queue). set_state pushes the change to all connected peers. Read state before asking peers questions — the answer may already be there. State is operational, not archival.
|
||||||
|
|
||||||
|
## Memory
|
||||||
|
Persistent knowledge that survives across sessions. Use remember(content, tags?) to store lessons, decisions, and incidents. Use recall(query) to search before asking peers. New peers should recall at session start to load institutional knowledge.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- "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)
|
||||||
|
- "low": pull-only via check_messages (FYI, non-blocking context)
|
||||||
|
|
||||||
|
## Coordination
|
||||||
|
Call list_peers at session start to understand who is online, their roles, and what they are working on. If you are a group lead, gather input from members before responding to external requests — do not answer alone. If you are a member, contribute to your lead when asked. Use @group messages for team-wide questions, direct messages for 1:1 coordination. Set a meaningful summary so peers know your current focus.
|
||||||
|
|
||||||
|
## Message Mode
|
||||||
|
Your message mode is "${messageMode}".
|
||||||
|
- push: messages arrive in real-time as channel notifications. Respond immediately.
|
||||||
|
- inbox: messages are held. You'll see "[inbox] New message from X" notifications. Call check_messages to read them.
|
||||||
|
- off: no message notifications. Use check_messages manually to poll.`,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,22 +265,32 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
const { to, message, priority } = (args ?? {}) as SendMessageArgs;
|
const { to, message, priority } = (args ?? {}) as SendMessageArgs;
|
||||||
if (!to || !message)
|
if (!to || !message)
|
||||||
return text("send_message: `to` and `message` required", true);
|
return text("send_message: `to` and `message` required", true);
|
||||||
const { client, targetSpec, error } = await resolveClient(to);
|
|
||||||
if (!client)
|
// Handle multi-target: to can be string or string[]
|
||||||
return text(`send_message: ${error ?? "no client resolved"}`, true);
|
const targets = Array.isArray(to) ? to : [to];
|
||||||
|
const results: string[] = [];
|
||||||
|
const seen = new Set<string>(); // dedup by resolved pubkey
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
const { client, targetSpec, error } = await resolveClient(target);
|
||||||
|
if (!client) {
|
||||||
|
results.push(`✗ ${target}: ${error ?? "no client resolved"}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (seen.has(targetSpec)) continue; // dedup
|
||||||
|
seen.add(targetSpec);
|
||||||
const result = await client.send(
|
const result = await client.send(
|
||||||
targetSpec,
|
targetSpec,
|
||||||
message,
|
message,
|
||||||
(priority ?? "next") as Priority,
|
(priority ?? "next") as Priority,
|
||||||
);
|
);
|
||||||
if (!result.ok)
|
if (!result.ok) {
|
||||||
return text(
|
results.push(`✗ ${target}: ${result.error}`);
|
||||||
`send_message failed (${client.meshSlug}): ${result.error}`,
|
} else {
|
||||||
true,
|
results.push(`✓ ${target} → ${result.messageId}`);
|
||||||
);
|
}
|
||||||
return text(
|
}
|
||||||
`Sent to ${targetSpec} via ${client.meshSlug} [${priority ?? "next"}] → ${result.messageId}`,
|
return text(results.join("\n"));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "list_peers": {
|
case "list_peers": {
|
||||||
@@ -226,6 +323,24 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
return text(sections.join("\n\n"));
|
return text(sections.join("\n\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "message_status": {
|
||||||
|
const { id } = (args ?? {}) as { id?: string };
|
||||||
|
if (!id) return text("message_status: `id` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("message_status: not connected", true);
|
||||||
|
const result = await client.messageStatus(id);
|
||||||
|
if (!result) return text(`Message ${id} not found or timed out.`);
|
||||||
|
const recipientLines = result.recipients.map(
|
||||||
|
(r: { name: string; pubkey: string; status: string }) =>
|
||||||
|
` - ${r.name} (${r.pubkey.slice(0, 12)}…): ${r.status}`,
|
||||||
|
);
|
||||||
|
return text(
|
||||||
|
`Message ${id.slice(0, 12)}… → ${result.targetSpec}\n` +
|
||||||
|
`Delivered: ${result.delivered}${result.deliveredAt ? ` at ${result.deliveredAt}` : ""}\n` +
|
||||||
|
`Recipients:\n${recipientLines.join("\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case "check_messages": {
|
case "check_messages": {
|
||||||
const drained: string[] = [];
|
const drained: string[] = [];
|
||||||
for (const c of allClients()) {
|
for (const c of allClients()) {
|
||||||
@@ -269,6 +384,382 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
return text(`Left @${groupName}`);
|
return text(`Left @${groupName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
case "set_state": {
|
||||||
|
const { key, value } = (args ?? {}) as { key?: string; value?: unknown };
|
||||||
|
if (!key) return text("set_state: `key` required", true);
|
||||||
|
for (const c of allClients()) await c.setState(key, value);
|
||||||
|
return text(`State set: ${key} = ${JSON.stringify(value)}`);
|
||||||
|
}
|
||||||
|
case "get_state": {
|
||||||
|
const { key } = (args ?? {}) as { key?: string };
|
||||||
|
if (!key) return text("get_state: `key` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("get_state: not connected", true);
|
||||||
|
const result = await client.getState(key);
|
||||||
|
if (!result) return text(`State "${key}" not found.`);
|
||||||
|
return text(`${key} = ${JSON.stringify(result.value)} (set by ${result.updatedBy} at ${result.updatedAt})`);
|
||||||
|
}
|
||||||
|
case "list_state": {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("list_state: not connected", true);
|
||||||
|
const entries = await client.listState();
|
||||||
|
if (entries.length === 0) return text("No shared state set.");
|
||||||
|
const lines = entries.map(e => `- **${e.key}** = ${JSON.stringify(e.value)} (by ${e.updatedBy})`);
|
||||||
|
return text(lines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Memory ---
|
||||||
|
case "remember": {
|
||||||
|
const { content, tags } = (args ?? {}) as { content?: string; tags?: string[] };
|
||||||
|
if (!content) return text("remember: `content` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("remember: not connected", true);
|
||||||
|
const id = await client.remember(content, tags);
|
||||||
|
return text(`Remembered${id ? ` (${id})` : ""}: "${content.slice(0, 80)}${content.length > 80 ? '...' : ''}"`);
|
||||||
|
}
|
||||||
|
case "recall": {
|
||||||
|
const { query } = (args ?? {}) as { query?: string };
|
||||||
|
if (!query) return text("recall: `query` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("recall: not connected", true);
|
||||||
|
const memories = await client.recall(query);
|
||||||
|
if (memories.length === 0) return text(`No memories found for "${query}".`);
|
||||||
|
const lines = memories.map(m => `- [${m.id.slice(0, 8)}] ${m.content} (by ${m.rememberedBy}, ${m.rememberedAt})`);
|
||||||
|
return text(`${memories.length} memor${memories.length === 1 ? 'y' : 'ies'}:\n${lines.join("\n")}`);
|
||||||
|
}
|
||||||
|
case "forget": {
|
||||||
|
const { id } = (args ?? {}) as { id?: string };
|
||||||
|
if (!id) return text("forget: `id` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("forget: not connected", true);
|
||||||
|
await client.forget(id);
|
||||||
|
return text(`Forgotten: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Files ---
|
||||||
|
case "share_file": {
|
||||||
|
const { path: filePath, name: fileName, tags } = (args ?? {}) as { path?: string; name?: string; tags?: string[] };
|
||||||
|
if (!filePath) return text("share_file: `path` required", true);
|
||||||
|
const { existsSync } = await import("node:fs");
|
||||||
|
if (!existsSync(filePath)) return text(`share_file: file not found: ${filePath}`, true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("share_file: not connected", true);
|
||||||
|
try {
|
||||||
|
const fileId = await client.uploadFile(filePath, client.meshId, client.meshSlug, {
|
||||||
|
name: fileName, tags, persistent: true,
|
||||||
|
});
|
||||||
|
return text(`Shared: ${fileName ?? filePath} (${fileId})`);
|
||||||
|
} catch (e) {
|
||||||
|
return text(`share_file: upload failed — ${e instanceof Error ? e.message : String(e)}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "get_file": {
|
||||||
|
const { id, save_to } = (args ?? {}) as { id?: string; save_to?: string };
|
||||||
|
if (!id || !save_to) return text("get_file: `id` and `save_to` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("get_file: not connected", true);
|
||||||
|
const result = await client.getFile(id);
|
||||||
|
if (!result) return text(`get_file: file ${id} not found`, true);
|
||||||
|
const res = await fetch(result.url, { signal: AbortSignal.timeout(30_000) });
|
||||||
|
if (!res.ok) return text(`get_file: download failed (${res.status})`, true);
|
||||||
|
const { writeFileSync, mkdirSync } = await import("node:fs");
|
||||||
|
const { dirname } = await import("node:path");
|
||||||
|
mkdirSync(dirname(save_to), { recursive: true });
|
||||||
|
writeFileSync(save_to, Buffer.from(await res.arrayBuffer()));
|
||||||
|
return text(`Downloaded: ${result.name} → ${save_to}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "list_files": {
|
||||||
|
const { query, from } = (args ?? {}) as { query?: string; from?: string };
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("list_files: not connected", true);
|
||||||
|
const files = await client.listFiles(query, from);
|
||||||
|
if (files.length === 0) return text("No files found.");
|
||||||
|
const lines = files.map(f =>
|
||||||
|
`- **${f.name}** (${f.id.slice(0, 8)}…, ${f.size} bytes) by ${f.uploadedBy}${f.tags.length ? ` [${f.tags.join(", ")}]` : ""}`
|
||||||
|
);
|
||||||
|
return text(lines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
case "file_status": {
|
||||||
|
const { id } = (args ?? {}) as { id?: string };
|
||||||
|
if (!id) return text("file_status: `id` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("file_status: not connected", true);
|
||||||
|
const accesses = await client.fileStatus(id);
|
||||||
|
if (accesses.length === 0) return text("No one has accessed this file yet.");
|
||||||
|
const lines = accesses.map(a => `- ${a.peerName} at ${a.accessedAt}`);
|
||||||
|
return text(`Accessed by:\n${lines.join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "delete_file": {
|
||||||
|
const { id } = (args ?? {}) as { id?: string };
|
||||||
|
if (!id) return text("delete_file: `id` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("delete_file: not connected", true);
|
||||||
|
await client.deleteFile(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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ping_mesh": {
|
||||||
|
const { priorities: pingPriorities } = (args ?? {}) as { priorities?: string[] };
|
||||||
|
const toTest = (pingPriorities ?? ["now", "next"]) as Priority[];
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("ping_mesh: not connected", true);
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
// Diagnostics: connection state
|
||||||
|
results.push(`WS status: ${client.status}`);
|
||||||
|
results.push(`Mesh: ${client.meshSlug}`);
|
||||||
|
|
||||||
|
// Check own peer status (explains priority gating)
|
||||||
|
const peers = await client.listPeers();
|
||||||
|
const selfPeer = peers.find(p => p.displayName === myName);
|
||||||
|
results.push(`Your status: ${selfPeer?.status ?? "not found in peer list"}`);
|
||||||
|
results.push(`Peers online: ${peers.length}`);
|
||||||
|
results.push(`Push buffer: ${client.pushHistory.length} buffered`);
|
||||||
|
|
||||||
|
// Test send→ack latency per priority (doesn't need round-trip)
|
||||||
|
for (const prio of toTest) {
|
||||||
|
const sendTime = Date.now();
|
||||||
|
// Send to a peer if one exists, otherwise broadcast
|
||||||
|
const target = peers.find(p => p.displayName !== myName);
|
||||||
|
const sendResult = await client.send(
|
||||||
|
target?.pubkey ?? "*",
|
||||||
|
`__ping__ ${prio} from ${myName} at ${new Date().toISOString()}`,
|
||||||
|
prio,
|
||||||
|
);
|
||||||
|
const ackTime = Date.now();
|
||||||
|
|
||||||
|
if (!sendResult.ok) {
|
||||||
|
results.push(`[${prio}] SEND FAILED: ${sendResult.error}`);
|
||||||
|
} else {
|
||||||
|
results.push(`[${prio}] send→ack: ${ackTime - sendTime}ms (msgId: ${sendResult.messageId?.slice(0, 12)})`);
|
||||||
|
if (prio !== "now" && selfPeer?.status === "working") {
|
||||||
|
results.push(` ⚠ peer status is "working" — broker holds "${prio}" until idle`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if notification pipeline works
|
||||||
|
results.push("");
|
||||||
|
results.push("Pipeline check:");
|
||||||
|
results.push(` onPush handlers: active`);
|
||||||
|
results.push(` messageMode: ${messageMode}`);
|
||||||
|
results.push(` server.notification: ${messageMode === "off" ? "disabled (mode=off)" : "enabled"}`);
|
||||||
|
|
||||||
|
return text(results.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return text(`Unknown tool: ${name}`, true);
|
return text(`Unknown tool: ${name}`, true);
|
||||||
}
|
}
|
||||||
@@ -284,12 +775,33 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
// any mesh's broker connection becomes a <channel source="claudemesh">
|
// any mesh's broker connection becomes a <channel source="claudemesh">
|
||||||
// system reminder injected into Claude Code's context.
|
// system reminder injected into Claude Code's context.
|
||||||
for (const client of allClients()) {
|
for (const client of allClients()) {
|
||||||
|
// Event-driven push: WS onPush fires immediately when a message arrives.
|
||||||
|
// Claude Code's setNotificationHandler → enqueue → React useEffect pipeline
|
||||||
|
// processes notifications instantly (no polling needed on Claude's side).
|
||||||
|
// The old poll-based approach was an overcorrection — Claude Code source
|
||||||
|
// confirms event-driven notification processing.
|
||||||
client.onPush(async (msg) => {
|
client.onPush(async (msg) => {
|
||||||
|
if (messageMode === "off") return;
|
||||||
|
|
||||||
const fromPubkey = msg.senderPubkey || "";
|
const fromPubkey = msg.senderPubkey || "";
|
||||||
// Resolve sender's display name from the cached peer list.
|
|
||||||
const fromName = fromPubkey
|
const fromName = fromPubkey
|
||||||
? await resolvePeerName(client, fromPubkey)
|
? await resolvePeerName(client, fromPubkey)
|
||||||
: "unknown";
|
: "unknown";
|
||||||
|
|
||||||
|
if (messageMode === "inbox") {
|
||||||
|
try {
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content: `[inbox] New message from ${fromName}. Use check_messages to read.`,
|
||||||
|
meta: { kind: "inbox_notification", from_name: fromName },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push mode — full content
|
||||||
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
||||||
try {
|
try {
|
||||||
await server.notification({
|
await server.notification({
|
||||||
@@ -308,13 +820,81 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
process.stderr.write(`[claudemesh] pushed: from=${fromName} content=${content.slice(0, 60)}\n`);
|
||||||
/* channel push is best-effort; check_messages is the fallback */
|
} catch (pushErr) {
|
||||||
|
process.stderr.write(`[claudemesh] push FAILED: ${pushErr}\n`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
try {
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content: `[state] ${change.key} = ${JSON.stringify(change.value)} (set by ${change.updatedBy})`,
|
||||||
|
meta: {
|
||||||
|
kind: "state_change",
|
||||||
|
key: change.key,
|
||||||
|
updated_by: change.updatedBy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Welcome notification: give Claude immediate context on connect.
|
||||||
|
// Triggers Claude to call mesh_info/list_peers without user input.
|
||||||
|
setTimeout(async () => {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client || client.status !== "open") return;
|
||||||
|
try {
|
||||||
|
const peers = await client.listPeers();
|
||||||
|
const peerNames = peers
|
||||||
|
.filter(p => p.displayName !== myName)
|
||||||
|
.map(p => p.displayName)
|
||||||
|
.join(", ") || "none";
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content: `[system] Connected as ${myName} to mesh ${client.meshSlug}. ${peers.length} peer(s) online: ${peerNames}. Call mesh_info for full details or set_summary to announce yourself.`,
|
||||||
|
meta: { kind: "welcome", mesh_slug: client.meshSlug },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
}, 3_000); // 3s delay: let WS connect + hello_ack complete first
|
||||||
|
|
||||||
|
// Event loop keepalive: Node.js stdout to a pipe is buffered. Without
|
||||||
|
// periodic event loop activity, stdout.write() from WS callbacks may not
|
||||||
|
// flush until the next I/O event. This 1s interval keeps the event loop
|
||||||
|
// ticking so channel notifications flush promptly — same pattern that made
|
||||||
|
// claude-intercom's push delivery reliable (its 1s HTTP poll had this
|
||||||
|
// effect as a side effect). The interval does nothing except prevent the
|
||||||
|
// event loop from settling.
|
||||||
|
const keepalive = setInterval(() => {
|
||||||
|
// Intentionally empty — the interval itself keeps the event loop active.
|
||||||
|
// Do NOT call .unref() — that would defeat the purpose.
|
||||||
|
}, 1_000);
|
||||||
|
void keepalive; // suppress unused warning
|
||||||
|
|
||||||
const shutdown = (): void => {
|
const shutdown = (): void => {
|
||||||
|
clearInterval(keepalive);
|
||||||
stopAll();
|
stopAll();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ export const TOOLS: Tool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
to: {
|
to: {
|
||||||
type: "string",
|
oneOf: [
|
||||||
description: "Peer name, pubkey, @group, or #channel",
|
{ type: "string", description: "Peer name, pubkey, @group" },
|
||||||
|
{ type: "array", items: { type: "string" }, description: "Multiple targets" },
|
||||||
|
],
|
||||||
|
description: "Single target or array of targets",
|
||||||
},
|
},
|
||||||
message: { type: "string", description: "Message text" },
|
message: { type: "string", description: "Message text" },
|
||||||
priority: {
|
priority: {
|
||||||
@@ -44,6 +47,21 @@ export const TOOLS: Tool[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "message_status",
|
||||||
|
description:
|
||||||
|
"Check the delivery status of a sent message. Shows whether each recipient received it.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
description: "Message ID (returned by send_message)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "check_messages",
|
name: "check_messages",
|
||||||
description:
|
description:
|
||||||
@@ -105,4 +123,453 @@ export const TOOLS: Tool[] = [
|
|||||||
required: ["name"],
|
required: ["name"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- State tools ---
|
||||||
|
{
|
||||||
|
name: "set_state",
|
||||||
|
description:
|
||||||
|
"Set a shared state value visible to all peers in the mesh. Pushes a change notification.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
key: { type: "string" },
|
||||||
|
value: { description: "Any JSON value" },
|
||||||
|
},
|
||||||
|
required: ["key", "value"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_state",
|
||||||
|
description: "Read a shared state value.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
key: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["key"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_state",
|
||||||
|
description: "List all shared state keys and values in the mesh.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Memory tools ---
|
||||||
|
{
|
||||||
|
name: "remember",
|
||||||
|
description:
|
||||||
|
"Store persistent knowledge in the mesh's shared memory. Survives across sessions.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
content: {
|
||||||
|
type: "string",
|
||||||
|
description: "The knowledge to remember",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Optional categorization tags",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["content"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recall",
|
||||||
|
description: "Search the mesh's shared memory by relevance.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string", description: "Search query" },
|
||||||
|
},
|
||||||
|
required: ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forget",
|
||||||
|
description: "Remove a memory from the mesh's shared knowledge.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "Memory ID to forget" },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- File tools ---
|
||||||
|
{
|
||||||
|
name: "share_file",
|
||||||
|
description:
|
||||||
|
"Share a persistent file with the mesh. All current and future peers can access it.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string", description: "Local file path to share" },
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
description: "Display name (defaults to filename)",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Tags for categorization",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_file",
|
||||||
|
description: "Download a shared file to a local path.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "File ID" },
|
||||||
|
save_to: {
|
||||||
|
type: "string",
|
||||||
|
description: "Local path to save the file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["id", "save_to"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_files",
|
||||||
|
description: "List files shared in the mesh.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string", description: "Search by name or tags" },
|
||||||
|
from: { type: "string", description: "Filter by uploader name" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file_status",
|
||||||
|
description: "Check who has accessed a shared file.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "File ID" },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete_file",
|
||||||
|
description: "Remove a shared file from the mesh.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "File 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: {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Diagnostics ---
|
||||||
|
{
|
||||||
|
name: "ping_mesh",
|
||||||
|
description:
|
||||||
|
"Send test messages through the full pipeline and measure round-trip timing per priority. Diagnoses push delivery issues.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
priorities: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string", enum: ["now", "next", "low"] },
|
||||||
|
description: "Priorities to test (default: [\"now\", \"next\"])",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export type Priority = "now" | "next" | "low";
|
|||||||
export type PeerStatus = "idle" | "working" | "dnd";
|
export type PeerStatus = "idle" | "working" | "dnd";
|
||||||
|
|
||||||
export interface SendMessageArgs {
|
export interface SendMessageArgs {
|
||||||
to: string; // peer name, pubkey, or #channel
|
to: string | string[]; // peer name, pubkey, @group, or array of targets
|
||||||
message: string;
|
message: string;
|
||||||
priority?: Priority;
|
priority?: Priority;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface Config {
|
|||||||
meshes: JoinedMesh[];
|
meshes: JoinedMesh[];
|
||||||
displayName?: string; // per-session override, written by `claudemesh launch --name`
|
displayName?: string; // per-session override, written by `claudemesh launch --name`
|
||||||
groups?: GroupEntry[];
|
groups?: GroupEntry[];
|
||||||
|
messageMode?: "push" | "inbox" | "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
||||||
@@ -53,7 +54,7 @@ export function loadConfig(): Config {
|
|||||||
if (!parsed || !Array.isArray(parsed.meshes)) {
|
if (!parsed || !Array.isArray(parsed.meshes)) {
|
||||||
return { version: 1, meshes: [] };
|
return { version: 1, meshes: [] };
|
||||||
}
|
}
|
||||||
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups };
|
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups, messageMode: parsed.messageMode };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export class BrokerClient {
|
|||||||
private pushHandlers = new Set<PushHandler>();
|
private pushHandlers = new Set<PushHandler>();
|
||||||
private pushBuffer: InboundPush[] = [];
|
private pushBuffer: InboundPush[] = [];
|
||||||
private listPeersResolvers: Array<(peers: PeerInfo[]) => void> = [];
|
private listPeersResolvers: Array<(peers: PeerInfo[]) => void> = [];
|
||||||
|
private stateResolvers: Array<(result: { key: string; value: unknown; updatedBy: string; updatedAt: string } | null) => void> = [];
|
||||||
|
private stateListResolvers: Array<(entries: Array<{ key: string; value: unknown; updatedBy: string; updatedAt: string }>) => void> = [];
|
||||||
|
private memoryStoreResolvers: Array<(id: string | null) => void> = [];
|
||||||
|
private memoryRecallResolvers: Array<(memories: Array<{ id: string; content: string; tags: string[]; rememberedBy: string; rememberedAt: string }>) => void> = [];
|
||||||
|
private stateChangeHandlers = new Set<(change: { key: string; value: unknown; updatedBy: string }) => void>();
|
||||||
private sessionPubkey: string | null = null;
|
private sessionPubkey: string | null = null;
|
||||||
private sessionSecretKey: string | null = null;
|
private sessionSecretKey: string | null = null;
|
||||||
private closed = false;
|
private closed = false;
|
||||||
@@ -325,6 +330,465 @@ export class BrokerClient {
|
|||||||
this.ws.send(JSON.stringify({ type: "leave_group", name }));
|
this.ws.send(JSON.stringify({ type: "leave_group", name }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
|
||||||
|
/** Set a shared state value visible to all peers in the mesh. */
|
||||||
|
async setState(key: string, value: unknown): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "set_state", key, value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read a shared state value. */
|
||||||
|
async getState(key: string): Promise<{ key: string; value: unknown; updatedBy: string; updatedAt: string } | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.stateResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "get_state", key }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.stateResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.stateResolvers.splice(idx, 1);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List all shared state keys and values. */
|
||||||
|
async listState(): Promise<Array<{ key: string; value: unknown; updatedBy: string; updatedAt: string }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.stateListResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "list_state" }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.stateListResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.stateListResolvers.splice(idx, 1);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Memory ---
|
||||||
|
|
||||||
|
/** Store persistent knowledge in the mesh's shared memory. */
|
||||||
|
async remember(content: string, tags?: string[]): Promise<string | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.memoryStoreResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "remember", content, tags }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.memoryStoreResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.memoryStoreResolvers.splice(idx, 1);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Search the mesh's shared memory by relevance. */
|
||||||
|
async recall(query: string): Promise<Array<{ id: string; content: string; tags: string[]; rememberedBy: string; rememberedAt: string }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.memoryRecallResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "recall", query }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.memoryRecallResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.memoryRecallResolvers.splice(idx, 1);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a memory from the mesh's shared knowledge. */
|
||||||
|
async forget(memoryId: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "forget", memoryId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check delivery status of a sent message. */
|
||||||
|
private messageStatusResolvers: Array<(result: { messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: 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 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> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.messageStatusResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "message_status", messageId }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.messageStatusResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.messageStatusResolvers.splice(idx, 1); resolve(null); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Files ---
|
||||||
|
|
||||||
|
/** Get a download URL for a shared file. */
|
||||||
|
async getFile(fileId: string): Promise<{ url: string; name: string } | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.fileUrlResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "get_file", fileId }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.fileUrlResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.fileUrlResolvers.splice(idx, 1);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List files shared in the mesh. */
|
||||||
|
async listFiles(query?: string, from?: string): Promise<Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.fileListResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "list_files", query, from }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.fileListResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.fileListResolvers.splice(idx, 1);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check who has accessed a shared file. */
|
||||||
|
async fileStatus(fileId: string): Promise<Array<{ peerName: string; accessedAt: string }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.fileStatusResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "file_status", fileId }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.fileStatusResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.fileStatusResolvers.splice(idx, 1);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete a shared file from the mesh. */
|
||||||
|
async deleteFile(fileId: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "delete_file", fileId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload a file to the broker via HTTP POST. Returns file ID or null. */
|
||||||
|
async uploadFile(filePath: string, meshId: string, memberId: string, opts: {
|
||||||
|
name?: string; tags?: string[]; persistent?: boolean; targetSpec?: string;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
const { readFileSync } = await import("node:fs");
|
||||||
|
const { basename } = await import("node:path");
|
||||||
|
const data = readFileSync(filePath);
|
||||||
|
const fileName = opts.name ?? basename(filePath);
|
||||||
|
|
||||||
|
// Convert WS broker URL to HTTP
|
||||||
|
const brokerHttp = this.mesh.brokerUrl
|
||||||
|
.replace("wss://", "https://")
|
||||||
|
.replace("ws://", "http://")
|
||||||
|
.replace("/ws", "");
|
||||||
|
|
||||||
|
const res = await fetch(`${brokerHttp}/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"X-Mesh-Id": meshId,
|
||||||
|
"X-Member-Id": memberId,
|
||||||
|
"X-File-Name": fileName,
|
||||||
|
"X-Tags": JSON.stringify(opts.tags ?? []),
|
||||||
|
"X-Persistent": String(opts.persistent ?? true),
|
||||||
|
"X-Target-Spec": opts.targetSpec ?? "",
|
||||||
|
},
|
||||||
|
body: data,
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
});
|
||||||
|
const body = await res.json() as { ok?: boolean; fileId?: string; error?: string };
|
||||||
|
if (!res.ok || !body.fileId) {
|
||||||
|
throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
return body.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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. */
|
||||||
|
onStateChange(handler: (change: { key: string; value: unknown; updatedBy: string }) => void): () => void {
|
||||||
|
this.stateChangeHandlers.add(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);
|
||||||
@@ -428,6 +892,172 @@ export class BrokerClient {
|
|||||||
})();
|
})();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === "state_result") {
|
||||||
|
const resolver = this.stateResolvers.shift();
|
||||||
|
if (resolver) {
|
||||||
|
if (msg.key) {
|
||||||
|
resolver({
|
||||||
|
key: String(msg.key),
|
||||||
|
value: msg.value,
|
||||||
|
updatedBy: String(msg.updatedBy ?? ""),
|
||||||
|
updatedAt: String(msg.updatedAt ?? ""),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolver(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "state_list") {
|
||||||
|
const entries = (msg.entries as Array<{ key: string; value: unknown; updatedBy: string; updatedAt: string }>) ?? [];
|
||||||
|
const resolver = this.stateListResolvers.shift();
|
||||||
|
if (resolver) resolver(entries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "state_change") {
|
||||||
|
const change = {
|
||||||
|
key: String(msg.key ?? ""),
|
||||||
|
value: msg.value,
|
||||||
|
updatedBy: String(msg.updatedBy ?? ""),
|
||||||
|
};
|
||||||
|
for (const h of this.stateChangeHandlers) {
|
||||||
|
try { h(change); } catch { /* handler errors are not the transport's problem */ }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "memory_stored") {
|
||||||
|
const resolver = this.memoryStoreResolvers.shift();
|
||||||
|
if (resolver) resolver(msg.id ? String(msg.id) : null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "memory_results") {
|
||||||
|
const memories = (msg.memories as Array<{ id: string; content: string; tags: string[]; rememberedBy: string; rememberedAt: string }>) ?? [];
|
||||||
|
const resolver = this.memoryRecallResolvers.shift();
|
||||||
|
if (resolver) resolver(memories);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "message_status_result") {
|
||||||
|
const resolver = this.messageStatusResolvers.shift();
|
||||||
|
if (resolver) resolver(msg as any);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "file_url") {
|
||||||
|
const resolver = this.fileUrlResolvers.shift();
|
||||||
|
if (resolver) {
|
||||||
|
if (msg.url) {
|
||||||
|
resolver({ url: String(msg.url), name: String(msg.name ?? "") });
|
||||||
|
} else {
|
||||||
|
resolver(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "file_list") {
|
||||||
|
const files = (msg.files as Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) ?? [];
|
||||||
|
const resolver = this.fileListResolvers.shift();
|
||||||
|
if (resolver) resolver(files);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "file_status_result") {
|
||||||
|
const accesses = (msg.accesses as Array<{ peerName: string; accessedAt: string }>) ?? [];
|
||||||
|
const resolver = this.fileStatusResolvers.shift();
|
||||||
|
if (resolver) resolver(accesses);
|
||||||
|
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;
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ ENV NEXT_PUBLIC_URL=$NEXT_PUBLIC_URL
|
|||||||
ENV NEXT_PUBLIC_PRODUCT_NAME=$NEXT_PUBLIC_PRODUCT_NAME
|
ENV NEXT_PUBLIC_PRODUCT_NAME=$NEXT_PUBLIC_PRODUCT_NAME
|
||||||
ENV NEXT_PUBLIC_DEFAULT_LOCALE=$NEXT_PUBLIC_DEFAULT_LOCALE
|
ENV NEXT_PUBLIC_DEFAULT_LOCALE=$NEXT_PUBLIC_DEFAULT_LOCALE
|
||||||
|
|
||||||
|
# TURBOPACK=0 forces webpack for production build — Payload CMS's
|
||||||
|
# richtext-lexical CSS imports fail under Turbopack.
|
||||||
|
ENV TURBOPACK=0
|
||||||
RUN npx turbo run build --filter=web...
|
RUN npx turbo run build --filter=web...
|
||||||
|
|
||||||
# Stage 2: runtime — standalone output only
|
# Stage 2: runtime — standalone output only
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build --no-turbopack",
|
||||||
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
|||||||
14
apps/web/src/app/(payload)/layout.tsx
Normal file
14
apps/web/src/app/(payload)/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
apps/web/src/app/(payload)/payload/[[...segments]]/page.tsx
Normal file
16
apps/web/src/app/(payload)/payload/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
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} />;
|
||||||
|
}
|
||||||
51
apps/web/src/app/(payload)/payload/importMap.js
Normal file
51
apps/web/src/app/(payload)/payload/importMap.js
Normal 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
|
||||||
|
}
|
||||||
@@ -4,27 +4,46 @@ import { Reveal, SectionIcon } from "./_reveal";
|
|||||||
|
|
||||||
const FEATURES = [
|
const FEATURES = [
|
||||||
{
|
{
|
||||||
key: "onboard",
|
key: "groups",
|
||||||
tab: "Onboarding",
|
tab: "Groups",
|
||||||
title: "Bootstrap any teammate",
|
title: "Peers self-organize through @groups",
|
||||||
body: "New hire's Claude inherits the team's context library on day one. No hand-holding, no week-long repo tour.",
|
body: "Name a group. Assign roles. Route messages to @frontend, @reviewers, or @all. The lead gathers; members contribute. No hardcoded pipelines — conventions in system prompts.",
|
||||||
|
code: `claudemesh launch --name Alice --role dev \\
|
||||||
|
--groups "frontend:lead,reviewers" -y`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "handoff",
|
key: "state",
|
||||||
tab: "Hand-offs",
|
tab: "Shared state",
|
||||||
title: "Work travels with context",
|
title: "Live facts the whole mesh can read",
|
||||||
body: "Pass an investigation to your teammate's session with full history — hypotheses, logs, files touched, commands run.",
|
body: "Set a value, every peer sees the change immediately. \"Is the deploy frozen?\" becomes a state read, not a conversation. Sprint number, PR queue, feature flags — shared operational truth.",
|
||||||
|
code: `set_state("deploy_frozen", true)
|
||||||
|
set_state("sprint", "2026-W14")
|
||||||
|
get_state("deploy_frozen") → true`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "refactor",
|
key: "memory",
|
||||||
tab: "Refactors",
|
tab: "Memory",
|
||||||
title: "Coordinate cross-cutting changes",
|
title: "The mesh gets smarter over time",
|
||||||
body: "Rename a type, rotate a secret, bump a schema — once. Every other agent picks up the change from its own repo.",
|
body: "New peers join with zero context. Memory stores institutional knowledge — decisions, incidents, lessons. Full-text searchable. Survives across sessions. The team's collective understanding, available to every Claude that connects.",
|
||||||
|
code: `remember("Payments API rate-limits at 100 req/s
|
||||||
|
after March incident", tags: ["payments"])
|
||||||
|
recall("rate limit") → ranked results`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "coordinate",
|
||||||
|
tab: "Coordination",
|
||||||
|
title: "Five patterns, zero orchestrator",
|
||||||
|
body: "Lead-gather: one lead collects from the group. Chain review: work passes through each member. Delegation: lead assigns subtasks. Voting: members set state, lead tallies. Flood: everyone responds. All through system prompts — no broker code.",
|
||||||
|
code: `send_message(to: "@frontend",
|
||||||
|
message: "auth API changed, update hooks")
|
||||||
|
send_message(to: "@pm",
|
||||||
|
message: "auth v2 done, 3 points, no blockers")`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Features = () => {
|
export const Features = () => {
|
||||||
const [active, setActive] = useState(0);
|
const [active, setActive] = useState(0);
|
||||||
|
const feature = FEATURES[active]!;
|
||||||
return (
|
return (
|
||||||
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
||||||
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
||||||
@@ -36,40 +55,19 @@ export const Features = () => {
|
|||||||
className="mx-auto max-w-4xl text-center text-[clamp(2rem,4.5vw,3.25rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
className="mx-auto max-w-4xl text-center text-[clamp(2rem,4.5vw,3.25rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
What could your mesh do?
|
What your mesh can do today
|
||||||
</h2>
|
</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={2} className="mt-10 flex justify-center">
|
<Reveal delay={2}>
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] px-4 py-3 text-[13px] text-[var(--cm-fg-secondary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
|
||||||
>
|
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
|
||||||
<span>curl -fsSL claudemesh.com/install | bash</span>
|
|
||||||
<button
|
|
||||||
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
|
||||||
aria-label="Copy"
|
|
||||||
>
|
|
||||||
copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
<Reveal delay={3}>
|
|
||||||
<p
|
<p
|
||||||
className="mt-4 text-center text-sm text-[var(--cm-fg-tertiary)]"
|
className="mx-auto mt-4 max-w-xl text-center text-sm text-[var(--cm-fg-tertiary)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
Free forever for solo developers · Or read the{" "}
|
30+ MCP tools. Groups, state, memory, messaging — all shipped.
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
|
||||||
>
|
|
||||||
documentation
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={4}>
|
<Reveal delay={3}>
|
||||||
<div className="mt-16 flex justify-center gap-2">
|
<div className="mt-12 flex flex-wrap justify-center gap-2">
|
||||||
{FEATURES.map((f, i) => (
|
{FEATURES.map((f, i) => (
|
||||||
<button
|
<button
|
||||||
key={f.key}
|
key={f.key}
|
||||||
@@ -86,20 +84,30 @@ export const Features = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-10 max-w-3xl rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-10 text-center">
|
<div className="mx-auto mt-8 max-w-3xl overflow-hidden rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]">
|
||||||
|
<div className="p-8 pb-4">
|
||||||
<h3
|
<h3
|
||||||
className="mb-4 text-[28px] font-medium leading-tight text-[var(--cm-fg)]"
|
className="mb-3 text-[24px] font-medium leading-tight text-[var(--cm-fg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
{FEATURES[active]?.title}
|
{feature.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p
|
||||||
className="text-[15px] leading-[1.65] text-[var(--cm-fg-secondary)]"
|
className="text-[14px] leading-[1.65] text-[var(--cm-fg-secondary)]"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
{FEATURES[active]?.body}
|
{feature.body}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border-t border-[var(--cm-border)] bg-[var(--cm-gray-900)] px-8 py-5">
|
||||||
|
<pre
|
||||||
|
className="text-[12px] leading-[1.7] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
<code>{feature.code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ export const Hero = () => {
|
|||||||
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
Peer mesh for Claude Code. Connect your sessions across repos and
|
Your Claude Code sessions form a team. They message each other,
|
||||||
machines. Messages are end-to-end encrypted, delivered mid-turn
|
share state, build collective memory, and self-organize through
|
||||||
as {"`<channel>`"} reminders. Your Claudes talk to each other; the
|
groups — all end-to-end encrypted. One command to launch. The broker
|
||||||
broker never sees plaintext.
|
routes ciphertext; it never reads your messages.
|
||||||
<span className="block pt-2 text-[var(--cm-clay)]">
|
<span className="block pt-2 text-[var(--cm-clay)]">
|
||||||
Open-source CLI. Free during public beta.
|
Open-source CLI. Free during public beta.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -229,31 +229,31 @@ type UseCase = {
|
|||||||
|
|
||||||
const USE_CASES: UseCase[] = [
|
const USE_CASES: UseCase[] = [
|
||||||
{
|
{
|
||||||
tag: "solo · multi-machine",
|
tag: "team · groups",
|
||||||
title: "One dev, three machines",
|
title: "Five agents, one sprint",
|
||||||
before:
|
before:
|
||||||
"Laptop, desktop, cloud dev box — each Claude session an island. You re-explain what you're doing every time you switch machines.",
|
"Each Claude works alone. When the frontend agent finishes auth, nobody tells the backend agent. You relay by hand. The PM asks for a status update; you copy-paste from three terminals.",
|
||||||
now: "Your desktop's Claude asks your laptop's Claude what it was touching. Context travels with you. The machine stops mattering.",
|
now: "Launch five sessions with --name and --groups. The @frontend lead finishes auth and messages @backend directly. The PM's Claude reads shared state: sprint number, PR queue, deploy status. Nobody relays anything.",
|
||||||
limits:
|
limits:
|
||||||
"Both peers have to be online. It shares live conversational context — not git state, not open files.",
|
"Peers must be online to receive direct messages. Group messages queue until delivery. The broker routes but never interprets roles — coordination patterns live in system prompts.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: "team · cross-repo",
|
tag: "knowledge · memory",
|
||||||
title: "Bug Alice fixed, Bob rediscovers",
|
title: "New hire's Claude knows the codebase",
|
||||||
before:
|
before:
|
||||||
"Alice in payments-api fixes a Stripe signature bug. Two weeks later, Bob in checkout-frontend hits the same thing. Alice's fix is buried in a PR thread. Bob re-solves it for three hours.",
|
"Alice in payments-api fixes a Stripe rate-limit bug. Three weeks later, a new hire hits the same wall. The fix is buried in a PR thread. They re-solve it for hours.",
|
||||||
now: "Bob's Claude asks the mesh: who's seen this? Alice's Claude volunteers with context. Bob solves in ten minutes. Alice isn't interrupted — her Claude shares the history on its own.",
|
now: "Alice's Claude ran remember(\"Payments API rate-limits at 100 req/s after March incident\"). The new hire's Claude runs recall(\"rate limit\") and gets ranked results. Ten minutes, not three hours.",
|
||||||
limits:
|
limits:
|
||||||
"Each Claude stays inside its own repo. Nobody's reading anyone else's files. Information flows at the agent layer, with a human still on the PR.",
|
"Memory stores text, not code diffs. Each Claude stays inside its own repo. Knowledge flows at the agent layer — the human still reviews the PR.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: "mobile · oversight",
|
tag: "coordination · state",
|
||||||
title: "CI fails at 3am",
|
title: "\"Is the deploy frozen?\" answered in zero messages",
|
||||||
before:
|
before:
|
||||||
"Alert on your phone. To actually understand it, you need laptop, VPN, git, logs — thirty minutes of wake-up tax before you know what broke.",
|
"You ask in Slack. Someone answers twenty minutes later. Meanwhile two PRs merge. The deploy breaks. Nobody knew it was frozen.",
|
||||||
now: "WhatsApp gateway peer forwards the alert. You ask the ops-server Claude what triggered it. It answers. You say roll it back. Done from bed.",
|
now: "set_state(\"deploy_frozen\", true). Every peer sees the change instantly. get_state(\"deploy_frozen\") returns true. No conversation needed. Shared operational facts, not shared opinions.",
|
||||||
limits:
|
limits:
|
||||||
"The WhatsApp/phone gateway is on the v0.2 roadmap — the protocol is ready, the bot isn't shipped yet. Someone could build it in a weekend.",
|
"State is operational — it lives as long as the mesh. Use memory for permanent knowledge. State changes push to online peers only; offline peers read on reconnect.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,61 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- claudemesh-internal
|
- claudemesh-internal
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- minio-data:/data
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: claudemesh
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-changeme}
|
||||||
|
expose:
|
||||||
|
- "9000"
|
||||||
|
networks:
|
||||||
|
- claudemesh-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mc", "ready", "local"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 10s
|
||||||
|
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
|
||||||
@@ -40,11 +95,26 @@ services:
|
|||||||
MAX_CONNECTIONS_PER_MESH: ${MAX_CONNECTIONS_PER_MESH:-100}
|
MAX_CONNECTIONS_PER_MESH: ${MAX_CONNECTIONS_PER_MESH:-100}
|
||||||
MAX_MESSAGE_BYTES: ${MAX_MESSAGE_BYTES:-65536}
|
MAX_MESSAGE_BYTES: ${MAX_MESSAGE_BYTES:-65536}
|
||||||
HOOK_RATE_LIMIT_PER_MIN: ${HOOK_RATE_LIMIT_PER_MIN:-30}
|
HOOK_RATE_LIMIT_PER_MIN: ${HOOK_RATE_LIMIT_PER_MIN:-30}
|
||||||
|
MINIO_ENDPOINT: minio:9000
|
||||||
|
MINIO_ACCESS_KEY: claudemesh
|
||||||
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-changeme}
|
||||||
|
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:
|
||||||
- coolify
|
- coolify
|
||||||
- claudemesh-internal
|
- claudemesh-internal
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
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
|
||||||
@@ -85,6 +155,11 @@ services:
|
|||||||
start_period: 20s
|
start_period: 20s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
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
|
||||||
coolify:
|
coolify:
|
||||||
|
|||||||
27
packages/db/migrations/0008_add-state-and-memory.sql
Normal file
27
packages/db/migrations/0008_add-state-and-memory.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE "mesh"."memory" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"mesh_id" text NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"tags" text[] DEFAULT '{}',
|
||||||
|
"remembered_by" text,
|
||||||
|
"remembered_by_name" text,
|
||||||
|
"remembered_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"forgotten_at" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "mesh"."state" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"mesh_id" text NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"value" jsonb NOT NULL,
|
||||||
|
"updated_by_presence" text,
|
||||||
|
"updated_by_name" text,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."memory" ADD CONSTRAINT "memory_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."memory" ADD CONSTRAINT "memory_remembered_by_member_id_fk" FOREIGN KEY ("remembered_by") REFERENCES "mesh"."member"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."state" ADD CONSTRAINT "state_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "state_mesh_key_idx" ON "mesh"."state" USING btree ("mesh_id","key");--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."memory" ADD COLUMN IF NOT EXISTS "search_vector" tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED;--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "memory_search_idx" ON "mesh"."memory" USING gin("search_vector");
|
||||||
28
packages/db/migrations/0009_add-file-tables.sql
Normal file
28
packages/db/migrations/0009_add-file-tables.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE "mesh"."file" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"mesh_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"size_bytes" integer NOT NULL,
|
||||||
|
"mime_type" text,
|
||||||
|
"minio_key" text NOT NULL,
|
||||||
|
"tags" text[] DEFAULT '{}',
|
||||||
|
"persistent" boolean DEFAULT true NOT NULL,
|
||||||
|
"uploaded_by_name" text,
|
||||||
|
"uploaded_by_member" text,
|
||||||
|
"target_spec" text,
|
||||||
|
"uploaded_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"expires_at" timestamp,
|
||||||
|
"deleted_at" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "mesh"."file_access" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"file_id" text NOT NULL,
|
||||||
|
"peer_session_pubkey" text,
|
||||||
|
"peer_name" text,
|
||||||
|
"accessed_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_uploaded_by_member_member_id_fk" FOREIGN KEY ("uploaded_by_member") REFERENCES "mesh"."member"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."file_access" ADD CONSTRAINT "file_access_file_id_file_id_fk" FOREIGN KEY ("file_id") REFERENCES "mesh"."file"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
33
packages/db/migrations/0010_add-context-and-tasks.sql
Normal file
33
packages/db/migrations/0010_add-context-and-tasks.sql
Normal 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;
|
||||||
10
packages/db/migrations/0011_add-streams.sql
Normal file
10
packages/db/migrations/0011_add-streams.sql
Normal 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");
|
||||||
3049
packages/db/migrations/meta/0008_snapshot.json
Normal file
3049
packages/db/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3237
packages/db/migrations/meta/0009_snapshot.json
Normal file
3237
packages/db/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3467
packages/db/migrations/meta/0010_snapshot.json
Normal file
3467
packages/db/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3548
packages/db/migrations/meta/0011_snapshot.json
Normal file
3548
packages/db/migrations/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,34 @@
|
|||||||
"when": 1775476994511,
|
"when": 1775476994511,
|
||||||
"tag": "0007_add-presence-groups",
|
"tag": "0007_add-presence-groups",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1775477883426,
|
||||||
|
"tag": "0008_add-state-and-memory",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1775480008546,
|
||||||
|
"tag": "0009_add-file-tables",
|
||||||
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
boolean,
|
||||||
integer,
|
integer,
|
||||||
jsonb,
|
jsonb,
|
||||||
pgSchema,
|
pgSchema,
|
||||||
timestamp,
|
timestamp,
|
||||||
text,
|
text,
|
||||||
|
uniqueIndex,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
import { generateId } from "@turbostarter/shared/utils";
|
import { generateId } from "@turbostarter/shared/utils";
|
||||||
@@ -251,6 +253,142 @@ export const pendingStatus = meshSchema.table("pending_status", {
|
|||||||
appliedAt: timestamp(),
|
appliedAt: timestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared key-value state scoped to a mesh. Any peer can read/write.
|
||||||
|
* Changes push to all connected peers in real time.
|
||||||
|
*/
|
||||||
|
export const meshState = meshSchema.table(
|
||||||
|
"state",
|
||||||
|
{
|
||||||
|
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||||
|
meshId: text()
|
||||||
|
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
key: text().notNull(),
|
||||||
|
value: jsonb().notNull(),
|
||||||
|
updatedByPresence: text(),
|
||||||
|
updatedByName: text(),
|
||||||
|
updatedAt: timestamp().defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex("state_mesh_key_idx").on(table.meshId, table.key)],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistent shared memory for a mesh. Full-text searchable via a
|
||||||
|
* tsvector generated column + GIN index added in raw SQL migration.
|
||||||
|
*/
|
||||||
|
export const meshMemory = meshSchema.table("memory", {
|
||||||
|
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||||
|
meshId: text()
|
||||||
|
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
content: text().notNull(),
|
||||||
|
tags: text().array().default([]),
|
||||||
|
rememberedBy: text().references(() => meshMember.id),
|
||||||
|
rememberedByName: text(),
|
||||||
|
rememberedAt: timestamp().defaultNow().notNull(),
|
||||||
|
forgottenAt: timestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File metadata for shared files in a mesh. Actual bytes live in MinIO;
|
||||||
|
* this table tracks ownership, access control, and soft-deletion.
|
||||||
|
*/
|
||||||
|
export const meshFile = meshSchema.table("file", {
|
||||||
|
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||||
|
meshId: text()
|
||||||
|
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
name: text().notNull(),
|
||||||
|
sizeBytes: integer().notNull(),
|
||||||
|
mimeType: text(),
|
||||||
|
minioKey: text().notNull(),
|
||||||
|
tags: text().array().default([]),
|
||||||
|
persistent: boolean().notNull().default(true),
|
||||||
|
uploadedByName: text(),
|
||||||
|
uploadedByMember: text().references(() => meshMember.id),
|
||||||
|
targetSpec: text(), // null = entire mesh
|
||||||
|
uploadedAt: timestamp().defaultNow().notNull(),
|
||||||
|
expiresAt: timestamp(),
|
||||||
|
deletedAt: timestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access log for file downloads. Tracks which peer accessed which file
|
||||||
|
* and when, for auditability and read-receipt semantics.
|
||||||
|
*/
|
||||||
|
export const meshFileAccess = meshSchema.table("file_access", {
|
||||||
|
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||||
|
fileId: text()
|
||||||
|
.references(() => meshFile.id, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
peerSessionPubkey: text(),
|
||||||
|
peerName: text(),
|
||||||
|
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],
|
||||||
@@ -311,6 +449,43 @@ export const auditLogRelations = relations(auditLog, ({ one }) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const meshStateRelations = relations(meshState, ({ one }) => ({
|
||||||
|
mesh: one(mesh, {
|
||||||
|
fields: [meshState.meshId],
|
||||||
|
references: [mesh.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const meshMemoryRelations = relations(meshMemory, ({ one }) => ({
|
||||||
|
mesh: one(mesh, {
|
||||||
|
fields: [meshMemory.meshId],
|
||||||
|
references: [mesh.id],
|
||||||
|
}),
|
||||||
|
member: one(meshMember, {
|
||||||
|
fields: [meshMemory.rememberedBy],
|
||||||
|
references: [meshMember.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const meshFileRelations = relations(meshFile, ({ one, many }) => ({
|
||||||
|
mesh: one(mesh, {
|
||||||
|
fields: [meshFile.meshId],
|
||||||
|
references: [mesh.id],
|
||||||
|
}),
|
||||||
|
uploader: one(meshMember, {
|
||||||
|
fields: [meshFile.uploadedByMember],
|
||||||
|
references: [meshMember.id],
|
||||||
|
}),
|
||||||
|
accesses: many(meshFileAccess),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const meshFileAccessRelations = relations(meshFileAccess, ({ one }) => ({
|
||||||
|
file: one(meshFile, {
|
||||||
|
fields: [meshFileAccess.fileId],
|
||||||
|
references: [meshFile.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
export const selectMeshSchema = createSelectSchema(mesh);
|
export const selectMeshSchema = createSelectSchema(mesh);
|
||||||
export const insertMeshSchema = createInsertSchema(mesh);
|
export const insertMeshSchema = createInsertSchema(mesh);
|
||||||
export const selectMemberSchema = createSelectSchema(meshMember);
|
export const selectMemberSchema = createSelectSchema(meshMember);
|
||||||
@@ -340,3 +515,61 @@ export type SelectMessageQueue = typeof messageQueue.$inferSelect;
|
|||||||
export type InsertMessageQueue = typeof messageQueue.$inferInsert;
|
export type InsertMessageQueue = typeof messageQueue.$inferInsert;
|
||||||
export type SelectPendingStatus = typeof pendingStatus.$inferSelect;
|
export type SelectPendingStatus = typeof pendingStatus.$inferSelect;
|
||||||
export type InsertPendingStatus = typeof pendingStatus.$inferInsert;
|
export type InsertPendingStatus = typeof pendingStatus.$inferInsert;
|
||||||
|
export const selectMeshStateSchema = createSelectSchema(meshState);
|
||||||
|
export const insertMeshStateSchema = createInsertSchema(meshState);
|
||||||
|
export const selectMeshMemorySchema = createSelectSchema(meshMemory);
|
||||||
|
export const insertMeshMemorySchema = createInsertSchema(meshMemory);
|
||||||
|
export type SelectMeshState = typeof meshState.$inferSelect;
|
||||||
|
export type InsertMeshState = typeof meshState.$inferInsert;
|
||||||
|
export type SelectMeshMemory = typeof meshMemory.$inferSelect;
|
||||||
|
export type InsertMeshMemory = typeof meshMemory.$inferInsert;
|
||||||
|
export const selectMeshFileSchema = createSelectSchema(meshFile);
|
||||||
|
export const insertMeshFileSchema = createInsertSchema(meshFile);
|
||||||
|
export const selectMeshFileAccessSchema = createSelectSchema(meshFileAccess);
|
||||||
|
export const insertMeshFileAccessSchema = createInsertSchema(meshFileAccess);
|
||||||
|
export type SelectMeshFile = typeof meshFile.$inferSelect;
|
||||||
|
export type InsertMeshFile = typeof meshFile.$inferInsert;
|
||||||
|
export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect;
|
||||||
|
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;
|
||||||
|
|||||||
246
pnpm-lock.yaml
generated
246
pnpm-lock.yaml
generated
@@ -111,6 +111,9 @@ importers:
|
|||||||
|
|
||||||
apps/broker:
|
apps/broker:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@qdrant/js-client-rest':
|
||||||
|
specifier: 1.17.0
|
||||||
|
version: 1.17.0(typescript@5.9.3)
|
||||||
'@turbostarter/db':
|
'@turbostarter/db':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/db
|
version: link:../../packages/db
|
||||||
@@ -123,6 +126,12 @@ importers:
|
|||||||
libsodium-wrappers:
|
libsodium-wrappers:
|
||||||
specifier: 0.7.15
|
specifier: 0.7.15
|
||||||
version: 0.7.15
|
version: 0.7.15
|
||||||
|
minio:
|
||||||
|
specifier: 8.0.7
|
||||||
|
version: 8.0.7
|
||||||
|
neo4j-driver:
|
||||||
|
specifier: 6.0.1
|
||||||
|
version: 6.0.1
|
||||||
ws:
|
ws:
|
||||||
specifier: 8.20.0
|
specifier: 8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
@@ -4582,6 +4591,16 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.8
|
'@opentelemetry/api': ^1.8
|
||||||
|
|
||||||
|
'@qdrant/js-client-rest@1.17.0':
|
||||||
|
resolution: {integrity: sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==}
|
||||||
|
engines: {node: '>=18.17.0', pnpm: '>=8'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.7'
|
||||||
|
|
||||||
|
'@qdrant/openapi-typescript-fetch@1.2.6':
|
||||||
|
resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==}
|
||||||
|
engines: {node: '>=18.0.0', pnpm: '>=8'}
|
||||||
|
|
||||||
'@radix-ui/colors@3.0.0':
|
'@radix-ui/colors@3.0.0':
|
||||||
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
|
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
|
||||||
|
|
||||||
@@ -7843,6 +7862,9 @@ packages:
|
|||||||
async-limiter@1.0.1:
|
async-limiter@1.0.1:
|
||||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||||
|
|
||||||
|
async@3.2.6:
|
||||||
|
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||||
|
|
||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
@@ -8034,6 +8056,9 @@ packages:
|
|||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
|
block-stream2@2.1.0:
|
||||||
|
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -8068,6 +8093,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
browser-or-node@2.1.1:
|
||||||
|
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.1:
|
||||||
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -8079,12 +8107,19 @@ packages:
|
|||||||
bson-objectid@2.0.4:
|
bson-objectid@2.0.4:
|
||||||
resolution: {integrity: sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==}
|
resolution: {integrity: sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==}
|
||||||
|
|
||||||
|
buffer-crc32@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
busboy@1.6.0:
|
busboy@1.6.0:
|
||||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||||
engines: {node: '>=10.16.0'}
|
engines: {node: '>=10.16.0'}
|
||||||
@@ -9271,6 +9306,9 @@ packages:
|
|||||||
eventemitter3@4.0.7:
|
eventemitter3@4.0.7:
|
||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4:
|
||||||
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
events@3.3.0:
|
events@3.3.0:
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
@@ -9486,10 +9524,17 @@ packages:
|
|||||||
fast-uri@3.0.6:
|
fast-uri@3.0.6:
|
||||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||||
|
|
||||||
|
fast-xml-builder@1.1.4:
|
||||||
|
resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==}
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
fast-xml-parser@5.5.10:
|
||||||
|
resolution: {integrity: sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||||
|
|
||||||
@@ -11220,6 +11265,10 @@ packages:
|
|||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
|
minio@8.0.7:
|
||||||
|
resolution: {integrity: sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==}
|
||||||
|
engines: {node: ^16 || ^18 || >=20}
|
||||||
|
|
||||||
minipass@7.1.2:
|
minipass@7.1.2:
|
||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
@@ -11329,6 +11378,16 @@ packages:
|
|||||||
neo-async@2.6.2:
|
neo-async@2.6.2:
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
|
|
||||||
|
neo4j-driver-bolt-connection@6.0.1:
|
||||||
|
resolution: {integrity: sha512-1KyG73TO+CwnYJisdHD0sjUw9yR+P5q3JFcmVPzsHT4/whzCjuXSMpmY4jZcHH2PdY2cBUq4l/6WcDiPMxW2UA==}
|
||||||
|
|
||||||
|
neo4j-driver-core@6.0.1:
|
||||||
|
resolution: {integrity: sha512-5I2KxICAvcHxnWdJyDqwu8PBAQvWVTlQH2ve3VQmtVdJScPqWhpXN1PiX5IIl+cRF3pFpz9GQF53B5n6s0QQUQ==}
|
||||||
|
|
||||||
|
neo4j-driver@6.0.1:
|
||||||
|
resolution: {integrity: sha512-8DDF2MwEJNz7y7cp97x4u8fmVIP4CWS8qNBxdwxTG0fWtsS+2NdeC+7uXwmmuFOpHvkfXqv63uWY73bfDtOH8Q==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
nested-error-stacks@2.0.1:
|
nested-error-stacks@2.0.1:
|
||||||
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
|
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
|
||||||
|
|
||||||
@@ -11717,6 +11776,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
path-expression-matcher@1.2.1:
|
||||||
|
resolution: {integrity: sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
path-is-absolute@1.0.1:
|
path-is-absolute@1.0.1:
|
||||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -13111,6 +13174,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
|
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
|
||||||
|
stream-chain@2.2.5:
|
||||||
|
resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==}
|
||||||
|
|
||||||
|
stream-json@1.9.1:
|
||||||
|
resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
|
||||||
|
|
||||||
streamsearch@1.1.0:
|
streamsearch@1.1.0:
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -13208,6 +13277,9 @@ packages:
|
|||||||
strnum@2.1.1:
|
strnum@2.1.1:
|
||||||
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
||||||
|
|
||||||
|
strnum@2.2.2:
|
||||||
|
resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==}
|
||||||
|
|
||||||
strtok3@10.3.5:
|
strtok3@10.3.5:
|
||||||
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -13379,6 +13451,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
|
|
||||||
through@2.3.8:
|
through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
|
|
||||||
@@ -13644,8 +13719,8 @@ packages:
|
|||||||
undici-types@7.8.0:
|
undici-types@7.8.0:
|
||||||
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||||
|
|
||||||
undici@6.21.3:
|
undici@6.24.1:
|
||||||
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
undici@7.24.4:
|
undici@7.24.4:
|
||||||
@@ -14173,6 +14248,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
|
resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
xmlbuilder@11.0.1:
|
xmlbuilder@11.0.1:
|
||||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -16649,7 +16728,7 @@ snapshots:
|
|||||||
structured-headers: 0.4.1
|
structured-headers: 0.4.1
|
||||||
tar: 7.5.2
|
tar: 7.5.2
|
||||||
terminal-link: 2.1.1
|
terminal-link: 2.1.1
|
||||||
undici: 6.21.3
|
undici: 6.24.1
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
ws: 8.20.0
|
ws: 8.20.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -18379,6 +18458,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@qdrant/js-client-rest@1.17.0(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||||
|
typescript: 5.9.3
|
||||||
|
undici: 6.24.1
|
||||||
|
|
||||||
|
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||||
|
|
||||||
'@radix-ui/colors@3.0.0': {}
|
'@radix-ui/colors@3.0.0': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
@@ -20879,7 +20966,7 @@ snapshots:
|
|||||||
'@sentry/bundler-plugin-core': 4.6.1(encoding@0.1.13)
|
'@sentry/bundler-plugin-core': 4.6.1(encoding@0.1.13)
|
||||||
unplugin: 1.0.1
|
unplugin: 1.0.1
|
||||||
uuid: 9.0.1
|
uuid: 9.0.1
|
||||||
webpack: 5.100.2
|
webpack: 5.100.2(esbuild@0.25.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -22249,7 +22336,7 @@ snapshots:
|
|||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
acorn-import-attributes@1.9.5(acorn@8.16.0):
|
acorn-import-attributes@1.9.5(acorn@8.16.0):
|
||||||
@@ -22491,6 +22578,8 @@ snapshots:
|
|||||||
|
|
||||||
async-limiter@1.0.1: {}
|
async-limiter@1.0.1: {}
|
||||||
|
|
||||||
|
async@3.2.6: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
@@ -22724,6 +22813,10 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
block-stream2@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -22772,6 +22865,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
|
|
||||||
|
browser-or-node@2.1.1: {}
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001727
|
caniuse-lite: 1.0.30001727
|
||||||
@@ -22785,6 +22880,8 @@ snapshots:
|
|||||||
|
|
||||||
bson-objectid@2.0.4: {}
|
bson-objectid@2.0.4: {}
|
||||||
|
|
||||||
|
buffer-crc32@1.0.0: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
@@ -22792,6 +22889,11 @@ snapshots:
|
|||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
busboy@1.6.0:
|
busboy@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
streamsearch: 1.1.0
|
streamsearch: 1.1.0
|
||||||
@@ -23356,8 +23458,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
|
|
||||||
decode-uri-component@0.2.2:
|
decode-uri-component@0.2.2: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -24095,6 +24196,8 @@ snapshots:
|
|||||||
|
|
||||||
eventemitter3@4.0.7: {}
|
eventemitter3@4.0.7: {}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
eventsource-parser@3.0.6: {}
|
eventsource-parser@3.0.6: {}
|
||||||
@@ -24385,10 +24488,20 @@ snapshots:
|
|||||||
|
|
||||||
fast-uri@3.0.6: {}
|
fast-uri@3.0.6: {}
|
||||||
|
|
||||||
|
fast-xml-builder@1.1.4:
|
||||||
|
dependencies:
|
||||||
|
path-expression-matcher: 1.2.1
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 2.1.1
|
strnum: 2.1.1
|
||||||
|
|
||||||
|
fast-xml-parser@5.5.10:
|
||||||
|
dependencies:
|
||||||
|
fast-xml-builder: 1.1.4
|
||||||
|
path-expression-matcher: 1.2.1
|
||||||
|
strnum: 2.2.2
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.1.0
|
reusify: 1.1.0
|
||||||
@@ -24454,8 +24567,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
|
|
||||||
filter-obj@1.1.0:
|
filter-obj@1.1.0: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
finalhandler@1.1.2:
|
finalhandler@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -26613,6 +26725,22 @@ snapshots:
|
|||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
|
minio@8.0.7:
|
||||||
|
dependencies:
|
||||||
|
async: 3.2.6
|
||||||
|
block-stream2: 2.1.0
|
||||||
|
browser-or-node: 2.1.1
|
||||||
|
buffer-crc32: 1.0.0
|
||||||
|
eventemitter3: 5.0.4
|
||||||
|
fast-xml-parser: 5.5.10
|
||||||
|
ipaddr.js: 2.2.0
|
||||||
|
lodash: 4.17.21
|
||||||
|
mime-types: 2.1.35
|
||||||
|
query-string: 7.1.3
|
||||||
|
stream-json: 1.9.1
|
||||||
|
through2: 4.0.2
|
||||||
|
xml2js: 0.6.2
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
minizlib@3.1.0:
|
minizlib@3.1.0:
|
||||||
@@ -26704,6 +26832,20 @@ snapshots:
|
|||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
|
neo4j-driver-bolt-connection@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
buffer: 6.0.3
|
||||||
|
neo4j-driver-core: 6.0.1
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
|
||||||
|
neo4j-driver-core@6.0.1: {}
|
||||||
|
|
||||||
|
neo4j-driver@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
neo4j-driver-bolt-connection: 6.0.1
|
||||||
|
neo4j-driver-core: 6.0.1
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
nested-error-stacks@2.0.1:
|
nested-error-stacks@2.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -26813,7 +26955,7 @@ snapshots:
|
|||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
styled-jsx: 5.1.6(react@19.2.3)
|
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 16.2.2
|
'@next/swc-darwin-arm64': 16.2.2
|
||||||
'@next/swc-darwin-x64': 16.2.2
|
'@next/swc-darwin-x64': 16.2.2
|
||||||
@@ -27246,6 +27388,8 @@ snapshots:
|
|||||||
|
|
||||||
path-exists@5.0.0: {}
|
path-exists@5.0.0: {}
|
||||||
|
|
||||||
|
path-expression-matcher@1.2.1: {}
|
||||||
|
|
||||||
path-is-absolute@1.0.1: {}
|
path-is-absolute@1.0.1: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
@@ -27712,7 +27856,6 @@ snapshots:
|
|||||||
filter-obj: 1.1.0
|
filter-obj: 1.1.0
|
||||||
split-on-first: 1.1.0
|
split-on-first: 1.1.0
|
||||||
strict-uri-encode: 2.0.0
|
strict-uri-encode: 2.0.0
|
||||||
optional: true
|
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
@@ -28714,8 +28857,7 @@ snapshots:
|
|||||||
immutable: 4.3.8
|
immutable: 4.3.8
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
sax@1.4.1:
|
sax@1.4.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
saxes@6.0.0:
|
saxes@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -29133,8 +29275,7 @@ snapshots:
|
|||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
split-on-first@1.1.0:
|
split-on-first@1.1.0: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
split2@4.2.0: {}
|
split2@4.2.0: {}
|
||||||
|
|
||||||
@@ -29179,10 +29320,15 @@ snapshots:
|
|||||||
stream-buffers@2.2.0:
|
stream-buffers@2.2.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
stream-chain@2.2.5: {}
|
||||||
|
|
||||||
|
stream-json@1.9.1:
|
||||||
|
dependencies:
|
||||||
|
stream-chain: 2.2.5
|
||||||
|
|
||||||
streamsearch@1.1.0: {}
|
streamsearch@1.1.0: {}
|
||||||
|
|
||||||
strict-uri-encode@2.0.0:
|
strict-uri-encode@2.0.0: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -29294,6 +29440,8 @@ snapshots:
|
|||||||
|
|
||||||
strnum@2.1.1: {}
|
strnum@2.1.1: {}
|
||||||
|
|
||||||
|
strnum@2.2.2: {}
|
||||||
|
|
||||||
strtok3@10.3.5:
|
strtok3@10.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
@@ -29329,12 +29477,6 @@ snapshots:
|
|||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
optional: true
|
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.2.3):
|
|
||||||
dependencies:
|
|
||||||
client-only: 0.0.1
|
|
||||||
react: 19.2.3
|
|
||||||
|
|
||||||
styleq@0.1.3:
|
styleq@0.1.3:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -29492,15 +29634,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
esbuild: 0.25.0
|
esbuild: 0.25.0
|
||||||
|
|
||||||
terser-webpack-plugin@5.3.14(webpack@5.100.2):
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
jest-worker: 27.5.1
|
|
||||||
schema-utils: 4.3.2
|
|
||||||
serialize-javascript: 6.0.2
|
|
||||||
terser: 5.43.1
|
|
||||||
webpack: 5.100.2
|
|
||||||
|
|
||||||
terser@5.43.1:
|
terser@5.43.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/source-map': 0.3.10
|
'@jridgewell/source-map': 0.3.10
|
||||||
@@ -29532,6 +29665,10 @@ snapshots:
|
|||||||
|
|
||||||
throttleit@2.1.0: {}
|
throttleit@2.1.0: {}
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
@@ -29723,7 +29860,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.2
|
||||||
|
|
||||||
typed-array-buffer@1.0.3:
|
typed-array-buffer@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -29794,8 +29931,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.8.0: {}
|
undici-types@7.8.0: {}
|
||||||
|
|
||||||
undici@6.21.3:
|
undici@6.24.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
undici@7.24.4: {}
|
undici@7.24.4: {}
|
||||||
|
|
||||||
@@ -30200,38 +30336,6 @@ snapshots:
|
|||||||
|
|
||||||
webpack-virtual-modules@0.5.0: {}
|
webpack-virtual-modules@0.5.0: {}
|
||||||
|
|
||||||
webpack@5.100.2:
|
|
||||||
dependencies:
|
|
||||||
'@types/eslint-scope': 3.7.7
|
|
||||||
'@types/estree': 1.0.8
|
|
||||||
'@types/json-schema': 7.0.15
|
|
||||||
'@webassemblyjs/ast': 1.14.1
|
|
||||||
'@webassemblyjs/wasm-edit': 1.14.1
|
|
||||||
'@webassemblyjs/wasm-parser': 1.14.1
|
|
||||||
acorn: 8.16.0
|
|
||||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
|
||||||
browserslist: 4.25.1
|
|
||||||
chrome-trace-event: 1.0.4
|
|
||||||
enhanced-resolve: 5.18.3
|
|
||||||
es-module-lexer: 1.7.0
|
|
||||||
eslint-scope: 5.1.1
|
|
||||||
events: 3.3.0
|
|
||||||
glob-to-regexp: 0.4.1
|
|
||||||
graceful-fs: 4.2.11
|
|
||||||
json-parse-even-better-errors: 2.3.1
|
|
||||||
loader-runner: 4.3.0
|
|
||||||
mime-types: 2.1.35
|
|
||||||
neo-async: 2.6.2
|
|
||||||
schema-utils: 4.3.2
|
|
||||||
tapable: 2.2.2
|
|
||||||
terser-webpack-plugin: 5.3.14(webpack@5.100.2)
|
|
||||||
watchpack: 2.4.4
|
|
||||||
webpack-sources: 3.3.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@swc/core'
|
|
||||||
- esbuild
|
|
||||||
- uglify-js
|
|
||||||
|
|
||||||
webpack@5.100.2(esbuild@0.25.0):
|
webpack@5.100.2(esbuild@0.25.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint-scope': 3.7.7
|
'@types/eslint-scope': 3.7.7
|
||||||
@@ -30406,8 +30510,12 @@ snapshots:
|
|||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
xmlbuilder@11.0.1:
|
xml2js@0.6.2:
|
||||||
optional: true
|
dependencies:
|
||||||
|
sax: 1.4.1
|
||||||
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1: {}
|
||||||
|
|
||||||
xmlbuilder@15.1.1:
|
xmlbuilder@15.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|||||||
Reference in New Issue
Block a user