Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b4e389f2b | ||
|
|
7a5f786e0c | ||
|
|
10e5fdcfd1 | ||
|
|
cc6e56aef9 |
180
SPEC.md
180
SPEC.md
@@ -563,9 +563,166 @@ Under 2000 tokens. Tool reference only — no behavioral scripts. Claude adapts
|
||||
| `file_status(id)` | Who accessed this file |
|
||||
| `delete_file(id)` | Remove a shared file |
|
||||
|
||||
### Vectors
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `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
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `graph_query(cypher)` | Run a read query on the per-mesh Neo4j database |
|
||||
| `graph_execute(cypher)` | Run a write query (CREATE, MERGE, DELETE) |
|
||||
|
||||
### Context
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `share_context(summary, files_read?, key_findings?, tags?)` | Share session understanding with the mesh |
|
||||
| `get_context(query)` | Find context from peers who explored an area |
|
||||
| `list_contexts()` | See what all peers currently know |
|
||||
|
||||
### Tasks
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `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 |
|
||||
|
||||
### Mesh Database
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `mesh_query(sql)` | Run a SELECT on the per-mesh PostgreSQL schema |
|
||||
| `mesh_execute(sql)` | Run DDL/DML (CREATE TABLE, INSERT, UPDATE) |
|
||||
| `mesh_schema()` | List tables and columns in the mesh database |
|
||||
|
||||
### Streams
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `create_stream(name)` | Create a real-time data stream |
|
||||
| `publish(stream, data)` | Push data to a stream |
|
||||
| `subscribe(stream)` | Receive stream data as push notifications |
|
||||
| `list_streams()` | List active streams in this mesh |
|
||||
|
||||
---
|
||||
|
||||
## 8. Encryption
|
||||
## 9. Shared Infrastructure
|
||||
|
||||
The broker provisions infrastructure per mesh. Services run in docker-compose on the internal network. Peers interact through MCP tools — they never configure infrastructure directly.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Broker (coordinator)
|
||||
├── PostgreSQL ← state, memory, tasks, context, mesh databases
|
||||
├── MinIO ← files
|
||||
├── Qdrant ← vector embeddings
|
||||
└── Neo4j ← entity graphs
|
||||
```
|
||||
|
||||
All auto-provisioned. First `vector_store` call creates the Qdrant collection. First `mesh_execute(CREATE TABLE...)` creates the schema. First `share_file` creates the MinIO bucket. Zero setup.
|
||||
|
||||
### Docker Compose additions
|
||||
|
||||
```yaml
|
||||
services:
|
||||
qdrant:
|
||||
image: qdrant/qdrant
|
||||
restart: always
|
||||
volumes: [qdrant-data:/qdrant/storage]
|
||||
expose: ["6333"]
|
||||
networks: [claudemesh-internal]
|
||||
|
||||
neo4j:
|
||||
image: neo4j:5
|
||||
restart: always
|
||||
environment:
|
||||
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-changeme}
|
||||
volumes: [neo4j-data:/data]
|
||||
expose: ["7687"]
|
||||
networks: [claudemesh-internal]
|
||||
```
|
||||
|
||||
### Per-mesh isolation
|
||||
|
||||
| Service | Isolation method |
|
||||
|---------|-----------------|
|
||||
| PostgreSQL | Schema per mesh: `mesh_{meshId}` |
|
||||
| MinIO | Bucket per mesh: `mesh-{meshId}` |
|
||||
| Qdrant | Collection per mesh: `mesh_{meshId}_{name}` |
|
||||
| Neo4j | Database per mesh: `mesh_{meshId}` |
|
||||
|
||||
### DB schema additions
|
||||
|
||||
```sql
|
||||
mesh.context (
|
||||
id text PK,
|
||||
mesh_id text FK,
|
||||
presence_id text FK,
|
||||
peer_name text,
|
||||
summary text NOT NULL,
|
||||
files_read text[] DEFAULT '{}',
|
||||
key_findings text[] DEFAULT '{}',
|
||||
tags text[] DEFAULT '{}',
|
||||
updated_at timestamp DEFAULT NOW()
|
||||
);
|
||||
|
||||
mesh.task (
|
||||
id text PK,
|
||||
mesh_id text FK,
|
||||
title text NOT NULL,
|
||||
assignee text,
|
||||
claimed_by_name text,
|
||||
claimed_by_presence text FK,
|
||||
priority text DEFAULT 'normal',
|
||||
status text DEFAULT 'open',
|
||||
tags text[] DEFAULT '{}',
|
||||
result text,
|
||||
created_by_name text,
|
||||
created_at timestamp DEFAULT NOW(),
|
||||
claimed_at timestamp,
|
||||
completed_at timestamp
|
||||
);
|
||||
|
||||
mesh.stream (
|
||||
id text PK,
|
||||
mesh_id text FK,
|
||||
name text NOT NULL,
|
||||
created_by_name text,
|
||||
created_at timestamp DEFAULT NOW(),
|
||||
UNIQUE(mesh_id, name)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. What peers share — the full picture
|
||||
|
||||
| Layer | Service | What | Lifetime |
|
||||
|-------|---------|------|----------|
|
||||
| Messages | Broker WS | Text conversations | Ephemeral (queue until delivered) |
|
||||
| State | PostgreSQL | Live coordination facts | Mesh lifetime |
|
||||
| Memory | PostgreSQL + tsvector | Institutional knowledge | Permanent |
|
||||
| Context | PostgreSQL | Session understanding | Session lifetime |
|
||||
| Files | MinIO | Binary artifacts | Persistent or 24h ephemeral |
|
||||
| Tasks | PostgreSQL | Work items + ownership | Until completed/deleted |
|
||||
| Vectors | Qdrant | Semantic embeddings | Persistent |
|
||||
| Graph | Neo4j | Entity relationships | Persistent |
|
||||
| Databases | PostgreSQL schemas | Structured data | Persistent |
|
||||
| Streams | Broker pub/sub | Real-time data feeds | Session lifetime |
|
||||
|
||||
---
|
||||
|
||||
## 11. Encryption
|
||||
|
||||
### Direct messages
|
||||
|
||||
@@ -585,7 +742,7 @@ The session keypair generates once on first connect and survives reconnects. Mes
|
||||
|
||||
---
|
||||
|
||||
## 9. Production hardening (implemented)
|
||||
## 12. Production hardening (implemented)
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
@@ -600,7 +757,7 @@ The session keypair generates once on first connect and survives reconnects. Mes
|
||||
|
||||
---
|
||||
|
||||
## 10. CLI commands
|
||||
## 13. CLI commands
|
||||
|
||||
```
|
||||
claudemesh install Register MCP server + hooks in Claude Code
|
||||
@@ -629,7 +786,7 @@ claudemesh mcp Start MCP server (invoked by Claude Code, not users)
|
||||
|
||||
---
|
||||
|
||||
## 11. Implementation status
|
||||
## 14. Implementation status
|
||||
|
||||
| Phase | Version | Status | What |
|
||||
|-------|---------|--------|------|
|
||||
@@ -647,13 +804,20 @@ claudemesh mcp Start MCP server (invoked by Claude Code, not users)
|
||||
| **Message status** | **v0.3.0** | **Done** | Per-recipient delivery detail |
|
||||
| **MCP instructions** | **v0.3.0** | **Done** | Dynamic identity, full tool guide, coordination patterns |
|
||||
| **Multicast fix** | **v0.3.0** | **Done** | Broadcast/group push directly, not queue race |
|
||||
| Files | v0.4.0 | Planned | MinIO-backed file sharing + message attachments |
|
||||
| Multi-target | v0.4.0 | Planned | Array `to` field with deduplication |
|
||||
| Dashboard | v0.5.0 | Planned | Live peers, state, memory, files in web UI |
|
||||
| **Files** | **v0.4.0** | **Done** | MinIO-backed file sharing + message attachments |
|
||||
| **Multi-target** | **v0.4.0** | **Done** | Array `to` field with deduplication |
|
||||
| **Targeted views** | **v0.4.0** | **Done** | MCP instruction pattern for per-audience messages |
|
||||
| Vectors | v0.5.0 | Planned | Qdrant per-mesh collections for semantic search |
|
||||
| Graph | v0.5.0 | Planned | Neo4j per-mesh databases for entity relationships |
|
||||
| Context sharing | v0.5.0 | Planned | Session understanding exchange between peers |
|
||||
| Tasks | v0.5.0 | Planned | First-class work items with claim/complete |
|
||||
| Mesh databases | v0.6.0 | Planned | Per-mesh PostgreSQL schemas for structured data |
|
||||
| Streams | v0.6.0 | Planned | Real-time pub/sub data channels |
|
||||
| Dashboard | v0.7.0 | Planned | Live peers, state, memory, files, graphs in web UI |
|
||||
|
||||
---
|
||||
|
||||
## 12. Design principles
|
||||
## 15. Design principles
|
||||
|
||||
1. **The broker is a dumb pipe.** It routes messages, stores state, holds memory. It does not interpret roles, enforce protocols, or run agents.
|
||||
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
},
|
||||
"prettier": "@turbostarter/prettier-config",
|
||||
"dependencies": {
|
||||
"@qdrant/js-client-rest": "1.17.0",
|
||||
"@turbostarter/db": "workspace:*",
|
||||
"@turbostarter/shared": "workspace:*",
|
||||
"drizzle-orm": "0.44.7",
|
||||
"libsodium-wrappers": "0.7.15",
|
||||
"minio": "8.0.7",
|
||||
"neo4j-driver": "6.0.1",
|
||||
"ws": "8.20.0",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
|
||||
@@ -34,9 +34,12 @@ import {
|
||||
mesh,
|
||||
meshFile,
|
||||
meshFileAccess,
|
||||
meshContext,
|
||||
meshMember as memberTable,
|
||||
meshMemory,
|
||||
meshState,
|
||||
meshStream,
|
||||
meshTask,
|
||||
messageQueue,
|
||||
pendingStatus,
|
||||
presence,
|
||||
@@ -889,6 +892,334 @@ export async function deleteFile(
|
||||
);
|
||||
}
|
||||
|
||||
// --- Context sharing ---
|
||||
|
||||
/**
|
||||
* Upsert a context snapshot for a peer. Each (meshId, presenceId) pair
|
||||
* has at most one context row — repeated calls update it in place.
|
||||
*/
|
||||
export async function shareContext(
|
||||
meshId: string,
|
||||
presenceId: string,
|
||||
peerName: string | undefined,
|
||||
summary: string,
|
||||
filesRead?: string[],
|
||||
keyFindings?: string[],
|
||||
tags?: string[],
|
||||
): Promise<string> {
|
||||
const now = new Date();
|
||||
// Try to find existing context for this presence in this mesh.
|
||||
const [existing] = await db
|
||||
.select({ id: meshContext.id })
|
||||
.from(meshContext)
|
||||
.where(
|
||||
and(
|
||||
eq(meshContext.meshId, meshId),
|
||||
eq(meshContext.presenceId, presenceId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
await db
|
||||
.update(meshContext)
|
||||
.set({
|
||||
peerName: peerName ?? null,
|
||||
summary,
|
||||
filesRead: filesRead ?? [],
|
||||
keyFindings: keyFindings ?? [],
|
||||
tags: tags ?? [],
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(meshContext.id, existing.id));
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
const [row] = await db
|
||||
.insert(meshContext)
|
||||
.values({
|
||||
meshId,
|
||||
presenceId,
|
||||
peerName: peerName ?? null,
|
||||
summary,
|
||||
filesRead: filesRead ?? [],
|
||||
keyFindings: keyFindings ?? [],
|
||||
tags: tags ?? [],
|
||||
updatedAt: now,
|
||||
})
|
||||
.returning({ id: meshContext.id });
|
||||
if (!row) throw new Error("failed to insert context");
|
||||
return row.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search contexts by tag match or summary ILIKE.
|
||||
*/
|
||||
export async function getContext(
|
||||
meshId: string,
|
||||
query: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
peerName: string;
|
||||
summary: string;
|
||||
filesRead: string[];
|
||||
keyFindings: string[];
|
||||
tags: string[];
|
||||
updatedAt: Date;
|
||||
}>
|
||||
> {
|
||||
const result = await db.execute<{
|
||||
peer_name: string | null;
|
||||
summary: string;
|
||||
files_read: string[] | null;
|
||||
key_findings: string[] | null;
|
||||
tags: string[] | null;
|
||||
updated_at: string | Date;
|
||||
}>(sql`
|
||||
SELECT peer_name, summary, files_read, key_findings, tags, updated_at
|
||||
FROM mesh.context
|
||||
WHERE mesh_id = ${meshId}
|
||||
AND (
|
||||
summary ILIKE ${"%" + query + "%"}
|
||||
OR ${query} = ANY(tags)
|
||||
)
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 20
|
||||
`);
|
||||
const rows = (result.rows ?? result) as Array<{
|
||||
peer_name: string | null;
|
||||
summary: string;
|
||||
files_read: string[] | null;
|
||||
key_findings: string[] | null;
|
||||
tags: string[] | null;
|
||||
updated_at: string | Date;
|
||||
}>;
|
||||
return rows.map((r) => ({
|
||||
peerName: r.peer_name ?? "unknown",
|
||||
summary: r.summary,
|
||||
filesRead: r.files_read ?? [],
|
||||
keyFindings: r.key_findings ?? [],
|
||||
tags: r.tags ?? [],
|
||||
updatedAt:
|
||||
r.updated_at instanceof Date ? r.updated_at : new Date(r.updated_at),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* List all contexts for a mesh, ordered by most recently updated.
|
||||
*/
|
||||
export async function listContexts(
|
||||
meshId: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
peerName: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
updatedAt: Date;
|
||||
}>
|
||||
> {
|
||||
const rows = await db
|
||||
.select({
|
||||
peerName: meshContext.peerName,
|
||||
summary: meshContext.summary,
|
||||
tags: meshContext.tags,
|
||||
updatedAt: meshContext.updatedAt,
|
||||
})
|
||||
.from(meshContext)
|
||||
.where(eq(meshContext.meshId, meshId))
|
||||
.orderBy(desc(meshContext.updatedAt));
|
||||
return rows.map((r) => ({
|
||||
peerName: r.peerName ?? "unknown",
|
||||
summary: r.summary,
|
||||
tags: (r.tags ?? []) as string[],
|
||||
updatedAt: r.updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Tasks ---
|
||||
|
||||
/**
|
||||
* Create a new task in a mesh. Returns the generated id.
|
||||
*/
|
||||
export async function createTask(
|
||||
meshId: string,
|
||||
title: string,
|
||||
assignee?: string,
|
||||
priority?: string,
|
||||
tags?: string[],
|
||||
createdByName?: string,
|
||||
): Promise<string> {
|
||||
const [row] = await db
|
||||
.insert(meshTask)
|
||||
.values({
|
||||
meshId,
|
||||
title,
|
||||
assignee: assignee ?? null,
|
||||
priority: priority ?? "normal",
|
||||
status: "open",
|
||||
tags: tags ?? [],
|
||||
createdByName: createdByName ?? null,
|
||||
})
|
||||
.returning({ id: meshTask.id });
|
||||
if (!row) throw new Error("failed to insert task");
|
||||
return row.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim an open task. Sets status to 'claimed' and records who claimed it.
|
||||
* Only succeeds if the task is currently 'open'.
|
||||
*/
|
||||
export async function claimTask(
|
||||
meshId: string,
|
||||
taskId: string,
|
||||
presenceId: string,
|
||||
peerName?: string,
|
||||
): Promise<boolean> {
|
||||
const now = new Date();
|
||||
const result = await db
|
||||
.update(meshTask)
|
||||
.set({
|
||||
status: "claimed",
|
||||
claimedByPresence: presenceId,
|
||||
claimedByName: peerName ?? null,
|
||||
claimedAt: now,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(meshTask.id, taskId),
|
||||
eq(meshTask.meshId, meshId),
|
||||
eq(meshTask.status, "open"),
|
||||
),
|
||||
)
|
||||
.returning({ id: meshTask.id });
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a task. Sets status to 'done', records the result and timestamp.
|
||||
*/
|
||||
export async function completeTask(
|
||||
meshId: string,
|
||||
taskId: string,
|
||||
result?: string,
|
||||
): Promise<boolean> {
|
||||
const now = new Date();
|
||||
const rows = await db
|
||||
.update(meshTask)
|
||||
.set({
|
||||
status: "done",
|
||||
result: result ?? null,
|
||||
completedAt: now,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(meshTask.id, taskId),
|
||||
eq(meshTask.meshId, meshId),
|
||||
),
|
||||
)
|
||||
.returning({ id: meshTask.id });
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* List tasks in a mesh with optional status and assignee filters.
|
||||
*/
|
||||
export async function listTasks(
|
||||
meshId: string,
|
||||
status?: string,
|
||||
assignee?: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
assignee: string | null;
|
||||
claimedBy: string | null;
|
||||
status: string;
|
||||
priority: string;
|
||||
createdBy: string | null;
|
||||
tags: string[];
|
||||
createdAt: Date;
|
||||
}>
|
||||
> {
|
||||
const conditions = [eq(meshTask.meshId, meshId)];
|
||||
if (status) {
|
||||
conditions.push(eq(meshTask.status, status));
|
||||
}
|
||||
if (assignee) {
|
||||
conditions.push(eq(meshTask.assignee, assignee));
|
||||
}
|
||||
const rows = await db
|
||||
.select({
|
||||
id: meshTask.id,
|
||||
title: meshTask.title,
|
||||
assignee: meshTask.assignee,
|
||||
claimedByName: meshTask.claimedByName,
|
||||
status: meshTask.status,
|
||||
priority: meshTask.priority,
|
||||
createdByName: meshTask.createdByName,
|
||||
tags: meshTask.tags,
|
||||
createdAt: meshTask.createdAt,
|
||||
})
|
||||
.from(meshTask)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(meshTask.createdAt))
|
||||
.limit(100);
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
assignee: r.assignee,
|
||||
claimedBy: r.claimedByName,
|
||||
status: r.status,
|
||||
priority: r.priority,
|
||||
createdBy: r.createdByName,
|
||||
tags: (r.tags ?? []) as string[],
|
||||
createdAt: r.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Streams ---
|
||||
|
||||
/**
|
||||
* Create a named real-time stream in a mesh. Upsert semantics: if a
|
||||
* stream with the same (meshId, name) already exists, return its id.
|
||||
*/
|
||||
export async function createStream(
|
||||
meshId: string,
|
||||
name: string,
|
||||
createdByName: string,
|
||||
): Promise<string> {
|
||||
const existing = await db
|
||||
.select({ id: meshStream.id })
|
||||
.from(meshStream)
|
||||
.where(and(eq(meshStream.meshId, meshId), eq(meshStream.name, name)));
|
||||
if (existing.length > 0) return existing[0]!.id;
|
||||
const [row] = await db
|
||||
.insert(meshStream)
|
||||
.values({ meshId, name, createdByName })
|
||||
.returning({ id: meshStream.id });
|
||||
return row!.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all streams in a mesh, ordered by creation time.
|
||||
*/
|
||||
export async function listStreams(
|
||||
meshId: string,
|
||||
): Promise<
|
||||
Array<{ id: string; name: string; createdBy: string | null; createdAt: Date }>
|
||||
> {
|
||||
return db
|
||||
.select({
|
||||
id: meshStream.id,
|
||||
name: meshStream.name,
|
||||
createdBy: meshStream.createdByName,
|
||||
createdAt: meshStream.createdAt,
|
||||
})
|
||||
.from(meshStream)
|
||||
.where(eq(meshStream.meshId, meshId))
|
||||
.orderBy(asc(meshStream.createdAt));
|
||||
}
|
||||
|
||||
// --- Message queueing + delivery ---
|
||||
|
||||
export interface QueueParams {
|
||||
@@ -1239,3 +1570,118 @@ export async function findMemberByPubkey(
|
||||
.limit(1);
|
||||
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 }));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ const envSchema = z.object({
|
||||
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
|
||||
.enum(["development", "production", "test"])
|
||||
.default("development"),
|
||||
|
||||
@@ -15,17 +15,21 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import type { Duplex } from "node:stream";
|
||||
import { WebSocketServer, type WebSocket } from "ws";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { env } from "./env";
|
||||
import { db } from "./db";
|
||||
import { messageQueue } from "@turbostarter/db/schema/mesh";
|
||||
import {
|
||||
claimTask,
|
||||
completeTask,
|
||||
connectPresence,
|
||||
createTask,
|
||||
deleteFile,
|
||||
disconnectPresence,
|
||||
drainForMember,
|
||||
findMemberByPubkey,
|
||||
forgetMemory,
|
||||
getContext,
|
||||
getFile,
|
||||
getFileStatus,
|
||||
getState,
|
||||
@@ -34,9 +38,11 @@ import {
|
||||
joinGroup,
|
||||
joinMesh,
|
||||
leaveGroup,
|
||||
listContexts,
|
||||
listFiles,
|
||||
listPeersInMesh,
|
||||
listState,
|
||||
listTasks,
|
||||
queueMessage,
|
||||
recallMemory,
|
||||
recordFileAccess,
|
||||
@@ -45,12 +51,21 @@ import {
|
||||
rememberMemory,
|
||||
setSummary,
|
||||
setState,
|
||||
shareContext,
|
||||
startSweepers,
|
||||
stopSweepers,
|
||||
uploadFile,
|
||||
writeStatus,
|
||||
ensureMeshSchema,
|
||||
meshQuery,
|
||||
meshExecute,
|
||||
meshSchema,
|
||||
createStream,
|
||||
listStreams,
|
||||
} from "./broker";
|
||||
import { ensureBucket, meshBucketName, minioClient } from "./minio";
|
||||
import { qdrant, meshCollectionName, ensureCollection } from "./qdrant";
|
||||
import { neo4jDriver, meshDbName, ensureDatabase } from "./neo4j-client";
|
||||
import type {
|
||||
HookSetStatusRequest,
|
||||
WSClientMessage,
|
||||
@@ -81,6 +96,9 @@ interface PeerConn {
|
||||
|
||||
const connections = new Map<string, PeerConn>();
|
||||
const connectionsPerMesh = new Map<string, number>();
|
||||
|
||||
// Stream subscriptions: "meshId:streamName" → Set of presenceIds
|
||||
const streamSubscriptions = new Map<string, Set<string>>();
|
||||
const hookRateLimit = new TokenBucket(
|
||||
env.HOOK_RATE_LIMIT_PER_MIN,
|
||||
env.HOOK_RATE_LIMIT_PER_MIN,
|
||||
@@ -1066,6 +1084,598 @@ function handleConnection(ws: WebSocket): void {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "share_context": {
|
||||
const sc = msg as Extract<WSClientMessage, { type: "share_context" }>;
|
||||
const memberInfo = conn.memberPubkey
|
||||
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||
: null;
|
||||
const ctxId = await shareContext(
|
||||
conn.meshId,
|
||||
presenceId,
|
||||
memberInfo?.displayName,
|
||||
sc.summary,
|
||||
sc.filesRead,
|
||||
sc.keyFindings,
|
||||
sc.tags,
|
||||
);
|
||||
sendToPeer(presenceId, {
|
||||
type: "context_shared",
|
||||
id: ctxId,
|
||||
});
|
||||
// Notify all other peers in the mesh that context was shared.
|
||||
for (const [pid, peer] of connections) {
|
||||
if (pid === presenceId) continue;
|
||||
if (peer.meshId !== conn.meshId) continue;
|
||||
sendToPeer(pid, {
|
||||
type: "state_change",
|
||||
key: `_context:${memberInfo?.displayName ?? "unknown"}`,
|
||||
value: sc.summary,
|
||||
updatedBy: memberInfo?.displayName ?? "unknown",
|
||||
});
|
||||
}
|
||||
log.info("ws share_context", {
|
||||
presence_id: presenceId,
|
||||
context_id: ctxId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "get_context": {
|
||||
const gc = msg as Extract<WSClientMessage, { type: "get_context" }>;
|
||||
const contexts = await getContext(conn.meshId, gc.query);
|
||||
sendToPeer(presenceId, {
|
||||
type: "context_results",
|
||||
contexts: contexts.map((c) => ({
|
||||
peerName: c.peerName,
|
||||
summary: c.summary,
|
||||
filesRead: c.filesRead,
|
||||
keyFindings: c.keyFindings,
|
||||
tags: c.tags,
|
||||
updatedAt: c.updatedAt.toISOString(),
|
||||
})),
|
||||
});
|
||||
log.info("ws get_context", {
|
||||
presence_id: presenceId,
|
||||
query: gc.query.slice(0, 80),
|
||||
results: contexts.length,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "list_contexts": {
|
||||
const allContexts = await listContexts(conn.meshId);
|
||||
sendToPeer(presenceId, {
|
||||
type: "context_list",
|
||||
contexts: allContexts.map((c) => ({
|
||||
peerName: c.peerName,
|
||||
summary: c.summary,
|
||||
tags: c.tags,
|
||||
updatedAt: c.updatedAt.toISOString(),
|
||||
})),
|
||||
});
|
||||
log.info("ws list_contexts", {
|
||||
presence_id: presenceId,
|
||||
mesh_id: conn.meshId,
|
||||
count: allContexts.length,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "create_task": {
|
||||
const ct = msg as Extract<WSClientMessage, { type: "create_task" }>;
|
||||
const memberInfo = conn.memberPubkey
|
||||
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||
: null;
|
||||
const taskId = await createTask(
|
||||
conn.meshId,
|
||||
ct.title,
|
||||
ct.assignee,
|
||||
ct.priority,
|
||||
ct.tags,
|
||||
memberInfo?.displayName,
|
||||
);
|
||||
sendToPeer(presenceId, {
|
||||
type: "task_created",
|
||||
id: taskId,
|
||||
});
|
||||
log.info("ws create_task", {
|
||||
presence_id: presenceId,
|
||||
task_id: taskId,
|
||||
title: ct.title.slice(0, 80),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "claim_task": {
|
||||
const clm = msg as Extract<WSClientMessage, { type: "claim_task" }>;
|
||||
const memberInfo = conn.memberPubkey
|
||||
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||
: null;
|
||||
const claimed = await claimTask(
|
||||
conn.meshId,
|
||||
clm.taskId,
|
||||
presenceId,
|
||||
memberInfo?.displayName,
|
||||
);
|
||||
if (!claimed) {
|
||||
sendError(conn.ws, "task_not_claimable", "task is not open or does not exist");
|
||||
break;
|
||||
}
|
||||
// Return updated task list so caller sees the change.
|
||||
const tasksAfterClaim = await listTasks(conn.meshId);
|
||||
sendToPeer(presenceId, {
|
||||
type: "task_list",
|
||||
tasks: tasksAfterClaim.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
assignee: t.assignee,
|
||||
claimedBy: t.claimedBy,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
createdBy: t.createdBy,
|
||||
tags: t.tags,
|
||||
createdAt: t.createdAt.toISOString(),
|
||||
})),
|
||||
});
|
||||
log.info("ws claim_task", {
|
||||
presence_id: presenceId,
|
||||
task_id: clm.taskId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "complete_task": {
|
||||
const cpt = msg as Extract<WSClientMessage, { type: "complete_task" }>;
|
||||
const completed = await completeTask(
|
||||
conn.meshId,
|
||||
cpt.taskId,
|
||||
cpt.result,
|
||||
);
|
||||
if (!completed) {
|
||||
sendError(conn.ws, "task_not_found", "task not found in this mesh");
|
||||
break;
|
||||
}
|
||||
// Return updated task list.
|
||||
const tasksAfterComplete = await listTasks(conn.meshId);
|
||||
sendToPeer(presenceId, {
|
||||
type: "task_list",
|
||||
tasks: tasksAfterComplete.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
assignee: t.assignee,
|
||||
claimedBy: t.claimedBy,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
createdBy: t.createdBy,
|
||||
tags: t.tags,
|
||||
createdAt: t.createdAt.toISOString(),
|
||||
})),
|
||||
});
|
||||
log.info("ws complete_task", {
|
||||
presence_id: presenceId,
|
||||
task_id: cpt.taskId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "list_tasks": {
|
||||
const lt = msg as Extract<WSClientMessage, { type: "list_tasks" }>;
|
||||
const tasks = await listTasks(conn.meshId, lt.status, lt.assignee);
|
||||
sendToPeer(presenceId, {
|
||||
type: "task_list",
|
||||
tasks: tasks.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
assignee: t.assignee,
|
||||
claimedBy: t.claimedBy,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
createdBy: t.createdBy,
|
||||
tags: t.tags,
|
||||
createdAt: t.createdAt.toISOString(),
|
||||
})),
|
||||
});
|
||||
log.info("ws list_tasks", {
|
||||
presence_id: presenceId,
|
||||
mesh_id: conn.meshId,
|
||||
count: tasks.length,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Streams ---
|
||||
|
||||
case "create_stream": {
|
||||
const cs = msg as Extract<WSClientMessage, { type: "create_stream" }>;
|
||||
const memberInfo = conn.memberPubkey
|
||||
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||
: null;
|
||||
const streamId = await createStream(
|
||||
conn.meshId,
|
||||
cs.name,
|
||||
memberInfo?.displayName ?? "peer",
|
||||
);
|
||||
sendToPeer(presenceId, {
|
||||
type: "stream_created",
|
||||
id: streamId,
|
||||
name: cs.name,
|
||||
});
|
||||
log.info("ws create_stream", {
|
||||
presence_id: presenceId,
|
||||
stream: cs.name,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "subscribe": {
|
||||
const sub = msg as Extract<WSClientMessage, { type: "subscribe" }>;
|
||||
const key = `${conn.meshId}:${sub.stream}`;
|
||||
if (!streamSubscriptions.has(key))
|
||||
streamSubscriptions.set(key, new Set());
|
||||
streamSubscriptions.get(key)!.add(presenceId);
|
||||
log.info("ws subscribe", {
|
||||
presence_id: presenceId,
|
||||
stream: sub.stream,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "unsubscribe": {
|
||||
const unsub = msg as Extract<
|
||||
WSClientMessage,
|
||||
{ type: "unsubscribe" }
|
||||
>;
|
||||
const key = `${conn.meshId}:${unsub.stream}`;
|
||||
streamSubscriptions.get(key)?.delete(presenceId);
|
||||
log.info("ws unsubscribe", {
|
||||
presence_id: presenceId,
|
||||
stream: unsub.stream,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "publish": {
|
||||
const pub = msg as Extract<WSClientMessage, { type: "publish" }>;
|
||||
const key = `${conn.meshId}:${pub.stream}`;
|
||||
const subs = streamSubscriptions.get(key);
|
||||
if (subs) {
|
||||
const memberInfo = conn.memberPubkey
|
||||
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||
: null;
|
||||
const push: WSServerMessage = {
|
||||
type: "stream_data",
|
||||
stream: pub.stream,
|
||||
data: pub.data,
|
||||
publishedBy: memberInfo?.displayName ?? "peer",
|
||||
};
|
||||
for (const subPid of subs) {
|
||||
if (subPid === presenceId) continue; // don't echo to publisher
|
||||
sendToPeer(subPid, push);
|
||||
}
|
||||
}
|
||||
metrics.messagesRoutedTotal.inc({ priority: "stream" });
|
||||
break;
|
||||
}
|
||||
|
||||
case "list_streams": {
|
||||
const streams = await listStreams(conn.meshId);
|
||||
sendToPeer(presenceId, {
|
||||
type: "stream_list",
|
||||
streams: streams.map((s) => {
|
||||
const key = `${conn.meshId}:${s.name}`;
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
createdBy: s.createdBy ?? "",
|
||||
createdAt: s.createdAt.toISOString(),
|
||||
subscriberCount: streamSubscriptions.get(key)?.size ?? 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
log.info("ws list_streams", {
|
||||
presence_id: presenceId,
|
||||
mesh_id: conn.meshId,
|
||||
count: streams.length,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Vector storage ---
|
||||
|
||||
case "vector_store": {
|
||||
const vs = msg as Extract<WSClientMessage, { type: "vector_store" }>;
|
||||
const collName = meshCollectionName(conn.meshId, vs.collection);
|
||||
await ensureCollection(collName);
|
||||
const { generateId } = await import("@turbostarter/shared/utils");
|
||||
const pointId = generateId();
|
||||
// Store text + metadata as payload. Use a zero vector as placeholder
|
||||
// — real embeddings should be computed client-side and sent directly
|
||||
// to Qdrant in a future version.
|
||||
const zeroVector = new Array(1536).fill(0) as number[];
|
||||
await qdrant.upsert(collName, {
|
||||
wait: true,
|
||||
points: [
|
||||
{
|
||||
id: pointId,
|
||||
vector: zeroVector,
|
||||
payload: {
|
||||
text: vs.text,
|
||||
mesh_id: conn.meshId,
|
||||
stored_by: conn.memberPubkey,
|
||||
stored_at: new Date().toISOString(),
|
||||
...(vs.metadata ?? {}),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
sendToPeer(presenceId, {
|
||||
type: "ack" as const,
|
||||
id: pointId,
|
||||
messageId: pointId,
|
||||
queued: false,
|
||||
});
|
||||
log.info("ws vector_store", {
|
||||
presence_id: presenceId,
|
||||
collection: vs.collection,
|
||||
point_id: pointId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "vector_search": {
|
||||
const vq = msg as Extract<WSClientMessage, { type: "vector_search" }>;
|
||||
const searchCollName = meshCollectionName(conn.meshId, vq.collection);
|
||||
const searchLimit = vq.limit ?? 10;
|
||||
try {
|
||||
// Keyword search via payload scroll + filter.
|
||||
// Full vector similarity requires client-computed embeddings (future).
|
||||
const queryLower = vq.query.toLowerCase();
|
||||
const scrollResult = await qdrant.scroll(searchCollName, {
|
||||
limit: 100,
|
||||
with_payload: true,
|
||||
with_vector: false,
|
||||
});
|
||||
const matches = (scrollResult.points ?? [])
|
||||
.filter((p) => {
|
||||
const text = (p.payload as Record<string, unknown>)?.text;
|
||||
return typeof text === "string" && text.toLowerCase().includes(queryLower);
|
||||
})
|
||||
.slice(0, searchLimit)
|
||||
.map((p) => {
|
||||
const payload = p.payload as Record<string, unknown>;
|
||||
return {
|
||||
id: String(p.id),
|
||||
text: (payload.text as string) ?? "",
|
||||
score: 1.0, // keyword match — no vector similarity score
|
||||
metadata: payload,
|
||||
};
|
||||
});
|
||||
sendToPeer(presenceId, {
|
||||
type: "vector_results",
|
||||
results: matches,
|
||||
});
|
||||
} catch {
|
||||
// Collection may not exist yet — return empty results.
|
||||
sendToPeer(presenceId, {
|
||||
type: "vector_results",
|
||||
results: [],
|
||||
});
|
||||
}
|
||||
log.info("ws vector_search", {
|
||||
presence_id: presenceId,
|
||||
collection: vq.collection,
|
||||
query: vq.query.slice(0, 80),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "vector_delete": {
|
||||
const vd = msg as Extract<WSClientMessage, { type: "vector_delete" }>;
|
||||
const deleteCollName = meshCollectionName(conn.meshId, vd.collection);
|
||||
try {
|
||||
await qdrant.delete(deleteCollName, {
|
||||
wait: true,
|
||||
points: [vd.id],
|
||||
});
|
||||
} catch {
|
||||
/* collection or point may not exist — idempotent */
|
||||
}
|
||||
sendToPeer(presenceId, {
|
||||
type: "ack" as const,
|
||||
id: vd.id,
|
||||
messageId: vd.id,
|
||||
queued: false,
|
||||
});
|
||||
log.info("ws vector_delete", {
|
||||
presence_id: presenceId,
|
||||
collection: vd.collection,
|
||||
point_id: vd.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "list_collections": {
|
||||
try {
|
||||
const qdrantResponse = await qdrant.getCollections();
|
||||
const prefix = `mesh_${conn.meshId}_`.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
||||
const meshCollections = (qdrantResponse.collections ?? [])
|
||||
.map((c) => c.name)
|
||||
.filter((name) => name.startsWith(prefix))
|
||||
.map((name) => name.slice(prefix.length));
|
||||
sendToPeer(presenceId, {
|
||||
type: "collection_list",
|
||||
collections: meshCollections,
|
||||
});
|
||||
} catch {
|
||||
sendToPeer(presenceId, {
|
||||
type: "collection_list",
|
||||
collections: [],
|
||||
});
|
||||
}
|
||||
log.info("ws list_collections", {
|
||||
presence_id: presenceId,
|
||||
mesh_id: conn.meshId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Graph database ---
|
||||
|
||||
case "graph_query": {
|
||||
const gq = msg as Extract<WSClientMessage, { type: "graph_query" }>;
|
||||
const gqDbName = meshDbName(conn.meshId);
|
||||
let gqSession;
|
||||
try {
|
||||
await ensureDatabase(gqDbName);
|
||||
gqSession = neo4jDriver.session({ database: gqDbName });
|
||||
} catch {
|
||||
// Community edition — fall back to default db.
|
||||
gqSession = neo4jDriver.session();
|
||||
}
|
||||
try {
|
||||
const gqResult = await gqSession.run(gq.cypher);
|
||||
const gqRecords = gqResult.records.map((r) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
for (const key of r.keys) {
|
||||
obj[key] = r.get(key);
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
sendToPeer(presenceId, {
|
||||
type: "graph_result",
|
||||
records: gqRecords,
|
||||
});
|
||||
} catch (gqErr) {
|
||||
sendError(conn.ws, "graph_error", gqErr instanceof Error ? gqErr.message : String(gqErr));
|
||||
} finally {
|
||||
await gqSession.close();
|
||||
}
|
||||
log.info("ws graph_query", {
|
||||
presence_id: presenceId,
|
||||
cypher: gq.cypher.slice(0, 80),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "graph_execute": {
|
||||
const ge = msg as Extract<WSClientMessage, { type: "graph_execute" }>;
|
||||
const geDbName = meshDbName(conn.meshId);
|
||||
let geSession;
|
||||
try {
|
||||
await ensureDatabase(geDbName);
|
||||
geSession = neo4jDriver.session({ database: geDbName });
|
||||
} catch {
|
||||
geSession = neo4jDriver.session();
|
||||
}
|
||||
try {
|
||||
const geResult = await geSession.run(ge.cypher);
|
||||
const geRecords = geResult.records.map((r) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
for (const key of r.keys) {
|
||||
obj[key] = r.get(key);
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
sendToPeer(presenceId, {
|
||||
type: "graph_result",
|
||||
records: geRecords,
|
||||
});
|
||||
} catch (geErr) {
|
||||
sendError(conn.ws, "graph_error", geErr instanceof Error ? geErr.message : String(geErr));
|
||||
} finally {
|
||||
await geSession.close();
|
||||
}
|
||||
log.info("ws graph_execute", {
|
||||
presence_id: presenceId,
|
||||
cypher: ge.cypher.slice(0, 80),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Mesh database (per-mesh PostgreSQL schema) ---
|
||||
|
||||
case "mesh_query": {
|
||||
const mq = msg as Extract<WSClientMessage, { type: "mesh_query" }>;
|
||||
try {
|
||||
const result = await meshQuery(conn.meshId, mq.sql);
|
||||
sendToPeer(presenceId, { type: "mesh_query_result", ...result });
|
||||
} catch (e) {
|
||||
sendError(
|
||||
conn.ws,
|
||||
"query_error",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
);
|
||||
}
|
||||
log.info("ws mesh_query", {
|
||||
presence_id: presenceId,
|
||||
sql: mq.sql.slice(0, 80),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "mesh_execute": {
|
||||
const me = msg as Extract<WSClientMessage, { type: "mesh_execute" }>;
|
||||
try {
|
||||
const result = await meshExecute(conn.meshId, me.sql);
|
||||
sendToPeer(presenceId, {
|
||||
type: "mesh_query_result",
|
||||
columns: [],
|
||||
rows: [],
|
||||
rowCount: result.rowCount,
|
||||
});
|
||||
} catch (e) {
|
||||
sendError(
|
||||
conn.ws,
|
||||
"execute_error",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
);
|
||||
}
|
||||
log.info("ws mesh_execute", {
|
||||
presence_id: presenceId,
|
||||
sql: me.sql.slice(0, 80),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "mesh_schema": {
|
||||
try {
|
||||
const tables = await meshSchema(conn.meshId);
|
||||
sendToPeer(presenceId, { type: "mesh_schema_result", tables });
|
||||
} catch (e) {
|
||||
sendError(
|
||||
conn.ws,
|
||||
"schema_error",
|
||||
e instanceof Error ? e.message : String(e),
|
||||
);
|
||||
}
|
||||
log.info("ws mesh_schema", { presence_id: presenceId });
|
||||
break;
|
||||
}
|
||||
case "mesh_info": {
|
||||
const [peers, stateEntries, memCount, fileCount, taskCounts, streams, tables] = await Promise.all([
|
||||
listPeersInMesh(conn.meshId),
|
||||
listState(conn.meshId),
|
||||
db.execute(sql`SELECT COUNT(*) as n FROM mesh.memory WHERE mesh_id = ${conn.meshId} AND forgotten_at IS NULL`).then(r => Number(((r.rows ?? r) as any[])[0]?.n ?? 0)),
|
||||
db.execute(sql`SELECT COUNT(*) as n FROM mesh.file WHERE mesh_id = ${conn.meshId} AND deleted_at IS NULL`).then(r => Number(((r.rows ?? r) as any[])[0]?.n ?? 0)),
|
||||
db.execute(sql`SELECT status, COUNT(*) as n FROM mesh.task WHERE mesh_id = ${conn.meshId} GROUP BY status`).then(r => {
|
||||
const rows = (r.rows ?? r) as Array<{ status: string; n: string }>;
|
||||
const counts = { open: 0, claimed: 0, done: 0 };
|
||||
for (const row of rows) counts[row.status as keyof typeof counts] = Number(row.n);
|
||||
return counts;
|
||||
}),
|
||||
listStreams(conn.meshId),
|
||||
meshSchema(conn.meshId).catch(() => []),
|
||||
]);
|
||||
const allGroups = new Set<string>();
|
||||
for (const p of peers) for (const g of p.groups) allGroups.add(`@${g.name}`);
|
||||
const myPresence = peers.find(p => p.sessionId === [...connections.entries()].find(([pid]) => pid === presenceId)?.[1]?.sessionPubkey);
|
||||
const peerConn = connections.get(presenceId);
|
||||
sendToPeer(presenceId, {
|
||||
type: "mesh_info_result",
|
||||
mesh: conn.meshId,
|
||||
peers: peers.length,
|
||||
groups: [...allGroups],
|
||||
stateKeys: stateEntries.map((e: any) => e.key),
|
||||
memoryCount: memCount,
|
||||
fileCount: fileCount,
|
||||
tasks: taskCounts,
|
||||
streams: streams.map(s => s.name),
|
||||
tables: tables.map((t: any) => t.name),
|
||||
collections: [],
|
||||
yourName: peerConn?.groups?.[0]?.name ?? "unknown",
|
||||
yourGroups: peerConn?.groups ?? [],
|
||||
});
|
||||
log.info("ws mesh_info", { presence_id: presenceId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
metrics.messagesRejectedTotal.inc({ reason: "parse_or_handler" });
|
||||
@@ -1081,6 +1691,11 @@ function handleConnection(ws: WebSocket): void {
|
||||
connections.delete(presenceId);
|
||||
if (conn) decMeshCount(conn.meshId);
|
||||
await disconnectPresence(presenceId);
|
||||
// Clean up stream subscriptions for this peer
|
||||
for (const [key, subs] of streamSubscriptions) {
|
||||
subs.delete(presenceId);
|
||||
if (subs.size === 0) streamSubscriptions.delete(key);
|
||||
}
|
||||
log.info("ws close", { presence_id: presenceId });
|
||||
}
|
||||
});
|
||||
|
||||
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" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -230,6 +230,133 @@ export interface WSMemoryResultsMessage {
|
||||
}>;
|
||||
}
|
||||
|
||||
// --- Vector storage messages ---
|
||||
|
||||
/** Client → broker: store a text document in a vector collection. */
|
||||
export interface WSVectorStoreMessage {
|
||||
type: "vector_store";
|
||||
collection: string;
|
||||
text: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Client → broker: search a vector collection. */
|
||||
export interface WSVectorSearchMessage {
|
||||
type: "vector_search";
|
||||
collection: string;
|
||||
query: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/** Client → broker: delete a point from a vector collection. */
|
||||
export interface WSVectorDeleteMessage {
|
||||
type: "vector_delete";
|
||||
collection: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Client → broker: list all vector collections for this mesh. */
|
||||
export interface WSListCollectionsMessage {
|
||||
type: "list_collections";
|
||||
}
|
||||
|
||||
// --- Graph database messages ---
|
||||
|
||||
/** Client → broker: run a read-only Cypher query. */
|
||||
export interface WSGraphQueryMessage {
|
||||
type: "graph_query";
|
||||
cypher: string;
|
||||
}
|
||||
|
||||
/** Client → broker: run a write Cypher statement. */
|
||||
export interface WSGraphExecuteMessage {
|
||||
type: "graph_execute";
|
||||
cypher: string;
|
||||
}
|
||||
|
||||
// --- Mesh database (per-mesh PostgreSQL schema) messages ---
|
||||
|
||||
/** Client → broker: run a SELECT query in the mesh's schema. */
|
||||
export interface WSMeshQueryMessage {
|
||||
type: "mesh_query";
|
||||
sql: string;
|
||||
}
|
||||
|
||||
/** Client → broker: run a DDL/DML statement in the mesh's schema. */
|
||||
export interface WSMeshExecuteMessage {
|
||||
type: "mesh_execute";
|
||||
sql: string;
|
||||
}
|
||||
|
||||
/** Client → broker: list tables and columns in the mesh's schema. */
|
||||
export interface WSMeshSchemaMessage {
|
||||
type: "mesh_schema";
|
||||
}
|
||||
|
||||
// --- Vector/Graph response messages ---
|
||||
|
||||
/** Broker → client: vector search results. */
|
||||
export interface WSVectorResultsMessage {
|
||||
type: "vector_results";
|
||||
results: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
score: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Broker → client: list of vector collections. */
|
||||
export interface WSCollectionListMessage {
|
||||
type: "collection_list";
|
||||
collections: string[];
|
||||
}
|
||||
|
||||
/** Broker → client: graph query results. */
|
||||
export interface WSGraphResultMessage {
|
||||
type: "graph_result";
|
||||
records: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/** Broker → client: mesh SQL query results. */
|
||||
export interface WSMeshQueryResultMessage {
|
||||
type: "mesh_query_result";
|
||||
columns: string[];
|
||||
rows: Array<Record<string, unknown>>;
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
/** Broker → client: mesh schema introspection results. */
|
||||
export interface WSMeshSchemaResultMessage {
|
||||
type: "mesh_schema_result";
|
||||
tables: Array<{
|
||||
name: string;
|
||||
columns: Array<{ name: string; type: string; nullable: boolean }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Client → broker: get full mesh overview. */
|
||||
export interface WSMeshInfoMessage {
|
||||
type: "mesh_info";
|
||||
}
|
||||
|
||||
/** Broker → client: aggregated mesh overview. */
|
||||
export interface WSMeshInfoResultMessage {
|
||||
type: "mesh_info_result";
|
||||
mesh: string;
|
||||
peers: number;
|
||||
groups: string[];
|
||||
stateKeys: string[];
|
||||
memoryCount: number;
|
||||
fileCount: number;
|
||||
tasks: { open: number; claimed: number; done: number };
|
||||
streams: string[];
|
||||
tables: string[];
|
||||
collections: string[];
|
||||
yourName: string;
|
||||
yourGroups: Array<{ name: string; role?: string }>;
|
||||
}
|
||||
|
||||
/** Client → broker: check delivery status of a message. */
|
||||
export interface WSMessageStatusMessage {
|
||||
type: "message_status";
|
||||
@@ -309,6 +436,170 @@ export interface WSFileStatusResultMessage {
|
||||
}>;
|
||||
}
|
||||
|
||||
// --- Context sharing messages ---
|
||||
|
||||
/** Client → broker: share current working context. */
|
||||
export interface WSShareContextMessage {
|
||||
type: "share_context";
|
||||
summary: string;
|
||||
filesRead?: string[];
|
||||
keyFindings?: string[];
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/** Client → broker: search contexts by query. */
|
||||
export interface WSGetContextMessage {
|
||||
type: "get_context";
|
||||
query: string;
|
||||
}
|
||||
|
||||
/** Client → broker: list all contexts in the mesh. */
|
||||
export interface WSListContextsMessage {
|
||||
type: "list_contexts";
|
||||
}
|
||||
|
||||
/** Broker → client: acknowledgement for share_context. */
|
||||
export interface WSContextSharedMessage {
|
||||
type: "context_shared";
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Broker → client: response to get_context. */
|
||||
export interface WSContextResultsMessage {
|
||||
type: "context_results";
|
||||
contexts: Array<{
|
||||
peerName: string;
|
||||
summary: string;
|
||||
filesRead: string[];
|
||||
keyFindings: string[];
|
||||
tags: string[];
|
||||
updatedAt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Broker → client: response to list_contexts. */
|
||||
export interface WSContextListMessage {
|
||||
type: "context_list";
|
||||
contexts: Array<{
|
||||
peerName: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
updatedAt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// --- Task messages ---
|
||||
|
||||
/** Client → broker: create a task. */
|
||||
export interface WSCreateTaskMessage {
|
||||
type: "create_task";
|
||||
title: string;
|
||||
assignee?: string;
|
||||
priority?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/** Client → broker: claim an open task. */
|
||||
export interface WSClaimTaskMessage {
|
||||
type: "claim_task";
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
/** Client → broker: mark a task as done. */
|
||||
export interface WSCompleteTaskMessage {
|
||||
type: "complete_task";
|
||||
taskId: string;
|
||||
result?: string;
|
||||
}
|
||||
|
||||
/** Client → broker: list tasks with optional filters. */
|
||||
export interface WSListTasksMessage {
|
||||
type: "list_tasks";
|
||||
status?: string;
|
||||
assignee?: string;
|
||||
}
|
||||
|
||||
/** Broker → client: acknowledgement for create_task. */
|
||||
export interface WSTaskCreatedMessage {
|
||||
type: "task_created";
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Broker → client: response to list_tasks, claim_task, complete_task. */
|
||||
export interface WSTaskListMessage {
|
||||
type: "task_list";
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
assignee: string | null;
|
||||
claimedBy: string | null;
|
||||
status: string;
|
||||
priority: string;
|
||||
createdBy: string | null;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// --- Stream messages ---
|
||||
|
||||
/** Client → broker: create a named real-time stream. */
|
||||
export interface WSCreateStreamMessage {
|
||||
type: "create_stream";
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Client → broker: publish data to a stream. */
|
||||
export interface WSPublishMessage {
|
||||
type: "publish";
|
||||
stream: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/** Client → broker: subscribe to a stream. */
|
||||
export interface WSSubscribeMessage {
|
||||
type: "subscribe";
|
||||
stream: string;
|
||||
}
|
||||
|
||||
/** Client → broker: unsubscribe from a stream. */
|
||||
export interface WSUnsubscribeMessage {
|
||||
type: "unsubscribe";
|
||||
stream: string;
|
||||
}
|
||||
|
||||
/** Client → broker: list all streams in the mesh. */
|
||||
export interface WSListStreamsMessage {
|
||||
type: "list_streams";
|
||||
}
|
||||
|
||||
/** Broker → client: acknowledgement for create_stream. */
|
||||
export interface WSStreamCreatedMessage {
|
||||
type: "stream_created";
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Broker → client: real-time data pushed from a stream. */
|
||||
export interface WSStreamDataMessage {
|
||||
type: "stream_data";
|
||||
stream: string;
|
||||
data: unknown;
|
||||
publishedBy: string;
|
||||
}
|
||||
|
||||
/** Broker → client: response to list_streams. */
|
||||
export interface WSStreamListMessage {
|
||||
type: "stream_list";
|
||||
streams: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
subscriberCount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Broker → client: structured error. */
|
||||
export interface WSErrorMessage {
|
||||
type: "error";
|
||||
@@ -335,7 +626,29 @@ export type WSClientMessage =
|
||||
| WSGetFileMessage
|
||||
| WSListFilesMessage
|
||||
| WSFileStatusMessage
|
||||
| WSDeleteFileMessage;
|
||||
| WSDeleteFileMessage
|
||||
| WSShareContextMessage
|
||||
| WSGetContextMessage
|
||||
| WSListContextsMessage
|
||||
| WSCreateTaskMessage
|
||||
| WSClaimTaskMessage
|
||||
| WSCompleteTaskMessage
|
||||
| WSListTasksMessage
|
||||
| WSVectorStoreMessage
|
||||
| WSVectorSearchMessage
|
||||
| WSVectorDeleteMessage
|
||||
| WSListCollectionsMessage
|
||||
| WSGraphQueryMessage
|
||||
| WSGraphExecuteMessage
|
||||
| WSMeshQueryMessage
|
||||
| WSMeshExecuteMessage
|
||||
| WSMeshSchemaMessage
|
||||
| WSCreateStreamMessage
|
||||
| WSPublishMessage
|
||||
| WSSubscribeMessage
|
||||
| WSUnsubscribeMessage
|
||||
| WSListStreamsMessage
|
||||
| WSMeshInfoMessage;
|
||||
|
||||
export type WSServerMessage =
|
||||
| WSHelloAckMessage
|
||||
@@ -351,4 +664,18 @@ export type WSServerMessage =
|
||||
| WSFileUrlMessage
|
||||
| WSFileListMessage
|
||||
| WSFileStatusResultMessage
|
||||
| WSContextSharedMessage
|
||||
| WSContextResultsMessage
|
||||
| WSContextListMessage
|
||||
| WSTaskCreatedMessage
|
||||
| WSTaskListMessage
|
||||
| WSVectorResultsMessage
|
||||
| WSCollectionListMessage
|
||||
| WSGraphResultMessage
|
||||
| WSMeshQueryResultMessage
|
||||
| WSMeshSchemaResultMessage
|
||||
| WSStreamCreatedMessage
|
||||
| WSStreamDataMessage
|
||||
| WSStreamListMessage
|
||||
| WSMeshInfoResultMessage
|
||||
| WSErrorMessage;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudemesh-cli",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
|
||||
@@ -166,6 +166,26 @@ When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATEL
|
||||
| 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. |
|
||||
|
||||
If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
|
||||
|
||||
@@ -192,6 +212,24 @@ Persistent knowledge that survives across sessions. Use remember(content, tags?)
|
||||
share_file for persistent references, send_message(file:) for ephemeral attachments.
|
||||
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)
|
||||
@@ -455,6 +493,213 @@ Call list_peers at session start to understand who is online, their roles, and w
|
||||
return text(`Deleted: ${id}`);
|
||||
}
|
||||
|
||||
// --- Vectors ---
|
||||
case "vector_store": {
|
||||
const { collection, text: storeText, metadata } = (args ?? {}) as { collection?: string; text?: string; metadata?: Record<string, unknown> };
|
||||
if (!collection || !storeText) return text("vector_store: `collection` and `text` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("vector_store: not connected", true);
|
||||
const id = await client.vectorStore(collection, storeText, metadata);
|
||||
return text(`Stored in ${collection}${id ? ` (${id})` : ""}`);
|
||||
}
|
||||
case "vector_search": {
|
||||
const { collection, query, limit } = (args ?? {}) as { collection?: string; query?: string; limit?: number };
|
||||
if (!collection || !query) return text("vector_search: `collection` and `query` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("vector_search: not connected", true);
|
||||
const results = await client.vectorSearch(collection, query, limit);
|
||||
if (results.length === 0) return text(`No results in ${collection} for "${query}".`);
|
||||
const lines = results.map(r => `- [${r.id.slice(0, 8)}…] (score: ${r.score.toFixed(3)}) ${r.text.slice(0, 120)}${r.text.length > 120 ? "…" : ""}`);
|
||||
return text(`${results.length} result(s) in ${collection}:\n${lines.join("\n")}`);
|
||||
}
|
||||
case "vector_delete": {
|
||||
const { collection, id } = (args ?? {}) as { collection?: string; id?: string };
|
||||
if (!collection || !id) return text("vector_delete: `collection` and `id` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("vector_delete: not connected", true);
|
||||
await client.vectorDelete(collection, id);
|
||||
return text(`Deleted ${id} from ${collection}`);
|
||||
}
|
||||
case "list_collections": {
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("list_collections: not connected", true);
|
||||
const collections = await client.listCollections();
|
||||
if (collections.length === 0) return text("No vector collections.");
|
||||
return text(`Collections:\n${collections.map(c => `- ${c}`).join("\n")}`);
|
||||
}
|
||||
|
||||
// --- Graph ---
|
||||
case "graph_query": {
|
||||
const { cypher } = (args ?? {}) as { cypher?: string };
|
||||
if (!cypher) return text("graph_query: `cypher` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("graph_query: not connected", true);
|
||||
const rows = await client.graphQuery(cypher);
|
||||
if (rows.length === 0) return text("No results.");
|
||||
return text(JSON.stringify(rows, null, 2));
|
||||
}
|
||||
case "graph_execute": {
|
||||
const { cypher } = (args ?? {}) as { cypher?: string };
|
||||
if (!cypher) return text("graph_execute: `cypher` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("graph_execute: not connected", true);
|
||||
const rows = await client.graphExecute(cypher);
|
||||
return text(rows.length > 0 ? JSON.stringify(rows, null, 2) : "Executed successfully.");
|
||||
}
|
||||
|
||||
// --- Context ---
|
||||
case "share_context": {
|
||||
const { summary, files_read, key_findings, tags } = (args ?? {}) as { summary?: string; files_read?: string[]; key_findings?: string[]; tags?: string[] };
|
||||
if (!summary) return text("share_context: `summary` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("share_context: not connected", true);
|
||||
await client.shareContext(summary, files_read, key_findings, tags);
|
||||
return text(`Context shared: "${summary.slice(0, 80)}${summary.length > 80 ? "…" : ""}"`);
|
||||
}
|
||||
case "get_context": {
|
||||
const { query } = (args ?? {}) as { query?: string };
|
||||
if (!query) return text("get_context: `query` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("get_context: not connected", true);
|
||||
const contexts = await client.getContext(query);
|
||||
if (contexts.length === 0) return text(`No context found for "${query}".`);
|
||||
const lines = contexts.map(c => {
|
||||
const files = c.filesRead.length ? `\n Files: ${c.filesRead.join(", ")}` : "";
|
||||
const findings = c.keyFindings.length ? `\n Findings: ${c.keyFindings.join("; ")}` : "";
|
||||
return `- **${c.peerName}** (${c.updatedAt}): ${c.summary}${files}${findings}`;
|
||||
});
|
||||
return text(`${contexts.length} context(s):\n${lines.join("\n")}`);
|
||||
}
|
||||
case "list_contexts": {
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("list_contexts: not connected", true);
|
||||
const contexts = await client.listContexts();
|
||||
if (contexts.length === 0) return text("No peer contexts shared yet.");
|
||||
const lines = contexts.map(c => `- **${c.peerName}**: ${c.summary}${c.tags.length ? ` [${c.tags.join(", ")}]` : ""}`);
|
||||
return text(`Peer contexts:\n${lines.join("\n")}`);
|
||||
}
|
||||
|
||||
// --- Tasks ---
|
||||
case "create_task": {
|
||||
const { title, assignee, priority, tags } = (args ?? {}) as { title?: string; assignee?: string; priority?: string; tags?: string[] };
|
||||
if (!title) return text("create_task: `title` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("create_task: not connected", true);
|
||||
const id = await client.createTask(title, assignee, priority, tags);
|
||||
return text(`Task created${id ? ` (${id})` : ""}: "${title}"${assignee ? ` → ${assignee}` : ""}`);
|
||||
}
|
||||
case "claim_task": {
|
||||
const { id } = (args ?? {}) as { id?: string };
|
||||
if (!id) return text("claim_task: `id` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("claim_task: not connected", true);
|
||||
await client.claimTask(id);
|
||||
return text(`Claimed task: ${id}`);
|
||||
}
|
||||
case "complete_task": {
|
||||
const { id, result } = (args ?? {}) as { id?: string; result?: string };
|
||||
if (!id) return text("complete_task: `id` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("complete_task: not connected", true);
|
||||
await client.completeTask(id, result);
|
||||
return text(`Completed task: ${id}${result ? ` — ${result}` : ""}`);
|
||||
}
|
||||
case "list_tasks": {
|
||||
const { status, assignee } = (args ?? {}) as { status?: string; assignee?: string };
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("list_tasks: not connected", true);
|
||||
const tasks = await client.listTasks(status, assignee);
|
||||
if (tasks.length === 0) return text("No tasks found.");
|
||||
const lines = tasks.map(t => `- [${t.id.slice(0, 8)}…] **${t.title}** (${t.status}, ${t.priority}) ${t.assignee ? `→ ${t.assignee}` : "unassigned"} (by ${t.createdBy})`);
|
||||
return text(`${tasks.length} task(s):\n${lines.join("\n")}`);
|
||||
}
|
||||
|
||||
// --- Mesh Database ---
|
||||
case "mesh_query": {
|
||||
const { sql: querySql } = (args ?? {}) as { sql?: string };
|
||||
if (!querySql) return text("mesh_query: `sql` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("mesh_query: not connected", true);
|
||||
const result = await client.meshQuery(querySql);
|
||||
if (!result) return text("mesh_query: query failed or timed out", true);
|
||||
if (result.rows.length === 0) return text(`Query returned 0 rows.`);
|
||||
const header = `| ${result.columns.join(" | ")} |`;
|
||||
const sep = `| ${result.columns.map(() => "---").join(" | ")} |`;
|
||||
const rows = result.rows.map(r => `| ${result.columns.map(c => String(r[c] ?? "")).join(" | ")} |`);
|
||||
return text(`${result.rowCount} row(s):\n${header}\n${sep}\n${rows.join("\n")}`);
|
||||
}
|
||||
case "mesh_execute": {
|
||||
const { sql: execSql } = (args ?? {}) as { sql?: string };
|
||||
if (!execSql) return text("mesh_execute: `sql` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("mesh_execute: not connected", true);
|
||||
await client.meshExecute(execSql);
|
||||
return text(`Executed.`);
|
||||
}
|
||||
case "mesh_schema": {
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("mesh_schema: not connected", true);
|
||||
const tables = await client.meshSchema();
|
||||
if (!tables || tables.length === 0) return text("No tables in mesh database.");
|
||||
const lines = tables.map(t => `**${t.name}**: ${t.columns.map(c => `${c.name} (${c.type}${c.nullable ? ", nullable" : ""})`).join(", ")}`);
|
||||
return text(lines.join("\n"));
|
||||
}
|
||||
|
||||
// --- Streams ---
|
||||
case "create_stream": {
|
||||
const { name: streamName } = (args ?? {}) as { name?: string };
|
||||
if (!streamName) return text("create_stream: `name` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("create_stream: not connected", true);
|
||||
const streamId = await client.createStream(streamName);
|
||||
return text(`Stream created: ${streamName}${streamId ? ` (${streamId})` : ""}`);
|
||||
}
|
||||
case "publish": {
|
||||
const { stream: pubStream, data: pubData } = (args ?? {}) as { stream?: string; data?: unknown };
|
||||
if (!pubStream) return text("publish: `stream` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("publish: not connected", true);
|
||||
await client.publish(pubStream, pubData);
|
||||
return text(`Published to ${pubStream}.`);
|
||||
}
|
||||
case "subscribe": {
|
||||
const { stream: subStream } = (args ?? {}) as { stream?: string };
|
||||
if (!subStream) return text("subscribe: `stream` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("subscribe: not connected", true);
|
||||
await client.subscribe(subStream);
|
||||
return text(`Subscribed to ${subStream}. Data pushes will arrive as channel notifications.`);
|
||||
}
|
||||
case "list_streams": {
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("list_streams: not connected", true);
|
||||
const streams = await client.listStreams();
|
||||
if (streams.length === 0) return text("No active streams.");
|
||||
const lines = streams.map(s => `- **${s.name}** (${s.id.slice(0, 8)}…) by ${s.createdBy}, ${s.subscriberCount} subscriber(s)`);
|
||||
return text(lines.join("\n"));
|
||||
}
|
||||
|
||||
case "mesh_info": {
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("mesh_info: not connected", true);
|
||||
const info = await client.meshInfo();
|
||||
if (!info) return text("mesh_info: timed out", true);
|
||||
const lines = [
|
||||
`**Mesh**: ${info.mesh}`,
|
||||
`**Peers**: ${info.peers}`,
|
||||
`**Groups**: ${(info.groups as string[])?.join(", ") || "none"}`,
|
||||
`**State keys**: ${(info.stateKeys as string[])?.join(", ") || "none"}`,
|
||||
`**Memories**: ${info.memoryCount}`,
|
||||
`**Files**: ${info.fileCount}`,
|
||||
`**Tasks**: open=${(info.tasks as any)?.open ?? 0}, claimed=${(info.tasks as any)?.claimed ?? 0}, done=${(info.tasks as any)?.done ?? 0}`,
|
||||
`**Streams**: ${(info.streams as string[])?.join(", ") || "none"}`,
|
||||
`**Tables**: ${(info.tables as string[])?.join(", ") || "none"}`,
|
||||
`**Your name**: ${info.yourName}`,
|
||||
`**Your groups**: ${(info.yourGroups as any[])?.map((g: any) => `@${g.name}${g.role ? ':' + g.role : ''}`).join(", ") || "none"}`,
|
||||
];
|
||||
return text(lines.join("\n"));
|
||||
}
|
||||
|
||||
default:
|
||||
return text(`Unknown tool: ${name}`, true);
|
||||
}
|
||||
@@ -499,6 +744,22 @@ Call list_peers at session start to understand who is online, their roles, and w
|
||||
}
|
||||
});
|
||||
|
||||
client.onStreamData(async (evt) => {
|
||||
try {
|
||||
await server.notification({
|
||||
method: "notifications/claude/channel",
|
||||
params: {
|
||||
content: `[stream:${evt.stream}] from ${evt.publishedBy}: ${JSON.stringify(evt.data)}`,
|
||||
meta: {
|
||||
kind: "stream_data",
|
||||
stream: evt.stream,
|
||||
published_by: evt.publishedBy,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch { /* best effort */ }
|
||||
});
|
||||
|
||||
client.onStateChange(async (change) => {
|
||||
try {
|
||||
await server.notification({
|
||||
|
||||
@@ -269,4 +269,290 @@ export const TOOLS: Tool[] = [
|
||||
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: {} },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -415,6 +415,19 @@ export class BrokerClient {
|
||||
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;
|
||||
@@ -517,12 +530,262 @@ export class BrokerClient {
|
||||
return body.fileId ?? null;
|
||||
}
|
||||
|
||||
// --- Vectors ---
|
||||
|
||||
/** Store an embedding in a per-mesh Qdrant collection. */
|
||||
async vectorStore(collection: string, text: string, metadata?: Record<string, unknown>): Promise<string | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
this.vectorStoredResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "vector_store", collection, text, metadata }));
|
||||
setTimeout(() => {
|
||||
const idx = this.vectorStoredResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.vectorStoredResolvers.splice(idx, 1); resolve(null); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** Semantic search over stored embeddings. */
|
||||
async vectorSearch(collection: string, query: string, limit?: number): Promise<Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.vectorResultsResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "vector_search", collection, query, limit }));
|
||||
setTimeout(() => {
|
||||
const idx = this.vectorResultsResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.vectorResultsResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** Remove an embedding from a collection. */
|
||||
async vectorDelete(collection: string, id: string): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "vector_delete", collection, id }));
|
||||
}
|
||||
|
||||
/** List vector collections in this mesh. */
|
||||
async listCollections(): Promise<string[]> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.collectionListResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "list_collections" }));
|
||||
setTimeout(() => {
|
||||
const idx = this.collectionListResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.collectionListResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Graph ---
|
||||
|
||||
/** Run a read query on the per-mesh Neo4j database. */
|
||||
async graphQuery(cypher: string): Promise<Array<Record<string, unknown>>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.graphResultResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "graph_query", cypher }));
|
||||
setTimeout(() => {
|
||||
const idx = this.graphResultResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.graphResultResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** Run a write query (CREATE, MERGE, DELETE) on the per-mesh Neo4j database. */
|
||||
async graphExecute(cypher: string): Promise<Array<Record<string, unknown>>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.graphResultResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "graph_execute", cypher }));
|
||||
setTimeout(() => {
|
||||
const idx = this.graphResultResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.graphResultResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Context ---
|
||||
|
||||
/** Share session understanding with the mesh. */
|
||||
async shareContext(summary: string, filesRead?: string[], keyFindings?: string[], tags?: string[]): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "share_context", summary, filesRead, keyFindings, tags }));
|
||||
}
|
||||
|
||||
/** Find context from peers who explored an area. */
|
||||
async getContext(query: string): Promise<Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.contextResultsResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "get_context", query }));
|
||||
setTimeout(() => {
|
||||
const idx = this.contextResultsResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.contextResultsResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** See what all peers currently know. */
|
||||
async listContexts(): Promise<Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.contextListResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "list_contexts" }));
|
||||
setTimeout(() => {
|
||||
const idx = this.contextListResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.contextListResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Tasks ---
|
||||
|
||||
/** Create a work item. */
|
||||
async createTask(title: string, assignee?: string, priority?: string, tags?: string[]): Promise<string | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
this.taskCreatedResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "create_task", title, assignee, priority, tags }));
|
||||
setTimeout(() => {
|
||||
const idx = this.taskCreatedResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.taskCreatedResolvers.splice(idx, 1); resolve(null); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** Claim an unclaimed task. */
|
||||
async claimTask(id: string): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "claim_task", id }));
|
||||
}
|
||||
|
||||
/** Mark a task done with optional result. */
|
||||
async completeTask(id: string, result?: string): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "complete_task", id, result }));
|
||||
}
|
||||
|
||||
/** List tasks filtered by status/assignee. */
|
||||
async listTasks(status?: string, assignee?: string): Promise<Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.taskListResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "list_tasks", status, assignee }));
|
||||
setTimeout(() => {
|
||||
const idx = this.taskListResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.taskListResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Mesh Database ---
|
||||
|
||||
/** Run a SELECT query on the per-mesh shared database. */
|
||||
async meshQuery(sql: string): Promise<{ columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
this.meshQueryResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "mesh_query", sql }));
|
||||
setTimeout(() => {
|
||||
const idx = this.meshQueryResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.meshQueryResolvers.splice(idx, 1); resolve(null); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE). */
|
||||
async meshExecute(sql: string): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "mesh_execute", sql }));
|
||||
}
|
||||
|
||||
/** List tables and columns in the per-mesh shared database. */
|
||||
async meshSchema(): Promise<Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.meshSchemaResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "mesh_schema" }));
|
||||
setTimeout(() => {
|
||||
const idx = this.meshSchemaResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.meshSchemaResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Streams ---
|
||||
|
||||
/** Create a real-time data stream in the mesh. */
|
||||
async createStream(name: string): Promise<string | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
this.streamCreatedResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "create_stream", name }));
|
||||
setTimeout(() => {
|
||||
const idx = this.streamCreatedResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.streamCreatedResolvers.splice(idx, 1); resolve(null); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** Push data to a stream. Subscribers receive it in real-time. */
|
||||
async publish(stream: string, data: unknown): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "publish", stream, data }));
|
||||
}
|
||||
|
||||
/** Subscribe to a stream. Data pushes arrive via onStreamData handler. */
|
||||
async subscribe(stream: string): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "subscribe", stream }));
|
||||
}
|
||||
|
||||
/** Unsubscribe from a stream. */
|
||||
async unsubscribe(stream: string): Promise<void> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||
this.ws.send(JSON.stringify({ type: "unsubscribe", stream }));
|
||||
}
|
||||
|
||||
/** List active streams in the mesh. */
|
||||
async listStreams(): Promise<Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
this.streamListResolvers.push(resolve);
|
||||
this.ws!.send(JSON.stringify({ type: "list_streams" }));
|
||||
setTimeout(() => {
|
||||
const idx = this.streamListResolvers.indexOf(resolve);
|
||||
if (idx !== -1) { this.streamListResolvers.splice(idx, 1); resolve([]); }
|
||||
}, 5_000);
|
||||
});
|
||||
}
|
||||
|
||||
/** Subscribe to stream data pushes. Returns an unsubscribe function. */
|
||||
onStreamData(handler: (data: { stream: string; data: unknown; publishedBy: string }) => void): () => void {
|
||||
this.streamDataHandlers.add(handler);
|
||||
return () => this.streamDataHandlers.delete(handler);
|
||||
}
|
||||
|
||||
/** Subscribe to state change notifications. Returns an unsubscribe function. */
|
||||
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 {
|
||||
this.closed = true;
|
||||
if (this.helloTimer) clearTimeout(this.helloTimer);
|
||||
@@ -698,6 +961,100 @@ export class BrokerClient {
|
||||
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") {
|
||||
this.debug(`broker error: ${msg.code} ${msg.message}`);
|
||||
const id = msg.id ? String(msg.id) : null;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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";
|
||||
|
||||
const INTERNAL_PACKAGES = [
|
||||
@@ -130,4 +133,4 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: env.ANALYZE,
|
||||
});
|
||||
|
||||
export default withBundleAnalyzer(config);
|
||||
export default withPayload(withBundleAnalyzer(config));
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
14
apps/web/src/app/(payload)/payload/[[...segments]]/page.tsx
Normal file
14
apps/web/src/app/(payload)/payload/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck — Payload generates these types at build time
|
||||
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||
import { importMap } from "../importMap";
|
||||
import config from "@payload-config";
|
||||
|
||||
type Args = { params: Promise<{ segments: string[] }> };
|
||||
|
||||
export const generateMetadata = ({ params }: Args) =>
|
||||
generatePageMetadata({ config, params });
|
||||
|
||||
export default function Page({ params }: Args) {
|
||||
return <RootPage config={config} params={params} importMap={importMap} />;
|
||||
}
|
||||
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 = [
|
||||
{
|
||||
key: "onboard",
|
||||
tab: "Onboarding",
|
||||
title: "Bootstrap any teammate",
|
||||
body: "New hire's Claude inherits the team's context library on day one. No hand-holding, no week-long repo tour.",
|
||||
key: "groups",
|
||||
tab: "Groups",
|
||||
title: "Peers self-organize through @groups",
|
||||
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",
|
||||
tab: "Hand-offs",
|
||||
title: "Work travels with context",
|
||||
body: "Pass an investigation to your teammate's session with full history — hypotheses, logs, files touched, commands run.",
|
||||
key: "state",
|
||||
tab: "Shared state",
|
||||
title: "Live facts the whole mesh can read",
|
||||
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",
|
||||
tab: "Refactors",
|
||||
title: "Coordinate cross-cutting changes",
|
||||
body: "Rename a type, rotate a secret, bump a schema — once. Every other agent picks up the change from its own repo.",
|
||||
key: "memory",
|
||||
tab: "Memory",
|
||||
title: "The mesh gets smarter over time",
|
||||
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 = () => {
|
||||
const [active, setActive] = useState(0);
|
||||
const feature = FEATURES[active]!;
|
||||
return (
|
||||
<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)]">
|
||||
@@ -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)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
What could your mesh do?
|
||||
What your mesh can do today
|
||||
</h2>
|
||||
</Reveal>
|
||||
<Reveal delay={2} className="mt-10 flex justify-center">
|
||||
<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}>
|
||||
<Reveal delay={2}>
|
||||
<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)" }}
|
||||
>
|
||||
Free forever for solo developers · Or read the{" "}
|
||||
<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>
|
||||
30+ MCP tools. Groups, state, memory, messaging — all shipped.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal delay={4}>
|
||||
<div className="mt-16 flex justify-center gap-2">
|
||||
<Reveal delay={3}>
|
||||
<div className="mt-12 flex flex-wrap justify-center gap-2">
|
||||
{FEATURES.map((f, i) => (
|
||||
<button
|
||||
key={f.key}
|
||||
@@ -86,20 +84,30 @@ export const Features = () => {
|
||||
</button>
|
||||
))}
|
||||
</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
|
||||
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)" }}
|
||||
>
|
||||
{FEATURES[active]?.title}
|
||||
{feature.title}
|
||||
</h3>
|
||||
<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)" }}
|
||||
>
|
||||
{FEATURES[active]?.body}
|
||||
{feature.body}
|
||||
</p>
|
||||
</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>
|
||||
</div>
|
||||
</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"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
Peer mesh for Claude Code. Connect your sessions across repos and
|
||||
machines. Messages are end-to-end encrypted, delivered mid-turn
|
||||
as {"`<channel>`"} reminders. Your Claudes talk to each other; the
|
||||
broker never sees plaintext.
|
||||
Your Claude Code sessions form a team. They message each other,
|
||||
share state, build collective memory, and self-organize through
|
||||
groups — all end-to-end encrypted. One command to launch. The broker
|
||||
routes ciphertext; it never reads your messages.
|
||||
<span className="block pt-2 text-[var(--cm-clay)]">
|
||||
Open-source CLI. Free during public beta.
|
||||
</span>
|
||||
|
||||
@@ -229,31 +229,31 @@ type UseCase = {
|
||||
|
||||
const USE_CASES: UseCase[] = [
|
||||
{
|
||||
tag: "solo · multi-machine",
|
||||
title: "One dev, three machines",
|
||||
tag: "team · groups",
|
||||
title: "Five agents, one sprint",
|
||||
before:
|
||||
"Laptop, desktop, cloud dev box — each Claude session an island. You re-explain what you're doing every time you switch machines.",
|
||||
now: "Your desktop's Claude asks your laptop's Claude what it was touching. Context travels with you. The machine stops mattering.",
|
||||
"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: "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:
|
||||
"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",
|
||||
title: "Bug Alice fixed, Bob rediscovers",
|
||||
tag: "knowledge · memory",
|
||||
title: "New hire's Claude knows the codebase",
|
||||
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.",
|
||||
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.",
|
||||
"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: "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:
|
||||
"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",
|
||||
title: "CI fails at 3am",
|
||||
tag: "coordination · state",
|
||||
title: "\"Is the deploy frozen?\" answered in zero messages",
|
||||
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.",
|
||||
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.",
|
||||
"You ask in Slack. Someone answers twenty minutes later. Meanwhile two PRs merge. The deploy breaks. Nobody knew it was frozen.",
|
||||
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:
|
||||
"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.",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -48,6 +48,41 @@ services:
|
||||
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:
|
||||
image: ${BROKER_IMAGE:-claudemesh-broker:latest}
|
||||
restart: always
|
||||
@@ -64,6 +99,10 @@ services:
|
||||
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:
|
||||
- "7900"
|
||||
networks:
|
||||
@@ -72,6 +111,10 @@ services:
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
qdrant:
|
||||
condition: service_healthy
|
||||
neo4j:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "bun", "-e", "fetch('http://localhost:7900/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
|
||||
interval: 15s
|
||||
@@ -114,6 +157,8 @@ services:
|
||||
|
||||
volumes:
|
||||
minio-data:
|
||||
qdrant-data:
|
||||
neo4j-data:
|
||||
|
||||
networks:
|
||||
# Coolify's shared Traefik network — must already exist on the host
|
||||
|
||||
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");
|
||||
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
@@ -71,6 +71,20 @@
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -327,6 +327,68 @@ export const meshFileAccess = meshSchema.table("file_access", {
|
||||
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 }) => ({
|
||||
owner: one(user, {
|
||||
fields: [mesh.ownerUserId],
|
||||
@@ -469,3 +531,45 @@ 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;
|
||||
|
||||
192
pnpm-lock.yaml
generated
192
pnpm-lock.yaml
generated
@@ -111,6 +111,9 @@ importers:
|
||||
|
||||
apps/broker:
|
||||
dependencies:
|
||||
'@qdrant/js-client-rest':
|
||||
specifier: 1.17.0
|
||||
version: 1.17.0(typescript@5.9.3)
|
||||
'@turbostarter/db':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/db
|
||||
@@ -123,6 +126,12 @@ importers:
|
||||
libsodium-wrappers:
|
||||
specifier: 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:
|
||||
specifier: 8.20.0
|
||||
version: 8.20.0
|
||||
@@ -4582,6 +4591,16 @@ packages:
|
||||
peerDependencies:
|
||||
'@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':
|
||||
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
|
||||
|
||||
@@ -7843,6 +7862,9 @@ packages:
|
||||
async-limiter@1.0.1:
|
||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
@@ -8034,6 +8056,9 @@ packages:
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
block-stream2@2.1.0:
|
||||
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
|
||||
|
||||
body-parser@2.2.2:
|
||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -8068,6 +8093,9 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
browser-or-node@2.1.1:
|
||||
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
|
||||
|
||||
browserslist@4.25.1:
|
||||
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
@@ -8079,12 +8107,19 @@ packages:
|
||||
bson-objectid@2.0.4:
|
||||
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:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
@@ -9271,6 +9306,9 @@ packages:
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
@@ -9486,10 +9524,17 @@ packages:
|
||||
fast-uri@3.0.6:
|
||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||
|
||||
fast-xml-builder@1.1.4:
|
||||
resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==}
|
||||
|
||||
fast-xml-parser@5.2.5:
|
||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||
hasBin: true
|
||||
|
||||
fast-xml-parser@5.5.10:
|
||||
resolution: {integrity: sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==}
|
||||
hasBin: true
|
||||
|
||||
fastq@1.19.1:
|
||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||
|
||||
@@ -11220,6 +11265,10 @@ packages:
|
||||
minimist@1.2.8:
|
||||
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:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -11329,6 +11378,16 @@ packages:
|
||||
neo-async@2.6.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
|
||||
|
||||
@@ -11717,6 +11776,10 @@ packages:
|
||||
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -13111,6 +13174,12 @@ packages:
|
||||
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -13208,6 +13277,9 @@ packages:
|
||||
strnum@2.1.1:
|
||||
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
||||
|
||||
strnum@2.2.2:
|
||||
resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==}
|
||||
|
||||
strtok3@10.3.5:
|
||||
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -13379,6 +13451,9 @@ packages:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
through2@4.0.2:
|
||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||
|
||||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
@@ -13648,6 +13723,10 @@ packages:
|
||||
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@6.24.1:
|
||||
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@7.24.4:
|
||||
resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
@@ -14173,6 +14252,10 @@ packages:
|
||||
resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
xml2js@0.6.2:
|
||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
xmlbuilder@11.0.1:
|
||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -18379,6 +18462,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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/number@1.1.1': {}
|
||||
@@ -22249,7 +22340,7 @@ snapshots:
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.1
|
||||
mime-types: 3.0.2
|
||||
negotiator: 1.0.0
|
||||
|
||||
acorn-import-attributes@1.9.5(acorn@8.16.0):
|
||||
@@ -22491,6 +22582,8 @@ snapshots:
|
||||
|
||||
async-limiter@1.0.1: {}
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
@@ -22724,6 +22817,10 @@ snapshots:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
block-stream2@2.1.0:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
|
||||
body-parser@2.2.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -22772,6 +22869,8 @@ snapshots:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
browser-or-node@2.1.1: {}
|
||||
|
||||
browserslist@4.25.1:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001727
|
||||
@@ -22785,6 +22884,8 @@ snapshots:
|
||||
|
||||
bson-objectid@2.0.4: {}
|
||||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.7.1:
|
||||
@@ -22792,6 +22893,11 @@ snapshots:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
@@ -23356,8 +23462,7 @@ snapshots:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
|
||||
decode-uri-component@0.2.2:
|
||||
optional: true
|
||||
decode-uri-component@0.2.2: {}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
@@ -24095,6 +24200,8 @@ snapshots:
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
@@ -24385,10 +24492,20 @@ snapshots:
|
||||
|
||||
fast-uri@3.0.6: {}
|
||||
|
||||
fast-xml-builder@1.1.4:
|
||||
dependencies:
|
||||
path-expression-matcher: 1.2.1
|
||||
|
||||
fast-xml-parser@5.2.5:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
@@ -24454,8 +24571,7 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
filter-obj@1.1.0:
|
||||
optional: true
|
||||
filter-obj@1.1.0: {}
|
||||
|
||||
finalhandler@1.1.2:
|
||||
dependencies:
|
||||
@@ -26613,6 +26729,22 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
minizlib@3.1.0:
|
||||
@@ -26704,6 +26836,20 @@ snapshots:
|
||||
|
||||
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:
|
||||
optional: true
|
||||
|
||||
@@ -27246,6 +27392,8 @@ snapshots:
|
||||
|
||||
path-exists@5.0.0: {}
|
||||
|
||||
path-expression-matcher@1.2.1: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
@@ -27712,7 +27860,6 @@ snapshots:
|
||||
filter-obj: 1.1.0
|
||||
split-on-first: 1.1.0
|
||||
strict-uri-encode: 2.0.0
|
||||
optional: true
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
@@ -28714,8 +28861,7 @@ snapshots:
|
||||
immutable: 4.3.8
|
||||
source-map-js: 1.2.1
|
||||
|
||||
sax@1.4.1:
|
||||
optional: true
|
||||
sax@1.4.1: {}
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
@@ -29133,8 +29279,7 @@ snapshots:
|
||||
signal-exit: 3.0.7
|
||||
which: 2.0.2
|
||||
|
||||
split-on-first@1.1.0:
|
||||
optional: true
|
||||
split-on-first@1.1.0: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
@@ -29179,10 +29324,15 @@ snapshots:
|
||||
stream-buffers@2.2.0:
|
||||
optional: true
|
||||
|
||||
stream-chain@2.2.5: {}
|
||||
|
||||
stream-json@1.9.1:
|
||||
dependencies:
|
||||
stream-chain: 2.2.5
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
strict-uri-encode@2.0.0:
|
||||
optional: true
|
||||
strict-uri-encode@2.0.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
@@ -29294,6 +29444,8 @@ snapshots:
|
||||
|
||||
strnum@2.1.1: {}
|
||||
|
||||
strnum@2.2.2: {}
|
||||
|
||||
strtok3@10.3.5:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
@@ -29532,6 +29684,10 @@ snapshots:
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
through2@4.0.2:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
@@ -29723,7 +29879,7 @@ snapshots:
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
media-typer: 1.1.0
|
||||
mime-types: 3.0.1
|
||||
mime-types: 3.0.2
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
dependencies:
|
||||
@@ -29797,6 +29953,8 @@ snapshots:
|
||||
undici@6.21.3:
|
||||
optional: true
|
||||
|
||||
undici@6.24.1: {}
|
||||
|
||||
undici@7.24.4: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||
@@ -30406,8 +30564,12 @@ snapshots:
|
||||
xmlbuilder: 11.0.1
|
||||
optional: true
|
||||
|
||||
xmlbuilder@11.0.1:
|
||||
optional: true
|
||||
xml2js@0.6.2:
|
||||
dependencies:
|
||||
sax: 1.4.1
|
||||
xmlbuilder: 11.0.1
|
||||
|
||||
xmlbuilder@11.0.1: {}
|
||||
|
||||
xmlbuilder@15.1.1:
|
||||
optional: true
|
||||
|
||||
Reference in New Issue
Block a user