9 Commits

Author SHA1 Message Date
Alejandro Gutiérrez
820ec085b2 feat(cli): v0.5.1 — message modes (push/inbox/off)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Release / Publish multi-arch images (push) Has been cancelled
--inbox: count-only notifications, no content in context
--no-messages: tools only, zero prompt injection risk
Default: push (real-time, current behavior)

Wizard shows mode picker when no flag provided.
MCP instructions tell Claude its current mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:53:41 +01:00
Alejandro Gutiérrez
9e6f6d7bc9 docs: add message modes + shared MCPs spec
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Message modes: push/inbox/off for controlling prompt injection risk.
Shared MCPs: mesh-level MCP servers proxied through the broker —
install once, every peer has access. Full architecture, DB schema,
WS protocol, credential isolation, resource limits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:52:43 +01:00
Alejandro Gutiérrez
7194e7d28e chore: regenerate lockfile from scratch
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:47:26 +01:00
Alejandro Gutiérrez
0b4e389f2b feat(web): restore payload CMS (cuidecar pattern + importMap)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Release / Publish multi-arch images (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:30:16 +01:00
Alejandro Gutiérrez
7a5f786e0c chore: sync lockfile
Some checks failed
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
2026-04-06 14:25:19 +01:00
Alejandro Gutiérrez
10e5fdcfd1 feat(web): rewrite landing for v0.3 product (groups, state, memory)
Some checks failed
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Hero: sessions form a team with groups, state, memory — not just
messaging. Features: 4 tabs with real CLI code (groups, state,
memory, coordination patterns). Use cases: team sprint with 5
agents, new-hire knowledge transfer via recall(), deploy-frozen
via shared state. All match the shipped spec (v0.3.0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:03:10 +01:00
Alejandro Gutiérrez
cc6e56aef9 docs: final spec — vectors, graph, context, tasks, streams, databases
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Full vision: claudemesh provisions shared infrastructure per mesh.
Peers share messages, state, memory, files, vector embeddings,
entity graphs, session context, tasks, structured databases, and
real-time streams. All through MCP tools, zero configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:00:50 +01:00
Alejandro Gutiérrez
1aaa483d60 feat: v0.4.0 — File sharing + multi-target messages
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Release / Publish multi-arch images (push) Has been cancelled
Files: MinIO-backed file sharing built into the broker.
share_file for persistent mesh files, send_message(file:) for
ephemeral attachments. Presigned URLs for download, access
tracking per peer.

Broker infra: MinIO in docker-compose, internal network.
HTTP POST /upload endpoint. WS handlers for get_file,
list_files, file_status, delete_file.

Multi-target: send_message(to:) accepts string or array.
Targets deduplicated before delivery.

Targeted views: MCP instructions teach Claude to send
tailored messages per audience instead of generic broadcasts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:56:01 +01:00
Alejandro Gutiérrez
99d9d19079 docs: update spec with files, multi-target, views, infra vision
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:48:32 +01:00
33 changed files with 14656 additions and 181 deletions

515
SPEC.md
View File

@@ -273,7 +273,184 @@ CREATE INDEX memory_search_idx ON mesh.memory USING gin(search_vector);
---
## 5. AI Context (CLAUDE.md)
## 5. Files
Built-in file sharing. AIs use tools, humans browse the dashboard. Same files, same storage, two interfaces.
### Two types of files
| | Message attachment | Shared file |
|---|---|---|
| Tool | `send_message(file: / files:)` | `share_file(path, tags?)` |
| Lifetime | Ephemeral — 24h or until read | Persistent — until deleted |
| Audience | Message recipients only | Entire mesh (current + future) |
| Findable | Under "Recent" for 24h | `list_files` / search by tags |
| Use case | "look at this screenshot" | "everyone needs this API spec" |
### AI view (MCP tools)
```
# Attach file to a message (ephemeral)
send_message(to: "@reviewers", message: "PR screenshot", file: "/tmp/screenshot.png")
# Attach multiple files
send_message(to: "@team", message: "PR ready", files: ["/tmp/api.ts", "/tmp/test.ts"])
# Share a persistent file with the mesh
share_file(path: "/tmp/api-contract.yaml", tags: ["api", "auth"], name: "Auth v2 Contract")
# Find files
list_files(query?: "auth", from?: "Alice")
# Download
get_file(id: "f_abc", save_to: "/tmp/")
# Check who accessed a file
file_status(id: "f_abc") → [{peer: "Alice", read: true, readAt: "..."}, ...]
# Delete a shared file
delete_file(id: "f_abc")
```
### Human view (Dashboard)
```
claudemesh / dev-team /
├── shared/ ← persistent files, grouped by tags
│ ├── auth/
│ │ ├── api-spec.yaml
│ │ └── wireframes.pdf
│ └── onboarding/
│ └── setup-guide.md
└── recent/ ← message attachments, by date
├── 2026-04-06/
│ └── screenshot-abc.png
└── 2026-04-07/
```
Tags become folders in the dashboard. Humans browse, AIs search.
### Storage
MinIO in the broker's docker-compose. Internal network, invisible to clients.
One bucket per mesh: `mesh-{meshId}`. Flat key structure:
```
mesh-{meshId}/shared/{fileId}/{original-name} ← persistent
mesh-{meshId}/ephemeral/{date}/{fileId}/{name} ← auto-cleaned 24h
```
MinIO lifecycle policy deletes `ephemeral/` after 24h.
### Access model
- Persistent files (`share_file`): accessible to all mesh members
- Ephemeral files (`send_message file:`): accessible to message recipients only
- `get_file` checks access before generating a presigned download URL
- `file_status` tracks who downloaded the file
### Upload flow
1. CLI reads local file, HTTP POSTs to `broker /upload` (multipart)
2. Broker stores in MinIO, creates `mesh.file` row
3. Broker returns file_id
4. For message attachments: file_id attached to the message push
5. Recipients see `📎 filename (size) — use get_file("id")` in the push
### DB schema
```sql
mesh.file (
id text PK,
mesh_id text FK,
name text NOT NULL,
size_bytes bigint NOT NULL,
mime_type text,
minio_key text NOT NULL,
tags text[] DEFAULT '{}',
persistent boolean DEFAULT true,
uploaded_by_name text,
uploaded_by_member text FK,
target_spec text, -- null = entire mesh, else message audience
uploaded_at timestamp DEFAULT NOW(),
expires_at timestamp, -- null for persistent, +24h for ephemeral
deleted_at timestamp
);
mesh.file_access (
id text PK,
file_id text FK,
peer_session_pubkey text,
peer_name text,
accessed_at timestamp DEFAULT NOW()
);
```
### Docker Compose (broker infra)
```yaml
services:
broker:
# ... existing broker config
environment:
MINIO_ENDPOINT: minio:9000
MINIO_ACCESS_KEY: claudemesh
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
depends_on:
- minio
minio:
image: minio/minio
command: server /data
volumes:
- minio-data:/data
environment:
MINIO_ROOT_USER: claudemesh
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
# Internal only — not exposed to the internet
volumes:
minio-data:
```
---
## 6. Multi-target messages
The `to` field accepts a string or array:
```
# Single target
send_message(to: "Alice", message: "hey")
# Multiple targets
send_message(to: ["Alice", "@backend", "Bob"], message: "sprint starts")
```
Broker resolves each target, deduplicates recipients, delivers once per peer.
---
## 7. Targeted views (MCP instruction pattern)
Not a broker feature — a convention taught via MCP instructions. When sending related information to different audiences, Claude sends tailored messages instead of one generic broadcast:
```
# Instead of:
send_message(to: "*", message: "Auth v2 ready. Check endpoints and UI.")
# Do:
send_message(to: "@frontend", message: "Auth v2: useAuth hook changed, see src/auth/")
send_message(to: "@backend", message: "Auth v2: new /api/auth/v2 endpoints, v1 deprecated 2 weeks")
send_message(to: "@pm", message: "Auth v2 done. 3 points, no blockers.")
```
Zero broker changes. Claude reads the instruction, decides when to split.
---
## 8. AI Context (MCP Instructions)
Each `claudemesh install` copies a `CLAUDEMESH.md` file to `~/.claudemesh/CLAUDEMESH.md`. Claude Code discovers it and injects it as context.
@@ -341,9 +518,9 @@ Under 2000 tokens. Tool reference only — no behavioral scripts. Claude adapts
| Tool | Description |
|------|-------------|
| `send_message(to, message, priority?)` | Send to peer name, @group, or * |
| `send_message(to, message, priority?, file?, files?)` | Send to name, @group, or * with optional file attachments |
| `check_messages()` | Drain buffered messages |
| `message_status(id)` | Check if a sent message was delivered |
| `message_status(id)` | Delivery status with per-recipient detail |
### Presence
@@ -376,9 +553,309 @@ Under 2000 tokens. Tool reference only — no behavioral scripts. Claude adapts
| `recall(query)` | Search by relevance |
| `forget(id)` | Soft-delete |
### Files
| Tool | Description |
|------|-------------|
| `share_file(path, tags?, name?)` | Share a persistent file with the mesh |
| `get_file(id, save_to)` | Download a shared file |
| `list_files(query?, from?)` | Find files shared with you |
| `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. Message Modes
Peers choose how messages reach them. Tools (state, memory, files, etc.) always work regardless of mode.
```bash
claudemesh launch --name Alice # push (default)
claudemesh launch --name Alice --inbox # held until check_messages
claudemesh launch --name Alice --no-messages # tools only, silent
```
| Mode | Messages | Prompt injection risk | Use case |
|------|----------|----------------------|----------|
| `push` | Real-time into context | Yes | Active collaboration, role-play |
| `inbox` | Count notification only | Minimal | Focused work, check when ready |
| `off` | None (check_messages manual) | Zero | Data analysis, shared infra only |
Wizard shows the choice when neither `--inbox` nor `--no-messages` is passed.
---
## 12. Shared MCPs
MCP servers installed once at the mesh level, available to all peers. The broker runs MCP processes and proxies tool calls.
### Why
Today: each peer loads MCPs from `~/.claude.json`. Four peers = four instances of the GitHub MCP, each with its own credentials, its own connection, its own state. Wasteful and inconsistent.
Mesh MCPs: the broker runs the MCP server once. Peers call tools through claudemesh. One install, every peer has access. Zero local config.
### Architecture
```
Peer A ──┐ ┌── GitHub MCP (one process)
Peer B ──┤── Broker (MCP proxy) ──┤── Postgres MCP (one process)
Peer C ──┘ └── Slack MCP (one process)
```
### Admin installs MCPs
```bash
# From a peer with admin role, or the CLI
claudemesh mcp-add --mesh dev-team github -- npx @modelcontextprotocol/server-github
claudemesh mcp-add --mesh dev-team postgres -- npx @modelcontextprotocol/server-postgres
claudemesh mcp-remove --mesh dev-team github
claudemesh mcp-list --mesh dev-team
```
Or via MCP tools (admin peers only):
```
mesh_mcp_add(name: "github", command: "npx", args: ["@modelcontextprotocol/server-github"], env: {"GITHUB_TOKEN": "..."})
mesh_mcp_remove(name: "github")
```
### Peer uses shared MCPs
```
list_mesh_mcps() → ["github (12 tools)", "postgres (8 tools)", "slack (6 tools)"]
mesh_tool(mcp: "github", tool: "search_issues", args: { query: "auth bug" })
```
Two tools. `list_mesh_mcps` for discovery, `mesh_tool` for execution. Claude reads the tool list, picks the right one, calls it.
### Broker internals
```sql
mesh.mcp_server (
id text PK,
mesh_id text FK,
name text NOT NULL,
command text NOT NULL,
args text[] DEFAULT '{}',
env jsonb DEFAULT '{}',
status text DEFAULT 'stopped',
installed_by text,
installed_at timestamp DEFAULT NOW(),
UNIQUE(mesh_id, name)
)
```
The broker:
1. Spawns each MCP as a child process with stdio transport
2. Keeps a JSON-RPC connection to each
3. On `list_mesh_mcps`: queries each MCP's `tools/list`
4. On `mesh_tool`: forwards the `tools/call` to the right MCP, returns the result
5. Restarts crashed MCPs automatically (like the WS reconnect logic)
6. Stops MCPs when the mesh has zero connected peers (resource savings)
### Credential isolation
- Env vars stored encrypted in the DB (mesh.mcp_server.env)
- Only the broker process reads them — never sent to peers
- Peers see tool names and descriptions, never credentials
- Admin can rotate credentials via `mesh_mcp_update`
### Resource limits
- Max N MCP servers per mesh (configurable, default 10)
- Max M concurrent tool calls per peer (default 5)
- Tool call timeout (default 30s)
- MCP process memory limit via Docker/cgroup
### WS protocol
| Type | Fields | Description |
|------|--------|-------------|
| `list_mesh_mcps` | — | List shared MCPs and their tools |
| `mesh_tool` | mcp, tool, args | Call a tool on a shared MCP |
| `mesh_mcp_add` | name, command, args?, env? | Install an MCP (admin) |
| `mesh_mcp_remove` | name | Uninstall an MCP (admin) |
| `mesh_mcp_list_result` | mcps[] | Response with MCP names + tool lists |
| `mesh_tool_result` | result | Tool call response |
### MCP tools for shared MCPs
| Tool | Description |
|------|-------------|
| `list_mesh_mcps()` | List shared MCPs with their tool summaries |
| `mesh_tool(mcp, tool, args)` | Execute a tool on a shared MCP |
| `mesh_mcp_add(name, command, args?, env?)` | Install a shared MCP (admin) |
| `mesh_mcp_remove(name)` | Uninstall a shared MCP (admin) |
### What this enables
- **Team onboarding**: new peer joins mesh, instantly has all team tools
- **Central credentials**: GitHub token, DB password — stored once on the broker
- **Tool standardization**: everyone uses the same MCP version, same config
- **Ephemeral peers**: a peer spun up for 5 minutes gets full tool access without any local setup
- **AI self-provisioning** (future): a peer calls `mesh_mcp_add` to install a new tool it needs
---
## 13. Encryption
### Direct messages
@@ -398,7 +875,7 @@ The session keypair generates once on first connect and survives reconnects. Mes
---
## 9. Production hardening (implemented)
## 14. Production hardening (implemented)
| Feature | Description |
|---------|-------------|
@@ -413,7 +890,7 @@ The session keypair generates once on first connect and survives reconnects. Mes
---
## 10. CLI commands
## 15. CLI commands
```
claudemesh install Register MCP server + hooks in Claude Code
@@ -442,7 +919,7 @@ claudemesh mcp Start MCP server (invoked by Claude Code, not users)
---
## 11. Implementation status
## 16. Implementation status
| Phase | Version | Status | What |
|-------|---------|--------|------|
@@ -455,14 +932,28 @@ claudemesh mcp Start MCP server (invoked by Claude Code, not users)
| Production hardening | v0.1.15 | Done | Stale sweep, decrypt fallback, sender exclusion |
| Delivery fix | v0.1.16 | Done | Same-member session message delivery |
| **Groups** | **v0.2.0** | **Done** | @group routing, roles, wizard, join/leave |
| State | v0.3.0 | Planned | Shared key-value store with push |
| Memory | v0.4.0 | Planned | Persistent knowledge with full-text search |
| AI Context | v0.2.1 | Planned | CLAUDEMESH.md shipped with CLI |
| Dashboard | v0.5.0 | Planned | Live peers, state, memory in web UI |
| **State** | **v0.3.0** | **Done** | Shared key-value store with push notifications |
| **Memory** | **v0.3.0** | **Done** | Persistent knowledge with full-text search |
| **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** | **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** | **Done** | Qdrant per-mesh collections for semantic search |
| **Graph** | **v0.5.0** | **Done** | Neo4j per-mesh databases for entity relationships |
| **Context sharing** | **v0.5.0** | **Done** | Session understanding exchange between peers |
| **Tasks** | **v0.5.0** | **Done** | First-class work items with claim/complete |
| **Mesh databases** | **v0.5.0** | **Done** | Per-mesh PostgreSQL schemas for structured data |
| **Streams** | **v0.5.0** | **Done** | Real-time pub/sub data channels |
| **mesh_info** | **v0.5.0** | **Done** | One-call aggregated mesh overview |
| Message modes | v0.5.1 | In progress | push/inbox/off modes for message delivery |
| Shared MCPs | v0.6.0 | Planned | Mesh-level MCP servers, broker as proxy |
| Dashboard | v0.7.0 | Planned | Live peers, state, memory, files, graphs in web UI |
---
## 12. Design principles
## 17. 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.

View File

@@ -15,10 +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:"
},

View File

@@ -32,9 +32,14 @@ import { db } from "./db";
import {
invite as inviteTable,
mesh,
meshFile,
meshFileAccess,
meshContext,
meshMember as memberTable,
meshMemory,
meshState,
meshStream,
meshTask,
messageQueue,
pendingStatus,
presence,
@@ -695,6 +700,526 @@ export async function forgetMemory(
);
}
// --- File sharing ---
/**
* Insert a file metadata row after upload to MinIO.
*/
export async function uploadFile(args: {
meshId: string;
name: string;
sizeBytes: number;
mimeType?: string;
minioKey: string;
tags?: string[];
persistent?: boolean;
uploadedByName?: string;
uploadedByMember?: string;
targetSpec?: string;
expiresAt?: Date;
}): Promise<string> {
const [row] = await db
.insert(meshFile)
.values({
meshId: args.meshId,
name: args.name,
sizeBytes: args.sizeBytes,
mimeType: args.mimeType ?? null,
minioKey: args.minioKey,
tags: args.tags ?? [],
persistent: args.persistent ?? true,
uploadedByName: args.uploadedByName ?? null,
uploadedByMember: args.uploadedByMember ?? null,
targetSpec: args.targetSpec ?? null,
expiresAt: args.expiresAt ?? null,
})
.returning({ id: meshFile.id });
if (!row) throw new Error("failed to insert file row");
return row.id;
}
/**
* Get a single file by id, check it belongs to the mesh and is not deleted.
*/
export async function getFile(
meshId: string,
fileId: string,
): Promise<{
id: string;
name: string;
sizeBytes: number;
mimeType: string | null;
minioKey: string;
tags: string[];
persistent: boolean;
uploadedByName: string | null;
targetSpec: string | null;
uploadedAt: Date;
} | null> {
const [row] = await db
.select({
id: meshFile.id,
name: meshFile.name,
sizeBytes: meshFile.sizeBytes,
mimeType: meshFile.mimeType,
minioKey: meshFile.minioKey,
tags: meshFile.tags,
persistent: meshFile.persistent,
uploadedByName: meshFile.uploadedByName,
targetSpec: meshFile.targetSpec,
uploadedAt: meshFile.uploadedAt,
})
.from(meshFile)
.where(
and(
eq(meshFile.id, fileId),
eq(meshFile.meshId, meshId),
isNull(meshFile.deletedAt),
),
)
.limit(1);
if (!row) return null;
return {
...row,
tags: (row.tags ?? []) as string[],
};
}
/**
* List files in a mesh. Optionally filter by query (name ILIKE) or uploader.
*/
export async function listFiles(
meshId: string,
query?: string,
from?: string,
): Promise<
Array<{
id: string;
name: string;
sizeBytes: number;
tags: string[];
uploadedBy: string;
uploadedAt: Date;
persistent: boolean;
}>
> {
const conditions = [
eq(meshFile.meshId, meshId),
isNull(meshFile.deletedAt),
];
if (query) {
conditions.push(sql`${meshFile.name} ILIKE ${"%" + query + "%"}`);
}
if (from) {
conditions.push(eq(meshFile.uploadedByName, from));
}
const rows = await db
.select({
id: meshFile.id,
name: meshFile.name,
sizeBytes: meshFile.sizeBytes,
tags: meshFile.tags,
uploadedByName: meshFile.uploadedByName,
uploadedAt: meshFile.uploadedAt,
persistent: meshFile.persistent,
})
.from(meshFile)
.where(and(...conditions))
.orderBy(desc(meshFile.uploadedAt))
.limit(100);
return rows.map((r) => ({
id: r.id,
name: r.name,
sizeBytes: r.sizeBytes,
tags: (r.tags ?? []) as string[],
uploadedBy: r.uploadedByName ?? "unknown",
uploadedAt: r.uploadedAt,
persistent: r.persistent,
}));
}
/**
* Record a file access event (download/presigned URL generation).
*/
export async function recordFileAccess(
fileId: string,
sessionPubkey?: string,
peerName?: string,
): Promise<void> {
await db.insert(meshFileAccess).values({
fileId,
peerSessionPubkey: sessionPubkey ?? null,
peerName: peerName ?? null,
});
}
/**
* Get access log for a file.
*/
export async function getFileStatus(
fileId: string,
): Promise<Array<{ peerName: string; accessedAt: Date }>> {
const rows = await db
.select({
peerName: meshFileAccess.peerName,
accessedAt: meshFileAccess.accessedAt,
})
.from(meshFileAccess)
.where(eq(meshFileAccess.fileId, fileId))
.orderBy(desc(meshFileAccess.accessedAt));
return rows.map((r) => ({
peerName: r.peerName ?? "unknown",
accessedAt: r.accessedAt,
}));
}
/**
* Soft-delete a file by setting deleted_at.
*/
export async function deleteFile(
meshId: string,
fileId: string,
): Promise<void> {
await db
.update(meshFile)
.set({ deletedAt: new Date() })
.where(
and(
eq(meshFile.id, fileId),
eq(meshFile.meshId, meshId),
isNull(meshFile.deletedAt),
),
);
}
// --- Context sharing ---
/**
* Upsert a context snapshot for a peer. Each (meshId, presenceId) pair
* has at most one context row — repeated calls update it in place.
*/
export async function shareContext(
meshId: string,
presenceId: string,
peerName: string | undefined,
summary: string,
filesRead?: string[],
keyFindings?: string[],
tags?: string[],
): Promise<string> {
const now = new Date();
// Try to find existing context for this presence in this mesh.
const [existing] = await db
.select({ id: meshContext.id })
.from(meshContext)
.where(
and(
eq(meshContext.meshId, meshId),
eq(meshContext.presenceId, presenceId),
),
)
.limit(1);
if (existing) {
await db
.update(meshContext)
.set({
peerName: peerName ?? null,
summary,
filesRead: filesRead ?? [],
keyFindings: keyFindings ?? [],
tags: tags ?? [],
updatedAt: now,
})
.where(eq(meshContext.id, existing.id));
return existing.id;
}
const [row] = await db
.insert(meshContext)
.values({
meshId,
presenceId,
peerName: peerName ?? null,
summary,
filesRead: filesRead ?? [],
keyFindings: keyFindings ?? [],
tags: tags ?? [],
updatedAt: now,
})
.returning({ id: meshContext.id });
if (!row) throw new Error("failed to insert context");
return row.id;
}
/**
* Search contexts by tag match or summary ILIKE.
*/
export async function getContext(
meshId: string,
query: string,
): Promise<
Array<{
peerName: string;
summary: string;
filesRead: string[];
keyFindings: string[];
tags: string[];
updatedAt: Date;
}>
> {
const result = await db.execute<{
peer_name: string | null;
summary: string;
files_read: string[] | null;
key_findings: string[] | null;
tags: string[] | null;
updated_at: string | Date;
}>(sql`
SELECT peer_name, summary, files_read, key_findings, tags, updated_at
FROM mesh.context
WHERE mesh_id = ${meshId}
AND (
summary ILIKE ${"%" + query + "%"}
OR ${query} = ANY(tags)
)
ORDER BY updated_at DESC
LIMIT 20
`);
const rows = (result.rows ?? result) as Array<{
peer_name: string | null;
summary: string;
files_read: string[] | null;
key_findings: string[] | null;
tags: string[] | null;
updated_at: string | Date;
}>;
return rows.map((r) => ({
peerName: r.peer_name ?? "unknown",
summary: r.summary,
filesRead: r.files_read ?? [],
keyFindings: r.key_findings ?? [],
tags: r.tags ?? [],
updatedAt:
r.updated_at instanceof Date ? r.updated_at : new Date(r.updated_at),
}));
}
/**
* List all contexts for a mesh, ordered by most recently updated.
*/
export async function listContexts(
meshId: string,
): Promise<
Array<{
peerName: string;
summary: string;
tags: string[];
updatedAt: Date;
}>
> {
const rows = await db
.select({
peerName: meshContext.peerName,
summary: meshContext.summary,
tags: meshContext.tags,
updatedAt: meshContext.updatedAt,
})
.from(meshContext)
.where(eq(meshContext.meshId, meshId))
.orderBy(desc(meshContext.updatedAt));
return rows.map((r) => ({
peerName: r.peerName ?? "unknown",
summary: r.summary,
tags: (r.tags ?? []) as string[],
updatedAt: r.updatedAt,
}));
}
// --- Tasks ---
/**
* Create a new task in a mesh. Returns the generated id.
*/
export async function createTask(
meshId: string,
title: string,
assignee?: string,
priority?: string,
tags?: string[],
createdByName?: string,
): Promise<string> {
const [row] = await db
.insert(meshTask)
.values({
meshId,
title,
assignee: assignee ?? null,
priority: priority ?? "normal",
status: "open",
tags: tags ?? [],
createdByName: createdByName ?? null,
})
.returning({ id: meshTask.id });
if (!row) throw new Error("failed to insert task");
return row.id;
}
/**
* Claim an open task. Sets status to 'claimed' and records who claimed it.
* Only succeeds if the task is currently 'open'.
*/
export async function claimTask(
meshId: string,
taskId: string,
presenceId: string,
peerName?: string,
): Promise<boolean> {
const now = new Date();
const result = await db
.update(meshTask)
.set({
status: "claimed",
claimedByPresence: presenceId,
claimedByName: peerName ?? null,
claimedAt: now,
})
.where(
and(
eq(meshTask.id, taskId),
eq(meshTask.meshId, meshId),
eq(meshTask.status, "open"),
),
)
.returning({ id: meshTask.id });
return result.length > 0;
}
/**
* Complete a task. Sets status to 'done', records the result and timestamp.
*/
export async function completeTask(
meshId: string,
taskId: string,
result?: string,
): Promise<boolean> {
const now = new Date();
const rows = await db
.update(meshTask)
.set({
status: "done",
result: result ?? null,
completedAt: now,
})
.where(
and(
eq(meshTask.id, taskId),
eq(meshTask.meshId, meshId),
),
)
.returning({ id: meshTask.id });
return rows.length > 0;
}
/**
* List tasks in a mesh with optional status and assignee filters.
*/
export async function listTasks(
meshId: string,
status?: string,
assignee?: string,
): Promise<
Array<{
id: string;
title: string;
assignee: string | null;
claimedBy: string | null;
status: string;
priority: string;
createdBy: string | null;
tags: string[];
createdAt: Date;
}>
> {
const conditions = [eq(meshTask.meshId, meshId)];
if (status) {
conditions.push(eq(meshTask.status, status));
}
if (assignee) {
conditions.push(eq(meshTask.assignee, assignee));
}
const rows = await db
.select({
id: meshTask.id,
title: meshTask.title,
assignee: meshTask.assignee,
claimedByName: meshTask.claimedByName,
status: meshTask.status,
priority: meshTask.priority,
createdByName: meshTask.createdByName,
tags: meshTask.tags,
createdAt: meshTask.createdAt,
})
.from(meshTask)
.where(and(...conditions))
.orderBy(desc(meshTask.createdAt))
.limit(100);
return rows.map((r) => ({
id: r.id,
title: r.title,
assignee: r.assignee,
claimedBy: r.claimedByName,
status: r.status,
priority: r.priority,
createdBy: r.createdByName,
tags: (r.tags ?? []) as string[],
createdAt: r.createdAt,
}));
}
// --- Streams ---
/**
* Create a named real-time stream in a mesh. Upsert semantics: if a
* stream with the same (meshId, name) already exists, return its id.
*/
export async function createStream(
meshId: string,
name: string,
createdByName: string,
): Promise<string> {
const existing = await db
.select({ id: meshStream.id })
.from(meshStream)
.where(and(eq(meshStream.meshId, meshId), eq(meshStream.name, name)));
if (existing.length > 0) return existing[0]!.id;
const [row] = await db
.insert(meshStream)
.values({ meshId, name, createdByName })
.returning({ id: meshStream.id });
return row!.id;
}
/**
* List all streams in a mesh, ordered by creation time.
*/
export async function listStreams(
meshId: string,
): Promise<
Array<{ id: string; name: string; createdBy: string | null; createdAt: Date }>
> {
return db
.select({
id: meshStream.id,
name: meshStream.name,
createdBy: meshStream.createdByName,
createdAt: meshStream.createdAt,
})
.from(meshStream)
.where(eq(meshStream.meshId, meshId))
.orderBy(asc(meshStream.createdAt));
}
// --- Message queueing + delivery ---
export interface QueueParams {
@@ -1045,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 }));
}

View File

@@ -20,6 +20,14 @@ const envSchema = z.object({
MAX_CONNECTIONS_PER_MESH: z.coerce.number().int().positive().default(100),
MAX_MESSAGE_BYTES: z.coerce.number().int().positive().default(65_536),
HOOK_RATE_LIMIT_PER_MIN: z.coerce.number().int().positive().default(30),
MINIO_ENDPOINT: z.string().default("minio:9000"),
MINIO_ACCESS_KEY: z.string().default("claudemesh"),
MINIO_SECRET_KEY: z.string().default("changeme"),
MINIO_USE_SSL: z.coerce.boolean().default(false),
QDRANT_URL: z.string().default("http://qdrant:6333"),
NEO4J_URL: z.string().default("bolt://neo4j:7687"),
NEO4J_USER: z.string().default("neo4j"),
NEO4J_PASSWORD: z.string().default("changeme"),
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),

View File

@@ -15,35 +15,57 @@
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,
handleHookSetStatus,
heartbeat,
joinGroup,
joinMesh,
leaveGroup,
listContexts,
listFiles,
listPeersInMesh,
listState,
listTasks,
queueMessage,
recallMemory,
recordFileAccess,
refreshQueueDepth,
refreshStatusFromJsonl,
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,
@@ -74,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,
@@ -140,7 +165,7 @@ function handleHttpRequest(req: IncomingMessage, res: ServerResponse): void {
const started = Date.now();
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Mesh-Id, X-Member-Id, X-File-Name, X-Tags, X-Persistent, X-Target-Spec");
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
@@ -177,6 +202,11 @@ function handleHttpRequest(req: IncomingMessage, res: ServerResponse): void {
return;
}
if (req.method === "POST" && req.url === "/upload") {
handleUploadPost(req, res, started);
return;
}
res.writeHead(404);
res.end("not found");
log.debug("http", { route, status: 404, latency_ms: Date.now() - started });
@@ -327,6 +357,119 @@ function handleJoinPost(
});
}
function handleUploadPost(
req: IncomingMessage,
res: ServerResponse,
started: number,
): void {
const meshId = req.headers["x-mesh-id"] as string | undefined;
const memberId = req.headers["x-member-id"] as string | undefined;
const fileName = req.headers["x-file-name"] as string | undefined;
const tagsRaw = req.headers["x-tags"] as string | undefined;
const persistentRaw = req.headers["x-persistent"] as string | undefined;
const targetSpec = req.headers["x-target-spec"] as string | undefined;
if (!meshId || !memberId || !fileName) {
writeJson(res, 400, {
ok: false,
error: "X-Mesh-Id, X-Member-Id, and X-File-Name headers required",
});
return;
}
const persistent = persistentRaw !== "false";
let tags: string[] = [];
if (tagsRaw) {
try {
tags = JSON.parse(tagsRaw);
} catch {
tags = [];
}
}
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
const chunks: Buffer[] = [];
let total = 0;
let aborted = false;
req.on("data", (chunk: Buffer) => {
if (aborted) return;
total += chunk.length;
if (total > MAX_UPLOAD_SIZE) {
aborted = true;
writeJson(res, 413, { ok: false, error: "file too large (max 50MB)" });
req.destroy();
return;
}
chunks.push(chunk);
});
req.on("end", async () => {
if (aborted) return;
try {
const body = Buffer.concat(chunks);
if (body.length === 0) {
writeJson(res, 400, { ok: false, error: "empty body" });
return;
}
// Generate a file ID for the MinIO key
const { generateId } = await import("@turbostarter/shared/utils");
const fileId = generateId();
const dateStr = new Date().toISOString().split("T")[0];
const keyPrefix = persistent
? `shared/${fileId}`
: `ephemeral/${dateStr}/${fileId}`;
const minioKey = `${keyPrefix}/${fileName}`;
const bucket = meshBucketName(meshId);
// Ensure bucket exists + upload
await ensureBucket(bucket);
await minioClient.putObject(
bucket,
minioKey,
body,
body.length,
req.headers["content-type"]
? { "Content-Type": req.headers["content-type"] }
: undefined,
);
// Insert DB row
const dbFileId = await uploadFile({
meshId,
name: fileName,
sizeBytes: body.length,
mimeType: (req.headers["content-type"] as string) || undefined,
minioKey,
tags,
persistent,
uploadedByMember: memberId,
targetSpec: targetSpec || undefined,
});
writeJson(res, 200, { ok: true, fileId: dbFileId });
log.info("upload", {
route: "POST /upload",
mesh_id: meshId,
file_id: dbFileId,
name: fileName,
size: body.length,
persistent,
latency_ms: Date.now() - started,
});
} catch (e) {
writeJson(res, 500, {
ok: false,
error: e instanceof Error ? e.message : String(e),
});
log.error("upload handler error", {
error: e instanceof Error ? e.message : String(e),
});
}
});
}
function handleUpgrade(
wss: WebSocketServer,
req: IncomingMessage,
@@ -775,6 +918,106 @@ function handleConnection(ws: WebSocket): void {
});
break;
}
case "get_file": {
const gf = msg as Extract<WSClientMessage, { type: "get_file" }>;
const file = await getFile(conn.meshId, gf.fileId);
if (!file) {
sendError(conn.ws, "not_found", "file not found");
break;
}
// Access control: if targetSpec is set, verify peer matches
if (file.targetSpec) {
const matches =
file.targetSpec === conn.memberPubkey ||
file.targetSpec === conn.sessionPubkey ||
file.targetSpec === "*";
if (!matches) {
sendError(conn.ws, "forbidden", "file not targeted at you");
break;
}
}
// Generate presigned URL (60s expiry)
const bucket = meshBucketName(conn.meshId);
const presignedUrl = await minioClient.presignedGetObject(
bucket,
file.minioKey,
60,
);
// Record access
const memberInfo = conn.memberPubkey
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
: null;
await recordFileAccess(
gf.fileId,
conn.sessionPubkey ?? undefined,
memberInfo?.displayName,
);
sendToPeer(presenceId, {
type: "file_url",
fileId: gf.fileId,
url: presignedUrl,
name: file.name,
});
log.info("ws get_file", {
presence_id: presenceId,
file_id: gf.fileId,
});
break;
}
case "list_files": {
const lf = msg as Extract<WSClientMessage, { type: "list_files" }>;
const files = await listFiles(conn.meshId, lf.query, lf.from);
sendToPeer(presenceId, {
type: "file_list",
files: files.map((f) => ({
id: f.id,
name: f.name,
size: f.sizeBytes,
tags: f.tags,
uploadedBy: f.uploadedBy,
uploadedAt: f.uploadedAt.toISOString(),
persistent: f.persistent,
})),
});
log.info("ws list_files", {
presence_id: presenceId,
mesh_id: conn.meshId,
count: files.length,
});
break;
}
case "file_status": {
const fs = msg as Extract<WSClientMessage, { type: "file_status" }>;
const accesses = await getFileStatus(fs.fileId);
sendToPeer(presenceId, {
type: "file_status_result",
fileId: fs.fileId,
accesses: accesses.map((a) => ({
peerName: a.peerName,
accessedAt: a.accessedAt.toISOString(),
})),
});
log.info("ws file_status", {
presence_id: presenceId,
file_id: fs.fileId,
});
break;
}
case "delete_file": {
const df = msg as Extract<WSClientMessage, { type: "delete_file" }>;
await deleteFile(conn.meshId, df.fileId);
sendToPeer(presenceId, {
type: "ack" as const,
id: df.fileId,
messageId: df.fileId,
queued: false,
});
log.info("ws delete_file", {
presence_id: presenceId,
file_id: df.fileId,
});
break;
}
case "message_status": {
const ms = msg as Extract<WSClientMessage, { type: "message_status" }>;
// Look up the message in the queue.
@@ -841,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" });
@@ -856,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 });
}
});

28
apps/broker/src/minio.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* MinIO client for file storage.
*
* Each mesh gets its own bucket (mesh-{meshId}). Files are stored under
* a key path that encodes persistence and origin:
* - persistent: shared/{fileId}/{originalName}
* - ephemeral: ephemeral/{YYYY-MM-DD}/{fileId}/{originalName}
*/
import { Client } from "minio";
import { env } from "./env";
export const minioClient = new Client({
endPoint: env.MINIO_ENDPOINT.split(":")[0]!,
port: parseInt(env.MINIO_ENDPOINT.split(":")[1] || "9000"),
useSSL: env.MINIO_USE_SSL,
accessKey: env.MINIO_ACCESS_KEY,
secretKey: env.MINIO_SECRET_KEY,
});
export async function ensureBucket(name: string): Promise<void> {
const exists = await minioClient.bucketExists(name);
if (!exists) await minioClient.makeBucket(name);
}
export function meshBucketName(meshId: string): string {
return `mesh-${meshId.toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
}

View File

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

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

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

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ interface LaunchArgs {
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
joinLink: string | null;
meshSlug: string | null;
messageMode: "push" | "inbox" | "off" | null;
quiet: boolean;
skipPermConfirm: boolean;
claudeArgs: string[];
@@ -38,6 +39,7 @@ function parseArgs(argv: string[]): LaunchArgs {
groups: null,
joinLink: null,
meshSlug: null,
messageMode: null,
quiet: false,
skipPermConfirm: false,
claudeArgs: [],
@@ -66,6 +68,10 @@ function parseArgs(argv: string[]): LaunchArgs {
result.meshSlug = argv[++i]!;
} else if (arg.startsWith("--mesh=")) {
result.meshSlug = arg.slice("--mesh=".length);
} else if (arg === "--inbox") {
result.messageMode = "inbox";
} else if (arg === "--no-messages") {
result.messageMode = "off";
} else if (arg === "--quiet") {
result.quiet = true;
} else if (arg === "-y" || arg === "--yes") {
@@ -171,7 +177,7 @@ async function confirmPermissions(): Promise<void> {
// --- Banner ---
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[]): void {
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[], messageMode: "push" | "inbox" | "off"): void {
const useColor =
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
@@ -183,9 +189,15 @@ function printBanner(name: string, meshSlug: string, role: string | null, groups
: "";
const rule = "─".repeat(60);
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags}`));
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags} [${messageMode}]`));
console.log(rule);
if (messageMode === "push") {
console.log("Peer messages arrive as <channel> reminders in real-time.");
} else if (messageMode === "inbox") {
console.log("Peer messages held in inbox. Use check_messages to read.");
} else {
console.log("Messages off. Use check_messages to poll manually.");
}
console.log("Peers send text only — they cannot call tools or read files.");
console.log(dim(`Config: ${getConfigPath()}`));
console.log(rule);
@@ -263,6 +275,8 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
let role: string | null = args.role;
let parsedGroups: GroupEntry[] = args.groups ? parseGroupsString(args.groups) : [];
let messageMode: "push" | "inbox" | "off" = args.messageMode ?? "push";
if (!args.quiet) {
if (role === null) {
const answer = await askLine(" Role (optional): ");
@@ -272,6 +286,18 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
const answer = await askLine(" Groups (comma-separated, optional): ");
if (answer) parsedGroups = parseGroupsString(answer);
}
if (args.messageMode === null) {
console.log("\n Message mode:");
console.log(" 1) Push (real-time, peers can interrupt your work)");
console.log(" 2) Inbox (held until you check, notification only)");
console.log(" 3) Off (tools only, no messages)");
console.log("");
const answer = await askLine(" Choice [1]: ");
const choice = parseInt(answer || "1", 10);
if (choice === 2) messageMode = "inbox";
else if (choice === 3) messageMode = "off";
else messageMode = "push";
}
if (role || parsedGroups.length) console.log("");
}
@@ -293,6 +319,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
meshes: [mesh],
displayName,
...(parsedGroups.length > 0 ? { groups: parsedGroups } : {}),
messageMode,
};
writeFileSync(
join(tmpDir, "config.json"),
@@ -302,7 +329,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
// 5. Banner + permission confirmation.
if (!args.quiet) {
printBanner(displayName, mesh.slug, role, parsedGroups);
printBanner(displayName, mesh.slug, role, parsedGroups, messageMode);
// Auto-permissions confirmation — needed for autonomous peer messaging.
if (!args.skipPermConfirm) {
await confirmPermissions();

View File

@@ -131,6 +131,7 @@ export async function startMcpServer(): Promise<void> {
const myName = config.displayName ?? "unnamed";
const myGroups = (config.groups ?? []).map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ') || "none";
const messageMode = config.messageMode ?? "push";
const server = new Server(
{ name: "claudemesh", version: "0.3.0" },
@@ -161,9 +162,44 @@ When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATEL
| remember(content, tags?) | Store persistent knowledge with optional tags. |
| recall(query) | Full-text search over mesh memory. |
| forget(id) | Soft-delete a memory entry. |
| share_file(path, name?, tags?) | Share a persistent file with the mesh. |
| get_file(id, save_to) | Download a shared file to a local path. |
| list_files(query?, from?) | Find files shared in the mesh. |
| file_status(id) | Check who has accessed a file. |
| delete_file(id) | Remove a shared file from the mesh. |
| vector_store(collection, text, metadata?) | Store embedding in per-mesh Qdrant collection. |
| vector_search(collection, query, limit?) | Semantic search over stored embeddings. |
| vector_delete(collection, id) | Remove an embedding. |
| list_collections() | List vector collections in this mesh. |
| graph_query(cypher) | Read-only Cypher query on per-mesh Neo4j. |
| graph_execute(cypher) | Write Cypher query (CREATE, MERGE, DELETE). |
| mesh_query(sql) | Run a SELECT query on the per-mesh shared database. |
| mesh_execute(sql) | Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE). |
| mesh_schema() | List tables and columns in the per-mesh shared database. |
| create_stream(name) | Create a real-time data stream in the mesh. |
| publish(stream, data) | Push data to a stream. Subscribers receive it in real-time. |
| subscribe(stream) | Subscribe to a stream. Data pushes arrive as channel notifications. |
| list_streams() | List active streams in the mesh. |
| share_context(summary, files_read?, key_findings?, tags?) | Share session understanding with peers. |
| get_context(query) | Find context from peers who explored an area. |
| list_contexts() | See what all peers currently know. |
| create_task(title, assignee?, priority?, tags?) | Create a work item. |
| claim_task(id) | Claim an unclaimed task. |
| complete_task(id, result?) | Mark task done with optional result. |
| list_tasks(status?, assignee?) | List tasks filtered by status/assignee. |
If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
Multi-target: send_message accepts an array of targets for the 'to' field.
send_message(to: ["Alice", "@backend"], message: "sprint starts")
Targets are deduplicated — each peer receives the message once.
Targeted views: when different audiences need different details about the same event,
send tailored messages instead of one generic broadcast:
send_message(to: "@frontend", message: "Auth v2: useAuth hook changed, see src/auth/")
send_message(to: "@backend", message: "Auth v2: new /api/auth/v2 endpoints, v1 deprecated")
send_message(to: "@pm", message: "Auth v2 done. 3 points, no blockers.")
## Groups
Groups are routing labels. Send to @groupname to multicast to all members. Roles are metadata that peers interpret: a "lead" gathers input before synthesizing a response, a "member" contributes when asked, an "observer" watches silently. Join and leave groups dynamically with join_group/leave_group. Check list_peers to see who belongs to which groups and their roles.
@@ -173,13 +209,41 @@ Shared key-value store scoped to the mesh. Use get_state/set_state for live coor
## Memory
Persistent knowledge that survives across sessions. Use remember(content, tags?) to store lessons, decisions, and incidents. Use recall(query) to search before asking peers. New peers should recall at session start to load institutional knowledge.
## Files
share_file for persistent references, send_message(file:) for ephemeral attachments.
Tags on shared files make them searchable. Use list_files to find what peers shared.
## Vectors
Store and search semantic embeddings. Use vector_store to index content, vector_search to find similar content.
## Graph
Build and query entity relationship graphs. Use graph_execute for writes (CREATE, MERGE), graph_query for reads (MATCH).
## Mesh Database
Per-mesh PostgreSQL database. Use mesh_execute for DDL/DML (CREATE TABLE, INSERT), mesh_query for SELECT, mesh_schema to inspect tables. Schema auto-created on first use.
## Streams
Real-time data channels. create_stream to start one, publish to push data, subscribe to receive pushes. Use for build logs, deploy status, live metrics.
## Context
Share your session understanding with peers. Use share_context after exploring a codebase area. Check get_context before re-reading files another peer already analyzed.
## Tasks
Create and claim work items. create_task to propose work, claim_task to take ownership, complete_task when done. Prevents duplicate effort.
## Priority
- "now": interrupt immediately, even if recipient is in DND (use for urgent: broken deploy, blocking issue)
- "next" (default): deliver when recipient goes idle (normal coordination)
- "low": pull-only via check_messages (FYI, non-blocking context)
## Coordination
Call list_peers at session start to understand who is online, their roles, and what they are working on. If you are a group lead, gather input from members before responding to external requests — do not answer alone. If you are a member, contribute to your lead when asked. Use @group messages for team-wide questions, direct messages for 1:1 coordination. Set a meaningful summary so peers know your current focus.`,
Call list_peers at session start to understand who is online, their roles, and what they are working on. If you are a group lead, gather input from members before responding to external requests — do not answer alone. If you are a member, contribute to your lead when asked. Use @group messages for team-wide questions, direct messages for 1:1 coordination. Set a meaningful summary so peers know your current focus.
## Message Mode
Your message mode is "${messageMode}".
- push: messages arrive in real-time as channel notifications. Respond immediately.
- inbox: messages are held. You'll see "[inbox] New message from X" notifications. Call check_messages to read them.
- off: no message notifications. Use check_messages manually to poll.`,
},
);
@@ -201,22 +265,32 @@ Call list_peers at session start to understand who is online, their roles, and w
const { to, message, priority } = (args ?? {}) as SendMessageArgs;
if (!to || !message)
return text("send_message: `to` and `message` required", true);
const { client, targetSpec, error } = await resolveClient(to);
if (!client)
return text(`send_message: ${error ?? "no client resolved"}`, true);
// Handle multi-target: to can be string or string[]
const targets = Array.isArray(to) ? to : [to];
const results: string[] = [];
const seen = new Set<string>(); // dedup by resolved pubkey
for (const target of targets) {
const { client, targetSpec, error } = await resolveClient(target);
if (!client) {
results.push(`${target}: ${error ?? "no client resolved"}`);
continue;
}
if (seen.has(targetSpec)) continue; // dedup
seen.add(targetSpec);
const result = await client.send(
targetSpec,
message,
(priority ?? "next") as Priority,
);
if (!result.ok)
return text(
`send_message failed (${client.meshSlug}): ${result.error}`,
true,
);
return text(
`Sent to ${targetSpec} via ${client.meshSlug} [${priority ?? "next"}] → ${result.messageId}`,
);
if (!result.ok) {
results.push(`${target}: ${result.error}`);
} else {
results.push(`${target}${result.messageId}`);
}
}
return text(results.join("\n"));
}
case "list_peers": {
@@ -363,6 +437,276 @@ Call list_peers at session start to understand who is online, their roles, and w
return text(`Forgotten: ${id}`);
}
// --- Files ---
case "share_file": {
const { path: filePath, name: fileName, tags } = (args ?? {}) as { path?: string; name?: string; tags?: string[] };
if (!filePath) return text("share_file: `path` required", true);
const { existsSync } = await import("node:fs");
if (!existsSync(filePath)) return text(`share_file: file not found: ${filePath}`, true);
const client = allClients()[0];
if (!client) return text("share_file: not connected", true);
const fileId = await client.uploadFile(filePath, client.meshId, client.meshSlug, {
name: fileName, tags, persistent: true,
});
if (!fileId) return text("share_file: upload failed", true);
return text(`Shared: ${fileName ?? filePath} (${fileId})`);
}
case "get_file": {
const { id, save_to } = (args ?? {}) as { id?: string; save_to?: string };
if (!id || !save_to) return text("get_file: `id` and `save_to` required", true);
const client = allClients()[0];
if (!client) return text("get_file: not connected", true);
const result = await client.getFile(id);
if (!result) return text(`get_file: file ${id} not found`, true);
const res = await fetch(result.url, { signal: AbortSignal.timeout(30_000) });
if (!res.ok) return text(`get_file: download failed (${res.status})`, true);
const { writeFileSync, mkdirSync } = await import("node:fs");
const { dirname } = await import("node:path");
mkdirSync(dirname(save_to), { recursive: true });
writeFileSync(save_to, Buffer.from(await res.arrayBuffer()));
return text(`Downloaded: ${result.name}${save_to}`);
}
case "list_files": {
const { query, from } = (args ?? {}) as { query?: string; from?: string };
const client = allClients()[0];
if (!client) return text("list_files: not connected", true);
const files = await client.listFiles(query, from);
if (files.length === 0) return text("No files found.");
const lines = files.map(f =>
`- **${f.name}** (${f.id.slice(0, 8)}…, ${f.size} bytes) by ${f.uploadedBy}${f.tags.length ? ` [${f.tags.join(", ")}]` : ""}`
);
return text(lines.join("\n"));
}
case "file_status": {
const { id } = (args ?? {}) as { id?: string };
if (!id) return text("file_status: `id` required", true);
const client = allClients()[0];
if (!client) return text("file_status: not connected", true);
const accesses = await client.fileStatus(id);
if (accesses.length === 0) return text("No one has accessed this file yet.");
const lines = accesses.map(a => `- ${a.peerName} at ${a.accessedAt}`);
return text(`Accessed by:\n${lines.join("\n")}`);
}
case "delete_file": {
const { id } = (args ?? {}) as { id?: string };
if (!id) return text("delete_file: `id` required", true);
const client = allClients()[0];
if (!client) return text("delete_file: not connected", true);
await client.deleteFile(id);
return text(`Deleted: ${id}`);
}
// --- Vectors ---
case "vector_store": {
const { collection, text: storeText, metadata } = (args ?? {}) as { collection?: string; text?: string; metadata?: Record<string, unknown> };
if (!collection || !storeText) return text("vector_store: `collection` and `text` required", true);
const client = allClients()[0];
if (!client) return text("vector_store: not connected", true);
const id = await client.vectorStore(collection, storeText, metadata);
return text(`Stored in ${collection}${id ? ` (${id})` : ""}`);
}
case "vector_search": {
const { collection, query, limit } = (args ?? {}) as { collection?: string; query?: string; limit?: number };
if (!collection || !query) return text("vector_search: `collection` and `query` required", true);
const client = allClients()[0];
if (!client) return text("vector_search: not connected", true);
const results = await client.vectorSearch(collection, query, limit);
if (results.length === 0) return text(`No results in ${collection} for "${query}".`);
const lines = results.map(r => `- [${r.id.slice(0, 8)}…] (score: ${r.score.toFixed(3)}) ${r.text.slice(0, 120)}${r.text.length > 120 ? "…" : ""}`);
return text(`${results.length} result(s) in ${collection}:\n${lines.join("\n")}`);
}
case "vector_delete": {
const { collection, id } = (args ?? {}) as { collection?: string; id?: string };
if (!collection || !id) return text("vector_delete: `collection` and `id` required", true);
const client = allClients()[0];
if (!client) return text("vector_delete: not connected", true);
await client.vectorDelete(collection, id);
return text(`Deleted ${id} from ${collection}`);
}
case "list_collections": {
const client = allClients()[0];
if (!client) return text("list_collections: not connected", true);
const collections = await client.listCollections();
if (collections.length === 0) return text("No vector collections.");
return text(`Collections:\n${collections.map(c => `- ${c}`).join("\n")}`);
}
// --- Graph ---
case "graph_query": {
const { cypher } = (args ?? {}) as { cypher?: string };
if (!cypher) return text("graph_query: `cypher` required", true);
const client = allClients()[0];
if (!client) return text("graph_query: not connected", true);
const rows = await client.graphQuery(cypher);
if (rows.length === 0) return text("No results.");
return text(JSON.stringify(rows, null, 2));
}
case "graph_execute": {
const { cypher } = (args ?? {}) as { cypher?: string };
if (!cypher) return text("graph_execute: `cypher` required", true);
const client = allClients()[0];
if (!client) return text("graph_execute: not connected", true);
const rows = await client.graphExecute(cypher);
return text(rows.length > 0 ? JSON.stringify(rows, null, 2) : "Executed successfully.");
}
// --- Context ---
case "share_context": {
const { summary, files_read, key_findings, tags } = (args ?? {}) as { summary?: string; files_read?: string[]; key_findings?: string[]; tags?: string[] };
if (!summary) return text("share_context: `summary` required", true);
const client = allClients()[0];
if (!client) return text("share_context: not connected", true);
await client.shareContext(summary, files_read, key_findings, tags);
return text(`Context shared: "${summary.slice(0, 80)}${summary.length > 80 ? "…" : ""}"`);
}
case "get_context": {
const { query } = (args ?? {}) as { query?: string };
if (!query) return text("get_context: `query` required", true);
const client = allClients()[0];
if (!client) return text("get_context: not connected", true);
const contexts = await client.getContext(query);
if (contexts.length === 0) return text(`No context found for "${query}".`);
const lines = contexts.map(c => {
const files = c.filesRead.length ? `\n Files: ${c.filesRead.join(", ")}` : "";
const findings = c.keyFindings.length ? `\n Findings: ${c.keyFindings.join("; ")}` : "";
return `- **${c.peerName}** (${c.updatedAt}): ${c.summary}${files}${findings}`;
});
return text(`${contexts.length} context(s):\n${lines.join("\n")}`);
}
case "list_contexts": {
const client = allClients()[0];
if (!client) return text("list_contexts: not connected", true);
const contexts = await client.listContexts();
if (contexts.length === 0) return text("No peer contexts shared yet.");
const lines = contexts.map(c => `- **${c.peerName}**: ${c.summary}${c.tags.length ? ` [${c.tags.join(", ")}]` : ""}`);
return text(`Peer contexts:\n${lines.join("\n")}`);
}
// --- Tasks ---
case "create_task": {
const { title, assignee, priority, tags } = (args ?? {}) as { title?: string; assignee?: string; priority?: string; tags?: string[] };
if (!title) return text("create_task: `title` required", true);
const client = allClients()[0];
if (!client) return text("create_task: not connected", true);
const id = await client.createTask(title, assignee, priority, tags);
return text(`Task created${id ? ` (${id})` : ""}: "${title}"${assignee ? `${assignee}` : ""}`);
}
case "claim_task": {
const { id } = (args ?? {}) as { id?: string };
if (!id) return text("claim_task: `id` required", true);
const client = allClients()[0];
if (!client) return text("claim_task: not connected", true);
await client.claimTask(id);
return text(`Claimed task: ${id}`);
}
case "complete_task": {
const { id, result } = (args ?? {}) as { id?: string; result?: string };
if (!id) return text("complete_task: `id` required", true);
const client = allClients()[0];
if (!client) return text("complete_task: not connected", true);
await client.completeTask(id, result);
return text(`Completed task: ${id}${result ? `${result}` : ""}`);
}
case "list_tasks": {
const { status, assignee } = (args ?? {}) as { status?: string; assignee?: string };
const client = allClients()[0];
if (!client) return text("list_tasks: not connected", true);
const tasks = await client.listTasks(status, assignee);
if (tasks.length === 0) return text("No tasks found.");
const lines = tasks.map(t => `- [${t.id.slice(0, 8)}…] **${t.title}** (${t.status}, ${t.priority}) ${t.assignee ? `${t.assignee}` : "unassigned"} (by ${t.createdBy})`);
return text(`${tasks.length} task(s):\n${lines.join("\n")}`);
}
// --- Mesh Database ---
case "mesh_query": {
const { sql: querySql } = (args ?? {}) as { sql?: string };
if (!querySql) return text("mesh_query: `sql` required", true);
const client = allClients()[0];
if (!client) return text("mesh_query: not connected", true);
const result = await client.meshQuery(querySql);
if (!result) return text("mesh_query: query failed or timed out", true);
if (result.rows.length === 0) return text(`Query returned 0 rows.`);
const header = `| ${result.columns.join(" | ")} |`;
const sep = `| ${result.columns.map(() => "---").join(" | ")} |`;
const rows = result.rows.map(r => `| ${result.columns.map(c => String(r[c] ?? "")).join(" | ")} |`);
return text(`${result.rowCount} row(s):\n${header}\n${sep}\n${rows.join("\n")}`);
}
case "mesh_execute": {
const { sql: execSql } = (args ?? {}) as { sql?: string };
if (!execSql) return text("mesh_execute: `sql` required", true);
const client = allClients()[0];
if (!client) return text("mesh_execute: not connected", true);
await client.meshExecute(execSql);
return text(`Executed.`);
}
case "mesh_schema": {
const client = allClients()[0];
if (!client) return text("mesh_schema: not connected", true);
const tables = await client.meshSchema();
if (!tables || tables.length === 0) return text("No tables in mesh database.");
const lines = tables.map(t => `**${t.name}**: ${t.columns.map(c => `${c.name} (${c.type}${c.nullable ? ", nullable" : ""})`).join(", ")}`);
return text(lines.join("\n"));
}
// --- Streams ---
case "create_stream": {
const { name: streamName } = (args ?? {}) as { name?: string };
if (!streamName) return text("create_stream: `name` required", true);
const client = allClients()[0];
if (!client) return text("create_stream: not connected", true);
const streamId = await client.createStream(streamName);
return text(`Stream created: ${streamName}${streamId ? ` (${streamId})` : ""}`);
}
case "publish": {
const { stream: pubStream, data: pubData } = (args ?? {}) as { stream?: string; data?: unknown };
if (!pubStream) return text("publish: `stream` required", true);
const client = allClients()[0];
if (!client) return text("publish: not connected", true);
await client.publish(pubStream, pubData);
return text(`Published to ${pubStream}.`);
}
case "subscribe": {
const { stream: subStream } = (args ?? {}) as { stream?: string };
if (!subStream) return text("subscribe: `stream` required", true);
const client = allClients()[0];
if (!client) return text("subscribe: not connected", true);
await client.subscribe(subStream);
return text(`Subscribed to ${subStream}. Data pushes will arrive as channel notifications.`);
}
case "list_streams": {
const client = allClients()[0];
if (!client) return text("list_streams: not connected", true);
const streams = await client.listStreams();
if (streams.length === 0) return text("No active streams.");
const lines = streams.map(s => `- **${s.name}** (${s.id.slice(0, 8)}…) by ${s.createdBy}, ${s.subscriberCount} subscriber(s)`);
return text(lines.join("\n"));
}
case "mesh_info": {
const client = allClients()[0];
if (!client) return text("mesh_info: not connected", true);
const info = await client.meshInfo();
if (!info) return text("mesh_info: timed out", true);
const lines = [
`**Mesh**: ${info.mesh}`,
`**Peers**: ${info.peers}`,
`**Groups**: ${(info.groups as string[])?.join(", ") || "none"}`,
`**State keys**: ${(info.stateKeys as string[])?.join(", ") || "none"}`,
`**Memories**: ${info.memoryCount}`,
`**Files**: ${info.fileCount}`,
`**Tasks**: open=${(info.tasks as any)?.open ?? 0}, claimed=${(info.tasks as any)?.claimed ?? 0}, done=${(info.tasks as any)?.done ?? 0}`,
`**Streams**: ${(info.streams as string[])?.join(", ") || "none"}`,
`**Tables**: ${(info.tables as string[])?.join(", ") || "none"}`,
`**Your name**: ${info.yourName}`,
`**Your groups**: ${(info.yourGroups as any[])?.map((g: any) => `@${g.name}${g.role ? ':' + g.role : ''}`).join(", ") || "none"}`,
];
return text(lines.join("\n"));
}
default:
return text(`Unknown tool: ${name}`, true);
}
@@ -379,11 +723,31 @@ Call list_peers at session start to understand who is online, their roles, and w
// system reminder injected into Claude Code's context.
for (const client of allClients()) {
client.onPush(async (msg) => {
// In "off" mode, silently skip notification — messages are still
// buffered in pushBuffer and accessible via check_messages.
if (messageMode === "off") return;
const fromPubkey = msg.senderPubkey || "";
// Resolve sender's display name from the cached peer list.
const fromName = fromPubkey
? await resolvePeerName(client, fromPubkey)
: "unknown";
if (messageMode === "inbox") {
// Count-only notification, no content
try {
await server.notification({
method: "notifications/claude/channel",
params: {
content: `[inbox] New message from ${fromName}. Use check_messages to read.`,
meta: { kind: "inbox_notification", from_name: fromName },
},
});
} catch { /* best effort */ }
return;
}
// push mode — full content notification
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
try {
await server.notification({
@@ -407,6 +771,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({

View File

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

View File

@@ -6,7 +6,7 @@ export type Priority = "now" | "next" | "low";
export type PeerStatus = "idle" | "working" | "dnd";
export interface SendMessageArgs {
to: string; // peer name, pubkey, or #channel
to: string | string[]; // peer name, pubkey, @group, or array of targets
message: string;
priority?: Priority;
}

View File

@@ -38,6 +38,7 @@ export interface Config {
meshes: JoinedMesh[];
displayName?: string; // per-session override, written by `claudemesh launch --name`
groups?: GroupEntry[];
messageMode?: "push" | "inbox" | "off";
}
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
@@ -53,7 +54,7 @@ export function loadConfig(): Config {
if (!parsed || !Array.isArray(parsed.meshes)) {
return { version: 1, meshes: [] };
}
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups };
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups, messageMode: parsed.messageMode };
} catch (e) {
throw new Error(
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,

View File

@@ -412,6 +412,22 @@ export class BrokerClient {
/** Check delivery status of a sent message. */
private messageStatusResolvers: Array<(result: { messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null) => void> = [];
private fileUrlResolvers: Array<(result: { url: string; name: string } | null) => void> = [];
private fileListResolvers: Array<(files: Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) => void> = [];
private fileStatusResolvers: Array<(accesses: Array<{ peerName: string; accessedAt: string }>) => void> = [];
private vectorStoredResolvers: Array<(id: string | null) => void> = [];
private vectorResultsResolvers: Array<(results: Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>) => void> = [];
private collectionListResolvers: Array<(collections: string[]) => void> = [];
private graphResultResolvers: Array<(rows: Array<Record<string, unknown>>) => void> = [];
private contextListResolvers: Array<(contexts: Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>) => void> = [];
private contextResultsResolvers: Array<(contexts: Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>) => void> = [];
private taskCreatedResolvers: Array<(id: string | null) => void> = [];
private taskListResolvers: Array<(tasks: Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>) => void> = [];
private meshQueryResolvers: Array<(result: { columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number } | null) => void> = [];
private meshSchemaResolvers: Array<(tables: Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>) => void> = [];
private streamCreatedResolvers: Array<(id: string | null) => void> = [];
private streamListResolvers: Array<(streams: Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>) => void> = [];
private streamDataHandlers = new Set<(data: { stream: string; data: unknown; publishedBy: string }) => void>();
async messageStatus(messageId: string): Promise<{ messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
@@ -425,12 +441,351 @@ export class BrokerClient {
});
}
// --- Files ---
/** Get a download URL for a shared file. */
async getFile(fileId: string): Promise<{ url: string; name: string } | null> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
return new Promise((resolve) => {
this.fileUrlResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "get_file", fileId }));
setTimeout(() => {
const idx = this.fileUrlResolvers.indexOf(resolve);
if (idx !== -1) {
this.fileUrlResolvers.splice(idx, 1);
resolve(null);
}
}, 5_000);
});
}
/** List files shared in the mesh. */
async listFiles(query?: string, from?: string): Promise<Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.fileListResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "list_files", query, from }));
setTimeout(() => {
const idx = this.fileListResolvers.indexOf(resolve);
if (idx !== -1) {
this.fileListResolvers.splice(idx, 1);
resolve([]);
}
}, 5_000);
});
}
/** Check who has accessed a shared file. */
async fileStatus(fileId: string): Promise<Array<{ peerName: string; accessedAt: string }>> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
return new Promise((resolve) => {
this.fileStatusResolvers.push(resolve);
this.ws!.send(JSON.stringify({ type: "file_status", fileId }));
setTimeout(() => {
const idx = this.fileStatusResolvers.indexOf(resolve);
if (idx !== -1) {
this.fileStatusResolvers.splice(idx, 1);
resolve([]);
}
}, 5_000);
});
}
/** Delete a shared file from the mesh. */
async deleteFile(fileId: string): Promise<void> {
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
this.ws.send(JSON.stringify({ type: "delete_file", fileId }));
}
/** Upload a file to the broker via HTTP POST. Returns file ID or null. */
async uploadFile(filePath: string, meshId: string, memberId: string, opts: {
name?: string; tags?: string[]; persistent?: boolean; targetSpec?: string;
}): Promise<string | null> {
const { readFileSync } = await import("node:fs");
const { basename } = await import("node:path");
const data = readFileSync(filePath);
const fileName = opts.name ?? basename(filePath);
// Convert WS broker URL to HTTP
const brokerHttp = this.mesh.brokerUrl
.replace("wss://", "https://")
.replace("ws://", "http://")
.replace("/ws", "");
const res = await fetch(`${brokerHttp}/upload`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"X-Mesh-Id": meshId,
"X-Member-Id": memberId,
"X-File-Name": fileName,
"X-Tags": JSON.stringify(opts.tags ?? []),
"X-Persistent": String(opts.persistent ?? true),
"X-Target-Spec": opts.targetSpec ?? "",
},
body: data,
signal: AbortSignal.timeout(30_000),
});
const body = await res.json() as { ok?: boolean; fileId?: string };
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);
@@ -583,6 +938,123 @@ export class BrokerClient {
if (resolver) resolver(msg as any);
return;
}
if (msg.type === "file_url") {
const resolver = this.fileUrlResolvers.shift();
if (resolver) {
if (msg.url) {
resolver({ url: String(msg.url), name: String(msg.name ?? "") });
} else {
resolver(null);
}
}
return;
}
if (msg.type === "file_list") {
const files = (msg.files as Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) ?? [];
const resolver = this.fileListResolvers.shift();
if (resolver) resolver(files);
return;
}
if (msg.type === "file_status_result") {
const accesses = (msg.accesses as Array<{ peerName: string; accessedAt: string }>) ?? [];
const resolver = this.fileStatusResolvers.shift();
if (resolver) resolver(accesses);
return;
}
if (msg.type === "vector_stored") {
const resolver = this.vectorStoredResolvers.shift();
if (resolver) resolver(msg.id ? String(msg.id) : null);
return;
}
if (msg.type === "vector_results") {
const results = (msg.results as Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>) ?? [];
const resolver = this.vectorResultsResolvers.shift();
if (resolver) resolver(results);
return;
}
if (msg.type === "collection_list") {
const collections = (msg.collections as string[]) ?? [];
const resolver = this.collectionListResolvers.shift();
if (resolver) resolver(collections);
return;
}
if (msg.type === "graph_result") {
const rows = (msg.rows as Array<Record<string, unknown>>) ?? [];
const resolver = this.graphResultResolvers.shift();
if (resolver) resolver(rows);
return;
}
if (msg.type === "context_list") {
const contexts = (msg.contexts as Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>) ?? [];
const resolver = this.contextListResolvers.shift();
if (resolver) resolver(contexts);
return;
}
if (msg.type === "context_results") {
const contexts = (msg.contexts as Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>) ?? [];
const resolver = this.contextResultsResolvers.shift();
if (resolver) resolver(contexts);
return;
}
if (msg.type === "task_created") {
const resolver = this.taskCreatedResolvers.shift();
if (resolver) resolver(msg.id ? String(msg.id) : null);
return;
}
if (msg.type === "task_list") {
const tasks = (msg.tasks as Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>) ?? [];
const resolver = this.taskListResolvers.shift();
if (resolver) resolver(tasks);
return;
}
if (msg.type === "mesh_query_result") {
const resolver = this.meshQueryResolvers.shift();
if (resolver) {
if (msg.columns) {
resolver({
columns: (msg.columns as string[]) ?? [],
rows: (msg.rows as Array<Record<string, unknown>>) ?? [],
rowCount: (msg.rowCount as number) ?? 0,
});
} else {
resolver(null);
}
}
return;
}
if (msg.type === "mesh_schema_result") {
const tables = (msg.tables as Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>) ?? [];
const resolver = this.meshSchemaResolvers.shift();
if (resolver) resolver(tables);
return;
}
if (msg.type === "stream_created") {
const resolver = this.streamCreatedResolvers.shift();
if (resolver) resolver(msg.id ? String(msg.id) : null);
return;
}
if (msg.type === "stream_list") {
const streams = (msg.streams as Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>) ?? [];
const resolver = this.streamListResolvers.shift();
if (resolver) resolver(streams);
return;
}
if (msg.type === "stream_data") {
const evt = {
stream: String(msg.stream ?? ""),
data: msg.data,
publishedBy: String(msg.publishedBy ?? ""),
};
for (const h of this.streamDataHandlers) {
try { h(evt); } catch { /* handler errors are not the transport's problem */ }
}
return;
}
if (msg.type === "mesh_info_result") {
const resolver = this.meshInfoResolvers.shift();
if (resolver) resolver(msg as Record<string, unknown>);
return;
}
if (msg.type === "error") {
this.debug(`broker error: ${msg.code} ${msg.message}`);
const id = msg.id ? String(msg.id) : null;

View File

@@ -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));

View File

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

View File

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

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.",
},
];

View File

@@ -28,6 +28,61 @@ services:
networks:
- claudemesh-internal
minio:
image: minio/minio
command: server /data --console-address ":9001"
restart: always
volumes:
- minio-data:/data
environment:
MINIO_ROOT_USER: claudemesh
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-changeme}
expose:
- "9000"
networks:
- claudemesh-internal
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 15s
timeout: 5s
start_period: 10s
retries: 3
qdrant:
image: qdrant/qdrant
restart: always
volumes:
- qdrant-data:/qdrant/storage
expose:
- "6333"
networks:
- claudemesh-internal
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:6333/readyz"]
interval: 15s
timeout: 5s
retries: 3
neo4j:
image: neo4j:5
restart: always
environment:
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-changeme}
NEO4J_PLUGINS: '[]'
volumes:
- neo4j-data:/data
expose:
- "7687"
- "7474"
networks:
- claudemesh-internal
healthcheck:
test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "${NEO4J_PASSWORD:-changeme}", "RETURN 1"]
interval: 15s
timeout: 5s
start_period: 30s
retries: 3
broker:
image: ${BROKER_IMAGE:-claudemesh-broker:latest}
restart: always
@@ -40,11 +95,26 @@ services:
MAX_CONNECTIONS_PER_MESH: ${MAX_CONNECTIONS_PER_MESH:-100}
MAX_MESSAGE_BYTES: ${MAX_MESSAGE_BYTES:-65536}
HOOK_RATE_LIMIT_PER_MIN: ${HOOK_RATE_LIMIT_PER_MIN:-30}
MINIO_ENDPOINT: minio:9000
MINIO_ACCESS_KEY: claudemesh
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-changeme}
MINIO_USE_SSL: "false"
QDRANT_URL: http://qdrant:6333
NEO4J_URL: bolt://neo4j:7687
NEO4J_USER: neo4j
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-changeme}
expose:
- "7900"
networks:
- coolify
- claudemesh-internal
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
@@ -85,6 +155,11 @@ services:
start_period: 20s
retries: 3
volumes:
minio-data:
qdrant-data:
neo4j-data:
networks:
# Coolify's shared Traefik network — must already exist on the host
coolify:

View File

@@ -0,0 +1,28 @@
CREATE TABLE "mesh"."file" (
"id" text PRIMARY KEY NOT NULL,
"mesh_id" text NOT NULL,
"name" text NOT NULL,
"size_bytes" integer NOT NULL,
"mime_type" text,
"minio_key" text NOT NULL,
"tags" text[] DEFAULT '{}',
"persistent" boolean DEFAULT true NOT NULL,
"uploaded_by_name" text,
"uploaded_by_member" text,
"target_spec" text,
"uploaded_at" timestamp DEFAULT now() NOT NULL,
"expires_at" timestamp,
"deleted_at" timestamp
);
--> statement-breakpoint
CREATE TABLE "mesh"."file_access" (
"id" text PRIMARY KEY NOT NULL,
"file_id" text NOT NULL,
"peer_session_pubkey" text,
"peer_name" text,
"accessed_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "mesh"."file" ADD CONSTRAINT "file_uploaded_by_member_member_id_fk" FOREIGN KEY ("uploaded_by_member") REFERENCES "mesh"."member"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mesh"."file_access" ADD CONSTRAINT "file_access_file_id_file_id_fk" FOREIGN KEY ("file_id") REFERENCES "mesh"."file"("id") ON DELETE cascade ON UPDATE no action;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,27 @@
"when": 1775477883426,
"tag": "0008_add-state-and-memory",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1775480008546,
"tag": "0009_add-file-tables",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1775480729014,
"tag": "0010_add-context-and-tasks",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1775481222701,
"tag": "0011_add-streams",
"breakpoints": true
}
]
}

View File

@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm";
import {
boolean,
integer,
jsonb,
pgSchema,
@@ -289,6 +290,105 @@ export const meshMemory = meshSchema.table("memory", {
forgottenAt: timestamp(),
});
/**
* File metadata for shared files in a mesh. Actual bytes live in MinIO;
* this table tracks ownership, access control, and soft-deletion.
*/
export const meshFile = meshSchema.table("file", {
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
name: text().notNull(),
sizeBytes: integer().notNull(),
mimeType: text(),
minioKey: text().notNull(),
tags: text().array().default([]),
persistent: boolean().notNull().default(true),
uploadedByName: text(),
uploadedByMember: text().references(() => meshMember.id),
targetSpec: text(), // null = entire mesh
uploadedAt: timestamp().defaultNow().notNull(),
expiresAt: timestamp(),
deletedAt: timestamp(),
});
/**
* Access log for file downloads. Tracks which peer accessed which file
* and when, for auditability and read-receipt semantics.
*/
export const meshFileAccess = meshSchema.table("file_access", {
id: text().primaryKey().notNull().$defaultFn(generateId),
fileId: text()
.references(() => meshFile.id, { onDelete: "cascade" })
.notNull(),
peerSessionPubkey: text(),
peerName: text(),
accessedAt: timestamp().defaultNow().notNull(),
});
/**
* Per-peer context snapshot. Each peer (presence) has at most one context
* entry per mesh, upserted on each share_context call. Allows peers to
* discover what others are working on, which files they've read, and
* key findings — without sending a direct message.
*/
export const meshContext = meshSchema.table("context", {
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
presenceId: text().references(() => presence.id, { onDelete: "cascade" }),
peerName: text(),
summary: text().notNull(),
filesRead: text().array().default([]),
keyFindings: text().array().default([]),
tags: text().array().default([]),
updatedAt: timestamp().defaultNow().notNull(),
});
/**
* Mesh-scoped task board. Peers can create tasks, claim them, and mark
* them done. Lightweight project management for multi-agent workflows.
*/
export const meshTask = meshSchema.table("task", {
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
title: text().notNull(),
assignee: text(),
claimedByName: text(),
claimedByPresence: text().references(() => presence.id),
priority: text().notNull().default("normal"),
status: text().notNull().default("open"),
tags: text().array().default([]),
result: text(),
createdByName: text(),
createdAt: timestamp().defaultNow().notNull(),
claimedAt: timestamp(),
completedAt: timestamp(),
});
/**
* Named real-time data channels within a mesh. One peer publishes, all
* subscribers receive. No message history — streams are live.
* Use cases: build logs, deploy status, monitoring data, live code diffs.
*/
export const meshStream = meshSchema.table(
"stream",
{
id: text().primaryKey().notNull().$defaultFn(generateId),
meshId: text()
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
.notNull(),
name: text().notNull(),
createdByName: text(),
createdAt: timestamp().defaultNow().notNull(),
},
(table) => [uniqueIndex("stream_mesh_name_idx").on(table.meshId, table.name)],
);
export const meshRelations = relations(mesh, ({ one, many }) => ({
owner: one(user, {
fields: [mesh.ownerUserId],
@@ -367,6 +467,25 @@ export const meshMemoryRelations = relations(meshMemory, ({ one }) => ({
}),
}));
export const meshFileRelations = relations(meshFile, ({ one, many }) => ({
mesh: one(mesh, {
fields: [meshFile.meshId],
references: [mesh.id],
}),
uploader: one(meshMember, {
fields: [meshFile.uploadedByMember],
references: [meshMember.id],
}),
accesses: many(meshFileAccess),
}));
export const meshFileAccessRelations = relations(meshFileAccess, ({ one }) => ({
file: one(meshFile, {
fields: [meshFileAccess.fileId],
references: [meshFile.id],
}),
}));
export const selectMeshSchema = createSelectSchema(mesh);
export const insertMeshSchema = createInsertSchema(mesh);
export const selectMemberSchema = createSelectSchema(meshMember);
@@ -404,3 +523,53 @@ export type SelectMeshState = typeof meshState.$inferSelect;
export type InsertMeshState = typeof meshState.$inferInsert;
export type SelectMeshMemory = typeof meshMemory.$inferSelect;
export type InsertMeshMemory = typeof meshMemory.$inferInsert;
export const selectMeshFileSchema = createSelectSchema(meshFile);
export const insertMeshFileSchema = createInsertSchema(meshFile);
export const selectMeshFileAccessSchema = createSelectSchema(meshFileAccess);
export const insertMeshFileAccessSchema = createInsertSchema(meshFileAccess);
export type SelectMeshFile = typeof meshFile.$inferSelect;
export type InsertMeshFile = typeof meshFile.$inferInsert;
export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect;
export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert;
export const selectMeshContextSchema = createSelectSchema(meshContext);
export const insertMeshContextSchema = createInsertSchema(meshContext);
export const selectMeshTaskSchema = createSelectSchema(meshTask);
export const insertMeshTaskSchema = createInsertSchema(meshTask);
export type SelectMeshContext = typeof meshContext.$inferSelect;
export type InsertMeshContext = typeof meshContext.$inferInsert;
export type SelectMeshTask = typeof meshTask.$inferSelect;
export type InsertMeshTask = typeof meshTask.$inferInsert;
export const meshContextRelations = relations(meshContext, ({ one }) => ({
mesh: one(mesh, {
fields: [meshContext.meshId],
references: [mesh.id],
}),
presence: one(presence, {
fields: [meshContext.presenceId],
references: [presence.id],
}),
}));
export const meshTaskRelations = relations(meshTask, ({ one }) => ({
mesh: one(mesh, {
fields: [meshTask.meshId],
references: [mesh.id],
}),
claimedPresence: one(presence, {
fields: [meshTask.claimedByPresence],
references: [presence.id],
}),
}));
export const meshStreamRelations = relations(meshStream, ({ one }) => ({
mesh: one(mesh, {
fields: [meshStream.meshId],
references: [mesh.id],
}),
}));
export const selectMeshStreamSchema = createSelectSchema(meshStream);
export const insertMeshStreamSchema = createInsertSchema(meshStream);
export type SelectMeshStream = typeof meshStream.$inferSelect;
export type InsertMeshStream = typeof meshStream.$inferInsert;

246
pnpm-lock.yaml generated
View File

@@ -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==}
@@ -13644,8 +13719,8 @@ packages:
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
undici@6.21.3:
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
undici@6.24.1:
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
engines: {node: '>=18.17'}
undici@7.24.4:
@@ -14173,6 +14248,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'}
@@ -16649,7 +16728,7 @@ snapshots:
structured-headers: 0.4.1
tar: 7.5.2
terminal-link: 2.1.1
undici: 6.21.3
undici: 6.24.1
wrap-ansi: 7.0.0
ws: 8.20.0
optionalDependencies:
@@ -18379,6 +18458,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': {}
@@ -20879,7 +20966,7 @@ snapshots:
'@sentry/bundler-plugin-core': 4.6.1(encoding@0.1.13)
unplugin: 1.0.1
uuid: 9.0.1
webpack: 5.100.2
webpack: 5.100.2(esbuild@0.25.0)
transitivePeerDependencies:
- encoding
- supports-color
@@ -22249,7 +22336,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 +22578,8 @@ snapshots:
async-limiter@1.0.1: {}
async@3.2.6: {}
asynckit@0.4.0: {}
atomic-sleep@1.0.0: {}
@@ -22724,6 +22813,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 +22865,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 +22880,8 @@ snapshots:
bson-objectid@2.0.4: {}
buffer-crc32@1.0.0: {}
buffer-from@1.1.2: {}
buffer@5.7.1:
@@ -22792,6 +22889,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 +23458,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 +24196,8 @@ snapshots:
eventemitter3@4.0.7: {}
eventemitter3@5.0.4: {}
events@3.3.0: {}
eventsource-parser@3.0.6: {}
@@ -24385,10 +24488,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 +24567,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 +26725,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 +26832,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
@@ -26813,7 +26955,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.2
'@next/swc-darwin-x64': 16.2.2
@@ -27246,6 +27388,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 +27856,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 +28857,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 +29275,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 +29320,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 +29440,8 @@ snapshots:
strnum@2.1.1: {}
strnum@2.2.2: {}
strtok3@10.3.5:
dependencies:
'@tokenizer/token': 0.3.0
@@ -29329,12 +29477,6 @@ snapshots:
react: 19.2.3
optionalDependencies:
'@babel/core': 7.28.5
optional: true
styled-jsx@5.1.6(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.3
styleq@0.1.3:
optional: true
@@ -29492,15 +29634,6 @@ snapshots:
optionalDependencies:
esbuild: 0.25.0
terser-webpack-plugin@5.3.14(webpack@5.100.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.2
serialize-javascript: 6.0.2
terser: 5.43.1
webpack: 5.100.2
terser@5.43.1:
dependencies:
'@jridgewell/source-map': 0.3.10
@@ -29532,6 +29665,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 +29860,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:
@@ -29794,8 +29931,7 @@ snapshots:
undici-types@7.8.0: {}
undici@6.21.3:
optional: true
undici@6.24.1: {}
undici@7.24.4: {}
@@ -30200,38 +30336,6 @@ snapshots:
webpack-virtual-modules@0.5.0: {}
webpack@5.100.2:
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
'@types/json-schema': 7.0.15
'@webassemblyjs/ast': 1.14.1
'@webassemblyjs/wasm-edit': 1.14.1
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.16.0
acorn-import-phases: 1.0.4(acorn@8.16.0)
browserslist: 4.25.1
chrome-trace-event: 1.0.4
enhanced-resolve: 5.18.3
es-module-lexer: 1.7.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
loader-runner: 4.3.0
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 4.3.2
tapable: 2.2.2
terser-webpack-plugin: 5.3.14(webpack@5.100.2)
watchpack: 2.4.4
webpack-sources: 3.3.3
transitivePeerDependencies:
- '@swc/core'
- esbuild
- uglify-js
webpack@5.100.2(esbuild@0.25.0):
dependencies:
'@types/eslint-scope': 3.7.7
@@ -30406,8 +30510,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