Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab08be04a5 | ||
|
|
ee585a8370 | ||
|
|
1f078bf0c8 | ||
|
|
2372032a68 | ||
|
|
a70c5fd124 | ||
|
|
5c62d287cf | ||
|
|
9ae378c2e3 | ||
|
|
7381738f0b | ||
|
|
8c6b0c0e07 | ||
|
|
ec9626503c | ||
|
|
820ec085b2 | ||
|
|
9e6f6d7bc9 | ||
|
|
7194e7d28e | ||
|
|
0b4e389f2b | ||
|
|
7a5f786e0c | ||
|
|
10e5fdcfd1 | ||
|
|
cc6e56aef9 |
372
SPEC.md
372
SPEC.md
@@ -563,9 +563,355 @@ Under 2000 tokens. Tool reference only — no behavioral scripts. Claude adapts
|
|||||||
| `file_status(id)` | Who accessed this file |
|
| `file_status(id)` | Who accessed this file |
|
||||||
| `delete_file(id)` | Remove a shared 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. Claude Code Integration — How Push Delivery Works
|
||||||
|
|
||||||
|
Understanding how Claude Code processes channel notifications is critical for claudemesh reliability.
|
||||||
|
|
||||||
|
### The notification pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
MCP server (claudemesh-cli)
|
||||||
|
└─ server.notification("notifications/claude/channel", { content, meta })
|
||||||
|
└─ writes JSON-RPC to stdout
|
||||||
|
└─ Claude Code reads from MCP process stdout
|
||||||
|
└─ setNotificationHandler fires
|
||||||
|
└─ enqueue({ mode: "prompt", value: wrappedContent, origin: { kind: "channel" } })
|
||||||
|
└─ React useSyncExternalStore triggers re-render
|
||||||
|
└─ useQueueProcessor effect fires
|
||||||
|
└─ processQueueIfReady() → executeInput()
|
||||||
|
└─ Claude sees ← claudemesh: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key requirements (from Claude Code source)
|
||||||
|
|
||||||
|
1. **Feature gate**: `feature('KAIROS') || feature('KAIROS_CHANNELS')` must be true. `KAIROS_CHANNELS` is external (GrowthBook). `--dangerously-load-development-channels` sets `entry.dev = true` which bypasses the allowlist check but still requires the feature gate.
|
||||||
|
|
||||||
|
2. **OAuth auth required**: Channel notifications require `claude.ai` authentication (OAuth tokens). API key users are blocked. This means `claude login --for-claude-ai` must have been run.
|
||||||
|
|
||||||
|
3. **Server name must match**: The MCP server's declared name (`new Server({ name: "claudemesh" })`) must match the channel entry from `--dangerously-load-development-channels server:claudemesh`.
|
||||||
|
|
||||||
|
4. **Meta keys**: Must match `/^[a-zA-Z_][a-zA-Z0-9_]*$/`. No hyphens. All values must be strings.
|
||||||
|
|
||||||
|
5. **Capability declaration**: Server must declare `experimental: { "claude/channel": {} }` in capabilities.
|
||||||
|
|
||||||
|
6. **Queue processing is event-driven**: `enqueue()` triggers a React store update → `useEffect` fires → processes immediately. No polling needed on the Claude Code side. The 1s poll timer in claudemesh is for draining the WS push buffer into notifications — Claude Code handles the rest instantly.
|
||||||
|
|
||||||
|
### Priority gating on the broker
|
||||||
|
|
||||||
|
The broker holds `"next"` and `"low"` priority messages when the peer's status is `"working"`. Only `"now"` messages deliver immediately regardless of status. This is by design — but can cause perceived "push not working" when the hook reports `working` status.
|
||||||
|
|
||||||
|
```
|
||||||
|
Status: idle → delivers: now, next, low
|
||||||
|
Status: working → delivers: now only
|
||||||
|
Status: dnd → delivers: now only
|
||||||
|
```
|
||||||
|
|
||||||
|
If a peer appears to not receive messages, check their status in `list_peers`. A peer stuck in `"working"` (e.g., stale hook) will only receive `"now"` priority messages.
|
||||||
|
|
||||||
|
### Common issues
|
||||||
|
|
||||||
|
| Symptom | Likely cause |
|
||||||
|
|---------|-------------|
|
||||||
|
| Messages never arrive | Session started before CLI update — restart with `claudemesh launch` |
|
||||||
|
| Messages arrive with 5+ minute delay | Peer status stuck on `"working"` — `next` messages held until idle |
|
||||||
|
| `← claudemesh:` never appears in idle session | Feature gate `KAIROS_CHANNELS` not enabled, or not OAuth-authenticated |
|
||||||
|
| Messages arrive only on `check_messages` | Channel handler not registered — check `--dangerously-load-development-channels` flag |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Encryption
|
||||||
|
|
||||||
### Direct messages
|
### Direct messages
|
||||||
|
|
||||||
@@ -585,7 +931,7 @@ The session keypair generates once on first connect and survives reconnects. Mes
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Production hardening (implemented)
|
## 14. Production hardening (implemented)
|
||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
@@ -600,7 +946,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
|
claudemesh install Register MCP server + hooks in Claude Code
|
||||||
@@ -629,7 +975,7 @@ claudemesh mcp Start MCP server (invoked by Claude Code, not users)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Implementation status
|
## 16. Implementation status
|
||||||
|
|
||||||
| Phase | Version | Status | What |
|
| Phase | Version | Status | What |
|
||||||
|-------|---------|--------|------|
|
|-------|---------|--------|------|
|
||||||
@@ -647,13 +993,23 @@ claudemesh mcp Start MCP server (invoked by Claude Code, not users)
|
|||||||
| **Message status** | **v0.3.0** | **Done** | Per-recipient delivery detail |
|
| **Message status** | **v0.3.0** | **Done** | Per-recipient delivery detail |
|
||||||
| **MCP instructions** | **v0.3.0** | **Done** | Dynamic identity, full tool guide, coordination patterns |
|
| **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 |
|
| **Multicast fix** | **v0.3.0** | **Done** | Broadcast/group push directly, not queue race |
|
||||||
| Files | v0.4.0 | Planned | MinIO-backed file sharing + message attachments |
|
| **Files** | **v0.4.0** | **Done** | MinIO-backed file sharing + message attachments |
|
||||||
| Multi-target | v0.4.0 | Planned | Array `to` field with deduplication |
|
| **Multi-target** | **v0.4.0** | **Done** | Array `to` field with deduplication |
|
||||||
| Dashboard | v0.5.0 | Planned | Live peers, state, memory, files in web UI |
|
| **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.
|
1. **The broker is a dumb pipe.** It routes messages, stores state, holds memory. It does not interpret roles, enforce protocols, or run agents.
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@turbostarter/prettier-config",
|
"prettier": "@turbostarter/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@qdrant/js-client-rest": "1.17.0",
|
||||||
"@turbostarter/db": "workspace:*",
|
"@turbostarter/db": "workspace:*",
|
||||||
"@turbostarter/shared": "workspace:*",
|
"@turbostarter/shared": "workspace:*",
|
||||||
"drizzle-orm": "0.44.7",
|
"drizzle-orm": "0.44.7",
|
||||||
"libsodium-wrappers": "0.7.15",
|
"libsodium-wrappers": "0.7.15",
|
||||||
"minio": "8.0.7",
|
"minio": "8.0.7",
|
||||||
|
"neo4j-driver": "6.0.1",
|
||||||
"ws": "8.20.0",
|
"ws": "8.20.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ import {
|
|||||||
mesh,
|
mesh,
|
||||||
meshFile,
|
meshFile,
|
||||||
meshFileAccess,
|
meshFileAccess,
|
||||||
|
meshContext,
|
||||||
meshMember as memberTable,
|
meshMember as memberTable,
|
||||||
meshMemory,
|
meshMemory,
|
||||||
meshState,
|
meshState,
|
||||||
|
meshStream,
|
||||||
|
meshTask,
|
||||||
messageQueue,
|
messageQueue,
|
||||||
pendingStatus,
|
pendingStatus,
|
||||||
presence,
|
presence,
|
||||||
@@ -889,6 +892,334 @@ export async function deleteFile(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Context sharing ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert a context snapshot for a peer. Each (meshId, presenceId) pair
|
||||||
|
* has at most one context row — repeated calls update it in place.
|
||||||
|
*/
|
||||||
|
export async function shareContext(
|
||||||
|
meshId: string,
|
||||||
|
presenceId: string,
|
||||||
|
peerName: string | undefined,
|
||||||
|
summary: string,
|
||||||
|
filesRead?: string[],
|
||||||
|
keyFindings?: string[],
|
||||||
|
tags?: string[],
|
||||||
|
): Promise<string> {
|
||||||
|
const now = new Date();
|
||||||
|
// Try to find existing context for this presence in this mesh.
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: meshContext.id })
|
||||||
|
.from(meshContext)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(meshContext.meshId, meshId),
|
||||||
|
eq(meshContext.presenceId, presenceId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await db
|
||||||
|
.update(meshContext)
|
||||||
|
.set({
|
||||||
|
peerName: peerName ?? null,
|
||||||
|
summary,
|
||||||
|
filesRead: filesRead ?? [],
|
||||||
|
keyFindings: keyFindings ?? [],
|
||||||
|
tags: tags ?? [],
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.where(eq(meshContext.id, existing.id));
|
||||||
|
return existing.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.insert(meshContext)
|
||||||
|
.values({
|
||||||
|
meshId,
|
||||||
|
presenceId,
|
||||||
|
peerName: peerName ?? null,
|
||||||
|
summary,
|
||||||
|
filesRead: filesRead ?? [],
|
||||||
|
keyFindings: keyFindings ?? [],
|
||||||
|
tags: tags ?? [],
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.returning({ id: meshContext.id });
|
||||||
|
if (!row) throw new Error("failed to insert context");
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search contexts by tag match or summary ILIKE.
|
||||||
|
*/
|
||||||
|
export async function getContext(
|
||||||
|
meshId: string,
|
||||||
|
query: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
peerName: string;
|
||||||
|
summary: string;
|
||||||
|
filesRead: string[];
|
||||||
|
keyFindings: string[];
|
||||||
|
tags: string[];
|
||||||
|
updatedAt: Date;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const result = await db.execute<{
|
||||||
|
peer_name: string | null;
|
||||||
|
summary: string;
|
||||||
|
files_read: string[] | null;
|
||||||
|
key_findings: string[] | null;
|
||||||
|
tags: string[] | null;
|
||||||
|
updated_at: string | Date;
|
||||||
|
}>(sql`
|
||||||
|
SELECT peer_name, summary, files_read, key_findings, tags, updated_at
|
||||||
|
FROM mesh.context
|
||||||
|
WHERE mesh_id = ${meshId}
|
||||||
|
AND (
|
||||||
|
summary ILIKE ${"%" + query + "%"}
|
||||||
|
OR ${query} = ANY(tags)
|
||||||
|
)
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 20
|
||||||
|
`);
|
||||||
|
const rows = (result.rows ?? result) as Array<{
|
||||||
|
peer_name: string | null;
|
||||||
|
summary: string;
|
||||||
|
files_read: string[] | null;
|
||||||
|
key_findings: string[] | null;
|
||||||
|
tags: string[] | null;
|
||||||
|
updated_at: string | Date;
|
||||||
|
}>;
|
||||||
|
return rows.map((r) => ({
|
||||||
|
peerName: r.peer_name ?? "unknown",
|
||||||
|
summary: r.summary,
|
||||||
|
filesRead: r.files_read ?? [],
|
||||||
|
keyFindings: r.key_findings ?? [],
|
||||||
|
tags: r.tags ?? [],
|
||||||
|
updatedAt:
|
||||||
|
r.updated_at instanceof Date ? r.updated_at : new Date(r.updated_at),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all contexts for a mesh, ordered by most recently updated.
|
||||||
|
*/
|
||||||
|
export async function listContexts(
|
||||||
|
meshId: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
peerName: string;
|
||||||
|
summary: string;
|
||||||
|
tags: string[];
|
||||||
|
updatedAt: Date;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
peerName: meshContext.peerName,
|
||||||
|
summary: meshContext.summary,
|
||||||
|
tags: meshContext.tags,
|
||||||
|
updatedAt: meshContext.updatedAt,
|
||||||
|
})
|
||||||
|
.from(meshContext)
|
||||||
|
.where(eq(meshContext.meshId, meshId))
|
||||||
|
.orderBy(desc(meshContext.updatedAt));
|
||||||
|
return rows.map((r) => ({
|
||||||
|
peerName: r.peerName ?? "unknown",
|
||||||
|
summary: r.summary,
|
||||||
|
tags: (r.tags ?? []) as string[],
|
||||||
|
updatedAt: r.updatedAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tasks ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new task in a mesh. Returns the generated id.
|
||||||
|
*/
|
||||||
|
export async function createTask(
|
||||||
|
meshId: string,
|
||||||
|
title: string,
|
||||||
|
assignee?: string,
|
||||||
|
priority?: string,
|
||||||
|
tags?: string[],
|
||||||
|
createdByName?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const [row] = await db
|
||||||
|
.insert(meshTask)
|
||||||
|
.values({
|
||||||
|
meshId,
|
||||||
|
title,
|
||||||
|
assignee: assignee ?? null,
|
||||||
|
priority: priority ?? "normal",
|
||||||
|
status: "open",
|
||||||
|
tags: tags ?? [],
|
||||||
|
createdByName: createdByName ?? null,
|
||||||
|
})
|
||||||
|
.returning({ id: meshTask.id });
|
||||||
|
if (!row) throw new Error("failed to insert task");
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim an open task. Sets status to 'claimed' and records who claimed it.
|
||||||
|
* Only succeeds if the task is currently 'open'.
|
||||||
|
*/
|
||||||
|
export async function claimTask(
|
||||||
|
meshId: string,
|
||||||
|
taskId: string,
|
||||||
|
presenceId: string,
|
||||||
|
peerName?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const now = new Date();
|
||||||
|
const result = await db
|
||||||
|
.update(meshTask)
|
||||||
|
.set({
|
||||||
|
status: "claimed",
|
||||||
|
claimedByPresence: presenceId,
|
||||||
|
claimedByName: peerName ?? null,
|
||||||
|
claimedAt: now,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(meshTask.id, taskId),
|
||||||
|
eq(meshTask.meshId, meshId),
|
||||||
|
eq(meshTask.status, "open"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning({ id: meshTask.id });
|
||||||
|
return result.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete a task. Sets status to 'done', records the result and timestamp.
|
||||||
|
*/
|
||||||
|
export async function completeTask(
|
||||||
|
meshId: string,
|
||||||
|
taskId: string,
|
||||||
|
result?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const now = new Date();
|
||||||
|
const rows = await db
|
||||||
|
.update(meshTask)
|
||||||
|
.set({
|
||||||
|
status: "done",
|
||||||
|
result: result ?? null,
|
||||||
|
completedAt: now,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(meshTask.id, taskId),
|
||||||
|
eq(meshTask.meshId, meshId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.returning({ id: meshTask.id });
|
||||||
|
return rows.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List tasks in a mesh with optional status and assignee filters.
|
||||||
|
*/
|
||||||
|
export async function listTasks(
|
||||||
|
meshId: string,
|
||||||
|
status?: string,
|
||||||
|
assignee?: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
assignee: string | null;
|
||||||
|
claimedBy: string | null;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
createdBy: string | null;
|
||||||
|
tags: string[];
|
||||||
|
createdAt: Date;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const conditions = [eq(meshTask.meshId, meshId)];
|
||||||
|
if (status) {
|
||||||
|
conditions.push(eq(meshTask.status, status));
|
||||||
|
}
|
||||||
|
if (assignee) {
|
||||||
|
conditions.push(eq(meshTask.assignee, assignee));
|
||||||
|
}
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: meshTask.id,
|
||||||
|
title: meshTask.title,
|
||||||
|
assignee: meshTask.assignee,
|
||||||
|
claimedByName: meshTask.claimedByName,
|
||||||
|
status: meshTask.status,
|
||||||
|
priority: meshTask.priority,
|
||||||
|
createdByName: meshTask.createdByName,
|
||||||
|
tags: meshTask.tags,
|
||||||
|
createdAt: meshTask.createdAt,
|
||||||
|
})
|
||||||
|
.from(meshTask)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(desc(meshTask.createdAt))
|
||||||
|
.limit(100);
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
title: r.title,
|
||||||
|
assignee: r.assignee,
|
||||||
|
claimedBy: r.claimedByName,
|
||||||
|
status: r.status,
|
||||||
|
priority: r.priority,
|
||||||
|
createdBy: r.createdByName,
|
||||||
|
tags: (r.tags ?? []) as string[],
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Streams ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a named real-time stream in a mesh. Upsert semantics: if a
|
||||||
|
* stream with the same (meshId, name) already exists, return its id.
|
||||||
|
*/
|
||||||
|
export async function createStream(
|
||||||
|
meshId: string,
|
||||||
|
name: string,
|
||||||
|
createdByName: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const existing = await db
|
||||||
|
.select({ id: meshStream.id })
|
||||||
|
.from(meshStream)
|
||||||
|
.where(and(eq(meshStream.meshId, meshId), eq(meshStream.name, name)));
|
||||||
|
if (existing.length > 0) return existing[0]!.id;
|
||||||
|
const [row] = await db
|
||||||
|
.insert(meshStream)
|
||||||
|
.values({ meshId, name, createdByName })
|
||||||
|
.returning({ id: meshStream.id });
|
||||||
|
return row!.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all streams in a mesh, ordered by creation time.
|
||||||
|
*/
|
||||||
|
export async function listStreams(
|
||||||
|
meshId: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{ id: string; name: string; createdBy: string | null; createdAt: Date }>
|
||||||
|
> {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
id: meshStream.id,
|
||||||
|
name: meshStream.name,
|
||||||
|
createdBy: meshStream.createdByName,
|
||||||
|
createdAt: meshStream.createdAt,
|
||||||
|
})
|
||||||
|
.from(meshStream)
|
||||||
|
.where(eq(meshStream.meshId, meshId))
|
||||||
|
.orderBy(asc(meshStream.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Message queueing + delivery ---
|
// --- Message queueing + delivery ---
|
||||||
|
|
||||||
export interface QueueParams {
|
export interface QueueParams {
|
||||||
@@ -1239,3 +1570,118 @@ export async function findMemberByPubkey(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
return row ?? null;
|
return row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mesh databases (per-mesh PostgreSQL schemas) ---
|
||||||
|
|
||||||
|
function meshSchemaName(meshId: string): string {
|
||||||
|
return `meshdb_${meshId.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate that user-provided SQL doesn't contain dangerous operations. */
|
||||||
|
function validateMeshSql(userSql: string): void {
|
||||||
|
const upper = userSql.toUpperCase();
|
||||||
|
const forbidden = [
|
||||||
|
"DROP SCHEMA",
|
||||||
|
"CREATE SCHEMA",
|
||||||
|
"SET SEARCH_PATH",
|
||||||
|
"SET ROLE",
|
||||||
|
"SET SESSION",
|
||||||
|
"SET LOCAL",
|
||||||
|
"GRANT",
|
||||||
|
"REVOKE",
|
||||||
|
];
|
||||||
|
for (const f of forbidden) {
|
||||||
|
if (upper.includes(f))
|
||||||
|
throw new Error(`Forbidden SQL operation: ${f}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensure the per-mesh schema exists. */
|
||||||
|
export async function ensureMeshSchema(meshId: string): Promise<string> {
|
||||||
|
const schema = meshSchemaName(meshId);
|
||||||
|
await db.execute(
|
||||||
|
sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw('"' + schema + '"')}`,
|
||||||
|
);
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run a SELECT query in the mesh's schema. */
|
||||||
|
export async function meshQuery(
|
||||||
|
meshId: string,
|
||||||
|
query: string,
|
||||||
|
): Promise<{
|
||||||
|
columns: string[];
|
||||||
|
rows: Array<Record<string, unknown>>;
|
||||||
|
rowCount: number;
|
||||||
|
}> {
|
||||||
|
validateMeshSql(query);
|
||||||
|
const schema = await ensureMeshSchema(meshId);
|
||||||
|
// Use a transaction so SET LOCAL is scoped and automatically reset.
|
||||||
|
return await db.transaction(async (tx) => {
|
||||||
|
await tx.execute(
|
||||||
|
sql.raw(`SET LOCAL search_path TO "${schema}"`)
|
||||||
|
);
|
||||||
|
const result = await tx.execute(sql.raw(query));
|
||||||
|
const rows = (result.rows ?? []) as Array<Record<string, unknown>>;
|
||||||
|
const columns = rows.length > 0 ? Object.keys(rows[0]!) : [];
|
||||||
|
return { columns, rows, rowCount: rows.length };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run a DDL/DML statement in the mesh's schema. */
|
||||||
|
export async function meshExecute(
|
||||||
|
meshId: string,
|
||||||
|
statement: string,
|
||||||
|
): Promise<{ rowCount: number }> {
|
||||||
|
validateMeshSql(statement);
|
||||||
|
const schema = await ensureMeshSchema(meshId);
|
||||||
|
return await db.transaction(async (tx) => {
|
||||||
|
await tx.execute(
|
||||||
|
sql.raw(`SET LOCAL search_path TO "${schema}"`)
|
||||||
|
);
|
||||||
|
const result = await tx.execute(sql.raw(statement));
|
||||||
|
return { rowCount: (result as any).rowCount ?? 0 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List tables and columns in the mesh's schema. */
|
||||||
|
export async function meshSchema(
|
||||||
|
meshId: string,
|
||||||
|
): Promise<
|
||||||
|
Array<{
|
||||||
|
name: string;
|
||||||
|
columns: Array<{ name: string; type: string; nullable: boolean }>;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const schema = meshSchemaName(meshId);
|
||||||
|
const result = await db.execute<{
|
||||||
|
table_name: string;
|
||||||
|
column_name: string;
|
||||||
|
data_type: string;
|
||||||
|
is_nullable: string;
|
||||||
|
}>(sql`
|
||||||
|
SELECT table_name, column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = ${schema}
|
||||||
|
ORDER BY table_name, ordinal_position
|
||||||
|
`);
|
||||||
|
const rows = (result.rows ?? result) as Array<{
|
||||||
|
table_name: string;
|
||||||
|
column_name: string;
|
||||||
|
data_type: string;
|
||||||
|
is_nullable: string;
|
||||||
|
}>;
|
||||||
|
const tables = new Map<
|
||||||
|
string,
|
||||||
|
Array<{ name: string; type: string; nullable: boolean }>
|
||||||
|
>();
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!tables.has(r.table_name)) tables.set(r.table_name, []);
|
||||||
|
tables.get(r.table_name)!.push({
|
||||||
|
name: r.column_name,
|
||||||
|
type: r.data_type,
|
||||||
|
nullable: r.is_nullable === "YES",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [...tables.entries()].map(([name, columns]) => ({ name, columns }));
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ const envSchema = z.object({
|
|||||||
MINIO_ACCESS_KEY: z.string().default("claudemesh"),
|
MINIO_ACCESS_KEY: z.string().default("claudemesh"),
|
||||||
MINIO_SECRET_KEY: z.string().default("changeme"),
|
MINIO_SECRET_KEY: z.string().default("changeme"),
|
||||||
MINIO_USE_SSL: z.coerce.boolean().default(false),
|
MINIO_USE_SSL: z.coerce.boolean().default(false),
|
||||||
|
QDRANT_URL: z.string().default("http://qdrant:6333"),
|
||||||
|
NEO4J_URL: z.string().default("bolt://neo4j:7687"),
|
||||||
|
NEO4J_USER: z.string().default("neo4j"),
|
||||||
|
NEO4J_PASSWORD: z.string().default("changeme"),
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "production", "test"])
|
.enum(["development", "production", "test"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
|
|||||||
@@ -15,17 +15,21 @@
|
|||||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||||
import type { Duplex } from "node:stream";
|
import type { Duplex } from "node:stream";
|
||||||
import { WebSocketServer, type WebSocket } from "ws";
|
import { WebSocketServer, type WebSocket } from "ws";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import { env } from "./env";
|
import { env } from "./env";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { messageQueue } from "@turbostarter/db/schema/mesh";
|
import { messageQueue } from "@turbostarter/db/schema/mesh";
|
||||||
import {
|
import {
|
||||||
|
claimTask,
|
||||||
|
completeTask,
|
||||||
connectPresence,
|
connectPresence,
|
||||||
|
createTask,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
disconnectPresence,
|
disconnectPresence,
|
||||||
drainForMember,
|
drainForMember,
|
||||||
findMemberByPubkey,
|
findMemberByPubkey,
|
||||||
forgetMemory,
|
forgetMemory,
|
||||||
|
getContext,
|
||||||
getFile,
|
getFile,
|
||||||
getFileStatus,
|
getFileStatus,
|
||||||
getState,
|
getState,
|
||||||
@@ -34,9 +38,11 @@ import {
|
|||||||
joinGroup,
|
joinGroup,
|
||||||
joinMesh,
|
joinMesh,
|
||||||
leaveGroup,
|
leaveGroup,
|
||||||
|
listContexts,
|
||||||
listFiles,
|
listFiles,
|
||||||
listPeersInMesh,
|
listPeersInMesh,
|
||||||
listState,
|
listState,
|
||||||
|
listTasks,
|
||||||
queueMessage,
|
queueMessage,
|
||||||
recallMemory,
|
recallMemory,
|
||||||
recordFileAccess,
|
recordFileAccess,
|
||||||
@@ -45,12 +51,21 @@ import {
|
|||||||
rememberMemory,
|
rememberMemory,
|
||||||
setSummary,
|
setSummary,
|
||||||
setState,
|
setState,
|
||||||
|
shareContext,
|
||||||
startSweepers,
|
startSweepers,
|
||||||
stopSweepers,
|
stopSweepers,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
writeStatus,
|
writeStatus,
|
||||||
|
ensureMeshSchema,
|
||||||
|
meshQuery,
|
||||||
|
meshExecute,
|
||||||
|
meshSchema,
|
||||||
|
createStream,
|
||||||
|
listStreams,
|
||||||
} from "./broker";
|
} from "./broker";
|
||||||
import { ensureBucket, meshBucketName, minioClient } from "./minio";
|
import { ensureBucket, meshBucketName, minioClient } from "./minio";
|
||||||
|
import { qdrant, meshCollectionName, ensureCollection } from "./qdrant";
|
||||||
|
import { neo4jDriver, meshDbName, ensureDatabase } from "./neo4j-client";
|
||||||
import type {
|
import type {
|
||||||
HookSetStatusRequest,
|
HookSetStatusRequest,
|
||||||
WSClientMessage,
|
WSClientMessage,
|
||||||
@@ -81,6 +96,9 @@ interface PeerConn {
|
|||||||
|
|
||||||
const connections = new Map<string, PeerConn>();
|
const connections = new Map<string, PeerConn>();
|
||||||
const connectionsPerMesh = new Map<string, number>();
|
const connectionsPerMesh = new Map<string, number>();
|
||||||
|
|
||||||
|
// Stream subscriptions: "meshId:streamName" → Set of presenceIds
|
||||||
|
const streamSubscriptions = new Map<string, Set<string>>();
|
||||||
const hookRateLimit = new TokenBucket(
|
const hookRateLimit = new TokenBucket(
|
||||||
env.HOOK_RATE_LIMIT_PER_MIN,
|
env.HOOK_RATE_LIMIT_PER_MIN,
|
||||||
env.HOOK_RATE_LIMIT_PER_MIN,
|
env.HOOK_RATE_LIMIT_PER_MIN,
|
||||||
@@ -1066,6 +1084,598 @@ function handleConnection(ws: WebSocket): void {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "share_context": {
|
||||||
|
const sc = msg as Extract<WSClientMessage, { type: "share_context" }>;
|
||||||
|
const memberInfo = conn.memberPubkey
|
||||||
|
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||||
|
: null;
|
||||||
|
const ctxId = await shareContext(
|
||||||
|
conn.meshId,
|
||||||
|
presenceId,
|
||||||
|
memberInfo?.displayName,
|
||||||
|
sc.summary,
|
||||||
|
sc.filesRead,
|
||||||
|
sc.keyFindings,
|
||||||
|
sc.tags,
|
||||||
|
);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "context_shared",
|
||||||
|
id: ctxId,
|
||||||
|
});
|
||||||
|
// Notify all other peers in the mesh that context was shared.
|
||||||
|
for (const [pid, peer] of connections) {
|
||||||
|
if (pid === presenceId) continue;
|
||||||
|
if (peer.meshId !== conn.meshId) continue;
|
||||||
|
sendToPeer(pid, {
|
||||||
|
type: "state_change",
|
||||||
|
key: `_context:${memberInfo?.displayName ?? "unknown"}`,
|
||||||
|
value: sc.summary,
|
||||||
|
updatedBy: memberInfo?.displayName ?? "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.info("ws share_context", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
context_id: ctxId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "get_context": {
|
||||||
|
const gc = msg as Extract<WSClientMessage, { type: "get_context" }>;
|
||||||
|
const contexts = await getContext(conn.meshId, gc.query);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "context_results",
|
||||||
|
contexts: contexts.map((c) => ({
|
||||||
|
peerName: c.peerName,
|
||||||
|
summary: c.summary,
|
||||||
|
filesRead: c.filesRead,
|
||||||
|
keyFindings: c.keyFindings,
|
||||||
|
tags: c.tags,
|
||||||
|
updatedAt: c.updatedAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
log.info("ws get_context", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
query: gc.query.slice(0, 80),
|
||||||
|
results: contexts.length,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "list_contexts": {
|
||||||
|
const allContexts = await listContexts(conn.meshId);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "context_list",
|
||||||
|
contexts: allContexts.map((c) => ({
|
||||||
|
peerName: c.peerName,
|
||||||
|
summary: c.summary,
|
||||||
|
tags: c.tags,
|
||||||
|
updatedAt: c.updatedAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
log.info("ws list_contexts", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
mesh_id: conn.meshId,
|
||||||
|
count: allContexts.length,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "create_task": {
|
||||||
|
const ct = msg as Extract<WSClientMessage, { type: "create_task" }>;
|
||||||
|
const memberInfo = conn.memberPubkey
|
||||||
|
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||||
|
: null;
|
||||||
|
const taskId = await createTask(
|
||||||
|
conn.meshId,
|
||||||
|
ct.title,
|
||||||
|
ct.assignee,
|
||||||
|
ct.priority,
|
||||||
|
ct.tags,
|
||||||
|
memberInfo?.displayName,
|
||||||
|
);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "task_created",
|
||||||
|
id: taskId,
|
||||||
|
});
|
||||||
|
log.info("ws create_task", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
task_id: taskId,
|
||||||
|
title: ct.title.slice(0, 80),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "claim_task": {
|
||||||
|
const clm = msg as Extract<WSClientMessage, { type: "claim_task" }>;
|
||||||
|
const memberInfo = conn.memberPubkey
|
||||||
|
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||||
|
: null;
|
||||||
|
const claimed = await claimTask(
|
||||||
|
conn.meshId,
|
||||||
|
clm.taskId,
|
||||||
|
presenceId,
|
||||||
|
memberInfo?.displayName,
|
||||||
|
);
|
||||||
|
if (!claimed) {
|
||||||
|
sendError(conn.ws, "task_not_claimable", "task is not open or does not exist");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Return updated task list so caller sees the change.
|
||||||
|
const tasksAfterClaim = await listTasks(conn.meshId);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "task_list",
|
||||||
|
tasks: tasksAfterClaim.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.title,
|
||||||
|
assignee: t.assignee,
|
||||||
|
claimedBy: t.claimedBy,
|
||||||
|
status: t.status,
|
||||||
|
priority: t.priority,
|
||||||
|
createdBy: t.createdBy,
|
||||||
|
tags: t.tags,
|
||||||
|
createdAt: t.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
log.info("ws claim_task", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
task_id: clm.taskId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "complete_task": {
|
||||||
|
const cpt = msg as Extract<WSClientMessage, { type: "complete_task" }>;
|
||||||
|
const completed = await completeTask(
|
||||||
|
conn.meshId,
|
||||||
|
cpt.taskId,
|
||||||
|
cpt.result,
|
||||||
|
);
|
||||||
|
if (!completed) {
|
||||||
|
sendError(conn.ws, "task_not_found", "task not found in this mesh");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Return updated task list.
|
||||||
|
const tasksAfterComplete = await listTasks(conn.meshId);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "task_list",
|
||||||
|
tasks: tasksAfterComplete.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.title,
|
||||||
|
assignee: t.assignee,
|
||||||
|
claimedBy: t.claimedBy,
|
||||||
|
status: t.status,
|
||||||
|
priority: t.priority,
|
||||||
|
createdBy: t.createdBy,
|
||||||
|
tags: t.tags,
|
||||||
|
createdAt: t.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
log.info("ws complete_task", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
task_id: cpt.taskId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "list_tasks": {
|
||||||
|
const lt = msg as Extract<WSClientMessage, { type: "list_tasks" }>;
|
||||||
|
const tasks = await listTasks(conn.meshId, lt.status, lt.assignee);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "task_list",
|
||||||
|
tasks: tasks.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.title,
|
||||||
|
assignee: t.assignee,
|
||||||
|
claimedBy: t.claimedBy,
|
||||||
|
status: t.status,
|
||||||
|
priority: t.priority,
|
||||||
|
createdBy: t.createdBy,
|
||||||
|
tags: t.tags,
|
||||||
|
createdAt: t.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
log.info("ws list_tasks", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
mesh_id: conn.meshId,
|
||||||
|
count: tasks.length,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Streams ---
|
||||||
|
|
||||||
|
case "create_stream": {
|
||||||
|
const cs = msg as Extract<WSClientMessage, { type: "create_stream" }>;
|
||||||
|
const memberInfo = conn.memberPubkey
|
||||||
|
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||||
|
: null;
|
||||||
|
const streamId = await createStream(
|
||||||
|
conn.meshId,
|
||||||
|
cs.name,
|
||||||
|
memberInfo?.displayName ?? "peer",
|
||||||
|
);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "stream_created",
|
||||||
|
id: streamId,
|
||||||
|
name: cs.name,
|
||||||
|
});
|
||||||
|
log.info("ws create_stream", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
stream: cs.name,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "subscribe": {
|
||||||
|
const sub = msg as Extract<WSClientMessage, { type: "subscribe" }>;
|
||||||
|
const key = `${conn.meshId}:${sub.stream}`;
|
||||||
|
if (!streamSubscriptions.has(key))
|
||||||
|
streamSubscriptions.set(key, new Set());
|
||||||
|
streamSubscriptions.get(key)!.add(presenceId);
|
||||||
|
log.info("ws subscribe", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
stream: sub.stream,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "unsubscribe": {
|
||||||
|
const unsub = msg as Extract<
|
||||||
|
WSClientMessage,
|
||||||
|
{ type: "unsubscribe" }
|
||||||
|
>;
|
||||||
|
const key = `${conn.meshId}:${unsub.stream}`;
|
||||||
|
streamSubscriptions.get(key)?.delete(presenceId);
|
||||||
|
log.info("ws unsubscribe", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
stream: unsub.stream,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "publish": {
|
||||||
|
const pub = msg as Extract<WSClientMessage, { type: "publish" }>;
|
||||||
|
const key = `${conn.meshId}:${pub.stream}`;
|
||||||
|
const subs = streamSubscriptions.get(key);
|
||||||
|
if (subs) {
|
||||||
|
const memberInfo = conn.memberPubkey
|
||||||
|
? await findMemberByPubkey(conn.meshId, conn.memberPubkey)
|
||||||
|
: null;
|
||||||
|
const push: WSServerMessage = {
|
||||||
|
type: "stream_data",
|
||||||
|
stream: pub.stream,
|
||||||
|
data: pub.data,
|
||||||
|
publishedBy: memberInfo?.displayName ?? "peer",
|
||||||
|
};
|
||||||
|
for (const subPid of subs) {
|
||||||
|
if (subPid === presenceId) continue; // don't echo to publisher
|
||||||
|
sendToPeer(subPid, push);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metrics.messagesRoutedTotal.inc({ priority: "stream" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "list_streams": {
|
||||||
|
const streams = await listStreams(conn.meshId);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "stream_list",
|
||||||
|
streams: streams.map((s) => {
|
||||||
|
const key = `${conn.meshId}:${s.name}`;
|
||||||
|
return {
|
||||||
|
id: s.id,
|
||||||
|
name: s.name,
|
||||||
|
createdBy: s.createdBy ?? "",
|
||||||
|
createdAt: s.createdAt.toISOString(),
|
||||||
|
subscriberCount: streamSubscriptions.get(key)?.size ?? 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
log.info("ws list_streams", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
mesh_id: conn.meshId,
|
||||||
|
count: streams.length,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vector storage ---
|
||||||
|
|
||||||
|
case "vector_store": {
|
||||||
|
const vs = msg as Extract<WSClientMessage, { type: "vector_store" }>;
|
||||||
|
const collName = meshCollectionName(conn.meshId, vs.collection);
|
||||||
|
await ensureCollection(collName);
|
||||||
|
const { generateId } = await import("@turbostarter/shared/utils");
|
||||||
|
const pointId = generateId();
|
||||||
|
// Store text + metadata as payload. Use a zero vector as placeholder
|
||||||
|
// — real embeddings should be computed client-side and sent directly
|
||||||
|
// to Qdrant in a future version.
|
||||||
|
const zeroVector = new Array(1536).fill(0) as number[];
|
||||||
|
await qdrant.upsert(collName, {
|
||||||
|
wait: true,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
id: pointId,
|
||||||
|
vector: zeroVector,
|
||||||
|
payload: {
|
||||||
|
text: vs.text,
|
||||||
|
mesh_id: conn.meshId,
|
||||||
|
stored_by: conn.memberPubkey,
|
||||||
|
stored_at: new Date().toISOString(),
|
||||||
|
...(vs.metadata ?? {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "ack" as const,
|
||||||
|
id: pointId,
|
||||||
|
messageId: pointId,
|
||||||
|
queued: false,
|
||||||
|
});
|
||||||
|
log.info("ws vector_store", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
collection: vs.collection,
|
||||||
|
point_id: pointId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "vector_search": {
|
||||||
|
const vq = msg as Extract<WSClientMessage, { type: "vector_search" }>;
|
||||||
|
const searchCollName = meshCollectionName(conn.meshId, vq.collection);
|
||||||
|
const searchLimit = vq.limit ?? 10;
|
||||||
|
try {
|
||||||
|
// Keyword search via payload scroll + filter.
|
||||||
|
// Full vector similarity requires client-computed embeddings (future).
|
||||||
|
const queryLower = vq.query.toLowerCase();
|
||||||
|
const scrollResult = await qdrant.scroll(searchCollName, {
|
||||||
|
limit: 100,
|
||||||
|
with_payload: true,
|
||||||
|
with_vector: false,
|
||||||
|
});
|
||||||
|
const matches = (scrollResult.points ?? [])
|
||||||
|
.filter((p) => {
|
||||||
|
const text = (p.payload as Record<string, unknown>)?.text;
|
||||||
|
return typeof text === "string" && text.toLowerCase().includes(queryLower);
|
||||||
|
})
|
||||||
|
.slice(0, searchLimit)
|
||||||
|
.map((p) => {
|
||||||
|
const payload = p.payload as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
id: String(p.id),
|
||||||
|
text: (payload.text as string) ?? "",
|
||||||
|
score: 1.0, // keyword match — no vector similarity score
|
||||||
|
metadata: payload,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "vector_results",
|
||||||
|
results: matches,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Collection may not exist yet — return empty results.
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "vector_results",
|
||||||
|
results: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.info("ws vector_search", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
collection: vq.collection,
|
||||||
|
query: vq.query.slice(0, 80),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "vector_delete": {
|
||||||
|
const vd = msg as Extract<WSClientMessage, { type: "vector_delete" }>;
|
||||||
|
const deleteCollName = meshCollectionName(conn.meshId, vd.collection);
|
||||||
|
try {
|
||||||
|
await qdrant.delete(deleteCollName, {
|
||||||
|
wait: true,
|
||||||
|
points: [vd.id],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* collection or point may not exist — idempotent */
|
||||||
|
}
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "ack" as const,
|
||||||
|
id: vd.id,
|
||||||
|
messageId: vd.id,
|
||||||
|
queued: false,
|
||||||
|
});
|
||||||
|
log.info("ws vector_delete", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
collection: vd.collection,
|
||||||
|
point_id: vd.id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "list_collections": {
|
||||||
|
try {
|
||||||
|
const qdrantResponse = await qdrant.getCollections();
|
||||||
|
const prefix = `mesh_${conn.meshId}_`.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
||||||
|
const meshCollections = (qdrantResponse.collections ?? [])
|
||||||
|
.map((c) => c.name)
|
||||||
|
.filter((name) => name.startsWith(prefix))
|
||||||
|
.map((name) => name.slice(prefix.length));
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "collection_list",
|
||||||
|
collections: meshCollections,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "collection_list",
|
||||||
|
collections: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.info("ws list_collections", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
mesh_id: conn.meshId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Graph database ---
|
||||||
|
|
||||||
|
case "graph_query": {
|
||||||
|
const gq = msg as Extract<WSClientMessage, { type: "graph_query" }>;
|
||||||
|
const gqDbName = meshDbName(conn.meshId);
|
||||||
|
let gqSession;
|
||||||
|
try {
|
||||||
|
await ensureDatabase(gqDbName);
|
||||||
|
gqSession = neo4jDriver.session({ database: gqDbName });
|
||||||
|
} catch {
|
||||||
|
// Community edition — fall back to default db.
|
||||||
|
gqSession = neo4jDriver.session();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const gqResult = await gqSession.run(gq.cypher);
|
||||||
|
const gqRecords = gqResult.records.map((r) => {
|
||||||
|
const obj: Record<string, unknown> = {};
|
||||||
|
for (const key of r.keys) {
|
||||||
|
obj[key] = r.get(key);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "graph_result",
|
||||||
|
records: gqRecords,
|
||||||
|
});
|
||||||
|
} catch (gqErr) {
|
||||||
|
sendError(conn.ws, "graph_error", gqErr instanceof Error ? gqErr.message : String(gqErr));
|
||||||
|
} finally {
|
||||||
|
await gqSession.close();
|
||||||
|
}
|
||||||
|
log.info("ws graph_query", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
cypher: gq.cypher.slice(0, 80),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "graph_execute": {
|
||||||
|
const ge = msg as Extract<WSClientMessage, { type: "graph_execute" }>;
|
||||||
|
const geDbName = meshDbName(conn.meshId);
|
||||||
|
let geSession;
|
||||||
|
try {
|
||||||
|
await ensureDatabase(geDbName);
|
||||||
|
geSession = neo4jDriver.session({ database: geDbName });
|
||||||
|
} catch {
|
||||||
|
geSession = neo4jDriver.session();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const geResult = await geSession.run(ge.cypher);
|
||||||
|
const geRecords = geResult.records.map((r) => {
|
||||||
|
const obj: Record<string, unknown> = {};
|
||||||
|
for (const key of r.keys) {
|
||||||
|
obj[key] = r.get(key);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "graph_result",
|
||||||
|
records: geRecords,
|
||||||
|
});
|
||||||
|
} catch (geErr) {
|
||||||
|
sendError(conn.ws, "graph_error", geErr instanceof Error ? geErr.message : String(geErr));
|
||||||
|
} finally {
|
||||||
|
await geSession.close();
|
||||||
|
}
|
||||||
|
log.info("ws graph_execute", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
cypher: ge.cypher.slice(0, 80),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mesh database (per-mesh PostgreSQL schema) ---
|
||||||
|
|
||||||
|
case "mesh_query": {
|
||||||
|
const mq = msg as Extract<WSClientMessage, { type: "mesh_query" }>;
|
||||||
|
try {
|
||||||
|
const result = await meshQuery(conn.meshId, mq.sql);
|
||||||
|
sendToPeer(presenceId, { type: "mesh_query_result", ...result });
|
||||||
|
} catch (e) {
|
||||||
|
sendError(
|
||||||
|
conn.ws,
|
||||||
|
"query_error",
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info("ws mesh_query", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
sql: mq.sql.slice(0, 80),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mesh_execute": {
|
||||||
|
const me = msg as Extract<WSClientMessage, { type: "mesh_execute" }>;
|
||||||
|
try {
|
||||||
|
const result = await meshExecute(conn.meshId, me.sql);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "mesh_query_result",
|
||||||
|
columns: [],
|
||||||
|
rows: [],
|
||||||
|
rowCount: result.rowCount,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
sendError(
|
||||||
|
conn.ws,
|
||||||
|
"execute_error",
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info("ws mesh_execute", {
|
||||||
|
presence_id: presenceId,
|
||||||
|
sql: me.sql.slice(0, 80),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mesh_schema": {
|
||||||
|
try {
|
||||||
|
const tables = await meshSchema(conn.meshId);
|
||||||
|
sendToPeer(presenceId, { type: "mesh_schema_result", tables });
|
||||||
|
} catch (e) {
|
||||||
|
sendError(
|
||||||
|
conn.ws,
|
||||||
|
"schema_error",
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info("ws mesh_schema", { presence_id: presenceId });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mesh_info": {
|
||||||
|
const [peers, stateEntries, memCount, fileCount, taskCounts, streams, tables] = await Promise.all([
|
||||||
|
listPeersInMesh(conn.meshId),
|
||||||
|
listState(conn.meshId),
|
||||||
|
db.execute(sql`SELECT COUNT(*) as n FROM mesh.memory WHERE mesh_id = ${conn.meshId} AND forgotten_at IS NULL`).then(r => Number(((r.rows ?? r) as any[])[0]?.n ?? 0)),
|
||||||
|
db.execute(sql`SELECT COUNT(*) as n FROM mesh.file WHERE mesh_id = ${conn.meshId} AND deleted_at IS NULL`).then(r => Number(((r.rows ?? r) as any[])[0]?.n ?? 0)),
|
||||||
|
db.execute(sql`SELECT status, COUNT(*) as n FROM mesh.task WHERE mesh_id = ${conn.meshId} GROUP BY status`).then(r => {
|
||||||
|
const rows = (r.rows ?? r) as Array<{ status: string; n: string }>;
|
||||||
|
const counts = { open: 0, claimed: 0, done: 0 };
|
||||||
|
for (const row of rows) counts[row.status as keyof typeof counts] = Number(row.n);
|
||||||
|
return counts;
|
||||||
|
}),
|
||||||
|
listStreams(conn.meshId),
|
||||||
|
meshSchema(conn.meshId).catch(() => []),
|
||||||
|
]);
|
||||||
|
const allGroups = new Set<string>();
|
||||||
|
for (const p of peers) for (const g of p.groups) allGroups.add(`@${g.name}`);
|
||||||
|
const myPresence = peers.find(p => p.sessionId === [...connections.entries()].find(([pid]) => pid === presenceId)?.[1]?.sessionPubkey);
|
||||||
|
const peerConn = connections.get(presenceId);
|
||||||
|
sendToPeer(presenceId, {
|
||||||
|
type: "mesh_info_result",
|
||||||
|
mesh: conn.meshId,
|
||||||
|
peers: peers.length,
|
||||||
|
groups: [...allGroups],
|
||||||
|
stateKeys: stateEntries.map((e: any) => e.key),
|
||||||
|
memoryCount: memCount,
|
||||||
|
fileCount: fileCount,
|
||||||
|
tasks: taskCounts,
|
||||||
|
streams: streams.map(s => s.name),
|
||||||
|
tables: tables.map((t: any) => t.name),
|
||||||
|
collections: [],
|
||||||
|
yourName: peerConn?.groups?.[0]?.name ?? "unknown",
|
||||||
|
yourGroups: peerConn?.groups ?? [],
|
||||||
|
});
|
||||||
|
log.info("ws mesh_info", { presence_id: presenceId });
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
metrics.messagesRejectedTotal.inc({ reason: "parse_or_handler" });
|
metrics.messagesRejectedTotal.inc({ reason: "parse_or_handler" });
|
||||||
@@ -1081,6 +1691,11 @@ function handleConnection(ws: WebSocket): void {
|
|||||||
connections.delete(presenceId);
|
connections.delete(presenceId);
|
||||||
if (conn) decMeshCount(conn.meshId);
|
if (conn) decMeshCount(conn.meshId);
|
||||||
await disconnectPresence(presenceId);
|
await disconnectPresence(presenceId);
|
||||||
|
// Clean up stream subscriptions for this peer
|
||||||
|
for (const [key, subs] of streamSubscriptions) {
|
||||||
|
subs.delete(presenceId);
|
||||||
|
if (subs.size === 0) streamSubscriptions.delete(key);
|
||||||
|
}
|
||||||
log.info("ws close", { presence_id: presenceId });
|
log.info("ws close", { presence_id: presenceId });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
22
apps/broker/src/neo4j-client.ts
Normal file
22
apps/broker/src/neo4j-client.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import neo4j from "neo4j-driver";
|
||||||
|
import { env } from "./env";
|
||||||
|
|
||||||
|
export const neo4jDriver = neo4j.driver(
|
||||||
|
env.NEO4J_URL,
|
||||||
|
neo4j.auth.basic(env.NEO4J_USER, env.NEO4J_PASSWORD),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function meshDbName(meshId: string): string {
|
||||||
|
return `mesh_${meshId.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureDatabase(name: string): Promise<void> {
|
||||||
|
const session = neo4jDriver.session({ database: "system" });
|
||||||
|
try {
|
||||||
|
await session.run(`CREATE DATABASE $name IF NOT EXISTS`, { name });
|
||||||
|
} catch {
|
||||||
|
/* may not support multi-db in community edition — fall back to default */
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
apps/broker/src/qdrant.ts
Normal file
24
apps/broker/src/qdrant.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||||
|
import { env } from "./env";
|
||||||
|
|
||||||
|
export const qdrant = new QdrantClient({ url: env.QDRANT_URL });
|
||||||
|
|
||||||
|
export function meshCollectionName(
|
||||||
|
meshId: string,
|
||||||
|
collection: string,
|
||||||
|
): string {
|
||||||
|
return `mesh_${meshId}_${collection}`.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureCollection(
|
||||||
|
name: string,
|
||||||
|
vectorSize = 1536,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await qdrant.getCollection(name);
|
||||||
|
} catch {
|
||||||
|
await qdrant.createCollection(name, {
|
||||||
|
vectors: { size: vectorSize, distance: "Cosine" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -230,6 +230,133 @@ export interface WSMemoryResultsMessage {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Vector storage messages ---
|
||||||
|
|
||||||
|
/** Client → broker: store a text document in a vector collection. */
|
||||||
|
export interface WSVectorStoreMessage {
|
||||||
|
type: "vector_store";
|
||||||
|
collection: string;
|
||||||
|
text: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: search a vector collection. */
|
||||||
|
export interface WSVectorSearchMessage {
|
||||||
|
type: "vector_search";
|
||||||
|
collection: string;
|
||||||
|
query: string;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: delete a point from a vector collection. */
|
||||||
|
export interface WSVectorDeleteMessage {
|
||||||
|
type: "vector_delete";
|
||||||
|
collection: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: list all vector collections for this mesh. */
|
||||||
|
export interface WSListCollectionsMessage {
|
||||||
|
type: "list_collections";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Graph database messages ---
|
||||||
|
|
||||||
|
/** Client → broker: run a read-only Cypher query. */
|
||||||
|
export interface WSGraphQueryMessage {
|
||||||
|
type: "graph_query";
|
||||||
|
cypher: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: run a write Cypher statement. */
|
||||||
|
export interface WSGraphExecuteMessage {
|
||||||
|
type: "graph_execute";
|
||||||
|
cypher: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mesh database (per-mesh PostgreSQL schema) messages ---
|
||||||
|
|
||||||
|
/** Client → broker: run a SELECT query in the mesh's schema. */
|
||||||
|
export interface WSMeshQueryMessage {
|
||||||
|
type: "mesh_query";
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: run a DDL/DML statement in the mesh's schema. */
|
||||||
|
export interface WSMeshExecuteMessage {
|
||||||
|
type: "mesh_execute";
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: list tables and columns in the mesh's schema. */
|
||||||
|
export interface WSMeshSchemaMessage {
|
||||||
|
type: "mesh_schema";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vector/Graph response messages ---
|
||||||
|
|
||||||
|
/** Broker → client: vector search results. */
|
||||||
|
export interface WSVectorResultsMessage {
|
||||||
|
type: "vector_results";
|
||||||
|
results: Array<{
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
score: number;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: list of vector collections. */
|
||||||
|
export interface WSCollectionListMessage {
|
||||||
|
type: "collection_list";
|
||||||
|
collections: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: graph query results. */
|
||||||
|
export interface WSGraphResultMessage {
|
||||||
|
type: "graph_result";
|
||||||
|
records: Array<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: mesh SQL query results. */
|
||||||
|
export interface WSMeshQueryResultMessage {
|
||||||
|
type: "mesh_query_result";
|
||||||
|
columns: string[];
|
||||||
|
rows: Array<Record<string, unknown>>;
|
||||||
|
rowCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: mesh schema introspection results. */
|
||||||
|
export interface WSMeshSchemaResultMessage {
|
||||||
|
type: "mesh_schema_result";
|
||||||
|
tables: Array<{
|
||||||
|
name: string;
|
||||||
|
columns: Array<{ name: string; type: string; nullable: boolean }>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: get full mesh overview. */
|
||||||
|
export interface WSMeshInfoMessage {
|
||||||
|
type: "mesh_info";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: aggregated mesh overview. */
|
||||||
|
export interface WSMeshInfoResultMessage {
|
||||||
|
type: "mesh_info_result";
|
||||||
|
mesh: string;
|
||||||
|
peers: number;
|
||||||
|
groups: string[];
|
||||||
|
stateKeys: string[];
|
||||||
|
memoryCount: number;
|
||||||
|
fileCount: number;
|
||||||
|
tasks: { open: number; claimed: number; done: number };
|
||||||
|
streams: string[];
|
||||||
|
tables: string[];
|
||||||
|
collections: string[];
|
||||||
|
yourName: string;
|
||||||
|
yourGroups: Array<{ name: string; role?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
/** Client → broker: check delivery status of a message. */
|
/** Client → broker: check delivery status of a message. */
|
||||||
export interface WSMessageStatusMessage {
|
export interface WSMessageStatusMessage {
|
||||||
type: "message_status";
|
type: "message_status";
|
||||||
@@ -309,6 +436,170 @@ export interface WSFileStatusResultMessage {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Context sharing messages ---
|
||||||
|
|
||||||
|
/** Client → broker: share current working context. */
|
||||||
|
export interface WSShareContextMessage {
|
||||||
|
type: "share_context";
|
||||||
|
summary: string;
|
||||||
|
filesRead?: string[];
|
||||||
|
keyFindings?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: search contexts by query. */
|
||||||
|
export interface WSGetContextMessage {
|
||||||
|
type: "get_context";
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: list all contexts in the mesh. */
|
||||||
|
export interface WSListContextsMessage {
|
||||||
|
type: "list_contexts";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: acknowledgement for share_context. */
|
||||||
|
export interface WSContextSharedMessage {
|
||||||
|
type: "context_shared";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: response to get_context. */
|
||||||
|
export interface WSContextResultsMessage {
|
||||||
|
type: "context_results";
|
||||||
|
contexts: Array<{
|
||||||
|
peerName: string;
|
||||||
|
summary: string;
|
||||||
|
filesRead: string[];
|
||||||
|
keyFindings: string[];
|
||||||
|
tags: string[];
|
||||||
|
updatedAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: response to list_contexts. */
|
||||||
|
export interface WSContextListMessage {
|
||||||
|
type: "context_list";
|
||||||
|
contexts: Array<{
|
||||||
|
peerName: string;
|
||||||
|
summary: string;
|
||||||
|
tags: string[];
|
||||||
|
updatedAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Task messages ---
|
||||||
|
|
||||||
|
/** Client → broker: create a task. */
|
||||||
|
export interface WSCreateTaskMessage {
|
||||||
|
type: "create_task";
|
||||||
|
title: string;
|
||||||
|
assignee?: string;
|
||||||
|
priority?: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: claim an open task. */
|
||||||
|
export interface WSClaimTaskMessage {
|
||||||
|
type: "claim_task";
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: mark a task as done. */
|
||||||
|
export interface WSCompleteTaskMessage {
|
||||||
|
type: "complete_task";
|
||||||
|
taskId: string;
|
||||||
|
result?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: list tasks with optional filters. */
|
||||||
|
export interface WSListTasksMessage {
|
||||||
|
type: "list_tasks";
|
||||||
|
status?: string;
|
||||||
|
assignee?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: acknowledgement for create_task. */
|
||||||
|
export interface WSTaskCreatedMessage {
|
||||||
|
type: "task_created";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: response to list_tasks, claim_task, complete_task. */
|
||||||
|
export interface WSTaskListMessage {
|
||||||
|
type: "task_list";
|
||||||
|
tasks: Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
assignee: string | null;
|
||||||
|
claimedBy: string | null;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
createdBy: string | null;
|
||||||
|
tags: string[];
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Stream messages ---
|
||||||
|
|
||||||
|
/** Client → broker: create a named real-time stream. */
|
||||||
|
export interface WSCreateStreamMessage {
|
||||||
|
type: "create_stream";
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: publish data to a stream. */
|
||||||
|
export interface WSPublishMessage {
|
||||||
|
type: "publish";
|
||||||
|
stream: string;
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: subscribe to a stream. */
|
||||||
|
export interface WSSubscribeMessage {
|
||||||
|
type: "subscribe";
|
||||||
|
stream: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: unsubscribe from a stream. */
|
||||||
|
export interface WSUnsubscribeMessage {
|
||||||
|
type: "unsubscribe";
|
||||||
|
stream: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client → broker: list all streams in the mesh. */
|
||||||
|
export interface WSListStreamsMessage {
|
||||||
|
type: "list_streams";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: acknowledgement for create_stream. */
|
||||||
|
export interface WSStreamCreatedMessage {
|
||||||
|
type: "stream_created";
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: real-time data pushed from a stream. */
|
||||||
|
export interface WSStreamDataMessage {
|
||||||
|
type: "stream_data";
|
||||||
|
stream: string;
|
||||||
|
data: unknown;
|
||||||
|
publishedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Broker → client: response to list_streams. */
|
||||||
|
export interface WSStreamListMessage {
|
||||||
|
type: "stream_list";
|
||||||
|
streams: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: string;
|
||||||
|
subscriberCount: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/** Broker → client: structured error. */
|
/** Broker → client: structured error. */
|
||||||
export interface WSErrorMessage {
|
export interface WSErrorMessage {
|
||||||
type: "error";
|
type: "error";
|
||||||
@@ -335,7 +626,29 @@ export type WSClientMessage =
|
|||||||
| WSGetFileMessage
|
| WSGetFileMessage
|
||||||
| WSListFilesMessage
|
| WSListFilesMessage
|
||||||
| WSFileStatusMessage
|
| WSFileStatusMessage
|
||||||
| WSDeleteFileMessage;
|
| WSDeleteFileMessage
|
||||||
|
| WSShareContextMessage
|
||||||
|
| WSGetContextMessage
|
||||||
|
| WSListContextsMessage
|
||||||
|
| WSCreateTaskMessage
|
||||||
|
| WSClaimTaskMessage
|
||||||
|
| WSCompleteTaskMessage
|
||||||
|
| WSListTasksMessage
|
||||||
|
| WSVectorStoreMessage
|
||||||
|
| WSVectorSearchMessage
|
||||||
|
| WSVectorDeleteMessage
|
||||||
|
| WSListCollectionsMessage
|
||||||
|
| WSGraphQueryMessage
|
||||||
|
| WSGraphExecuteMessage
|
||||||
|
| WSMeshQueryMessage
|
||||||
|
| WSMeshExecuteMessage
|
||||||
|
| WSMeshSchemaMessage
|
||||||
|
| WSCreateStreamMessage
|
||||||
|
| WSPublishMessage
|
||||||
|
| WSSubscribeMessage
|
||||||
|
| WSUnsubscribeMessage
|
||||||
|
| WSListStreamsMessage
|
||||||
|
| WSMeshInfoMessage;
|
||||||
|
|
||||||
export type WSServerMessage =
|
export type WSServerMessage =
|
||||||
| WSHelloAckMessage
|
| WSHelloAckMessage
|
||||||
@@ -351,4 +664,18 @@ export type WSServerMessage =
|
|||||||
| WSFileUrlMessage
|
| WSFileUrlMessage
|
||||||
| WSFileListMessage
|
| WSFileListMessage
|
||||||
| WSFileStatusResultMessage
|
| WSFileStatusResultMessage
|
||||||
|
| WSContextSharedMessage
|
||||||
|
| WSContextResultsMessage
|
||||||
|
| WSContextListMessage
|
||||||
|
| WSTaskCreatedMessage
|
||||||
|
| WSTaskListMessage
|
||||||
|
| WSVectorResultsMessage
|
||||||
|
| WSCollectionListMessage
|
||||||
|
| WSGraphResultMessage
|
||||||
|
| WSMeshQueryResultMessage
|
||||||
|
| WSMeshSchemaResultMessage
|
||||||
|
| WSStreamCreatedMessage
|
||||||
|
| WSStreamDataMessage
|
||||||
|
| WSStreamListMessage
|
||||||
|
| WSMeshInfoResultMessage
|
||||||
| WSErrorMessage;
|
| WSErrorMessage;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "0.4.0",
|
"version": "0.5.8",
|
||||||
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
"description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface LaunchArgs {
|
|||||||
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
|
groups: string | null; // comma-separated, e.g. "frontend:lead,reviewers:member"
|
||||||
joinLink: string | null;
|
joinLink: string | null;
|
||||||
meshSlug: string | null;
|
meshSlug: string | null;
|
||||||
|
messageMode: "push" | "inbox" | "off" | null;
|
||||||
quiet: boolean;
|
quiet: boolean;
|
||||||
skipPermConfirm: boolean;
|
skipPermConfirm: boolean;
|
||||||
claudeArgs: string[];
|
claudeArgs: string[];
|
||||||
@@ -38,6 +39,7 @@ function parseArgs(argv: string[]): LaunchArgs {
|
|||||||
groups: null,
|
groups: null,
|
||||||
joinLink: null,
|
joinLink: null,
|
||||||
meshSlug: null,
|
meshSlug: null,
|
||||||
|
messageMode: null,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
skipPermConfirm: false,
|
skipPermConfirm: false,
|
||||||
claudeArgs: [],
|
claudeArgs: [],
|
||||||
@@ -66,6 +68,10 @@ function parseArgs(argv: string[]): LaunchArgs {
|
|||||||
result.meshSlug = argv[++i]!;
|
result.meshSlug = argv[++i]!;
|
||||||
} else if (arg.startsWith("--mesh=")) {
|
} else if (arg.startsWith("--mesh=")) {
|
||||||
result.meshSlug = arg.slice("--mesh=".length);
|
result.meshSlug = arg.slice("--mesh=".length);
|
||||||
|
} else if (arg === "--inbox") {
|
||||||
|
result.messageMode = "inbox";
|
||||||
|
} else if (arg === "--no-messages") {
|
||||||
|
result.messageMode = "off";
|
||||||
} else if (arg === "--quiet") {
|
} else if (arg === "--quiet") {
|
||||||
result.quiet = true;
|
result.quiet = true;
|
||||||
} else if (arg === "-y" || arg === "--yes") {
|
} else if (arg === "-y" || arg === "--yes") {
|
||||||
@@ -171,7 +177,7 @@ async function confirmPermissions(): Promise<void> {
|
|||||||
|
|
||||||
// --- Banner ---
|
// --- Banner ---
|
||||||
|
|
||||||
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[]): void {
|
function printBanner(name: string, meshSlug: string, role: string | null, groups: GroupEntry[], messageMode: "push" | "inbox" | "off"): void {
|
||||||
const useColor =
|
const useColor =
|
||||||
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||||
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||||
@@ -183,9 +189,15 @@ function printBanner(name: string, meshSlug: string, role: string | null, groups
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const rule = "─".repeat(60);
|
const rule = "─".repeat(60);
|
||||||
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags}`));
|
console.log(bold(`claudemesh launch`) + dim(` — as ${name}${roleSuffix} on ${meshSlug}${groupTags} [${messageMode}]`));
|
||||||
console.log(rule);
|
console.log(rule);
|
||||||
console.log("Peer messages arrive as <channel> reminders in real-time.");
|
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("Peers send text only — they cannot call tools or read files.");
|
||||||
console.log(dim(`Config: ${getConfigPath()}`));
|
console.log(dim(`Config: ${getConfigPath()}`));
|
||||||
console.log(rule);
|
console.log(rule);
|
||||||
@@ -263,6 +275,8 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
let role: string | null = args.role;
|
let role: string | null = args.role;
|
||||||
let parsedGroups: GroupEntry[] = args.groups ? parseGroupsString(args.groups) : [];
|
let parsedGroups: GroupEntry[] = args.groups ? parseGroupsString(args.groups) : [];
|
||||||
|
|
||||||
|
let messageMode: "push" | "inbox" | "off" = args.messageMode ?? "push";
|
||||||
|
|
||||||
if (!args.quiet) {
|
if (!args.quiet) {
|
||||||
if (role === null) {
|
if (role === null) {
|
||||||
const answer = await askLine(" Role (optional): ");
|
const answer = await askLine(" Role (optional): ");
|
||||||
@@ -272,6 +286,18 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
const answer = await askLine(" Groups (comma-separated, optional): ");
|
const answer = await askLine(" Groups (comma-separated, optional): ");
|
||||||
if (answer) parsedGroups = parseGroupsString(answer);
|
if (answer) parsedGroups = parseGroupsString(answer);
|
||||||
}
|
}
|
||||||
|
if (args.messageMode === null) {
|
||||||
|
console.log("\n Message mode:");
|
||||||
|
console.log(" 1) Push (real-time, peers can interrupt your work)");
|
||||||
|
console.log(" 2) Inbox (held until you check, notification only)");
|
||||||
|
console.log(" 3) Off (tools only, no messages)");
|
||||||
|
console.log("");
|
||||||
|
const answer = await askLine(" Choice [1]: ");
|
||||||
|
const choice = parseInt(answer || "1", 10);
|
||||||
|
if (choice === 2) messageMode = "inbox";
|
||||||
|
else if (choice === 3) messageMode = "off";
|
||||||
|
else messageMode = "push";
|
||||||
|
}
|
||||||
if (role || parsedGroups.length) console.log("");
|
if (role || parsedGroups.length) console.log("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +319,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
meshes: [mesh],
|
meshes: [mesh],
|
||||||
displayName,
|
displayName,
|
||||||
...(parsedGroups.length > 0 ? { groups: parsedGroups } : {}),
|
...(parsedGroups.length > 0 ? { groups: parsedGroups } : {}),
|
||||||
|
messageMode,
|
||||||
};
|
};
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(tmpDir, "config.json"),
|
join(tmpDir, "config.json"),
|
||||||
@@ -302,7 +329,7 @@ export async function runLaunch(extraArgs: string[]): Promise<void> {
|
|||||||
|
|
||||||
// 5. Banner + permission confirmation.
|
// 5. Banner + permission confirmation.
|
||||||
if (!args.quiet) {
|
if (!args.quiet) {
|
||||||
printBanner(displayName, mesh.slug, role, parsedGroups);
|
printBanner(displayName, mesh.slug, role, parsedGroups, messageMode);
|
||||||
// Auto-permissions confirmation — needed for autonomous peer messaging.
|
// Auto-permissions confirmation — needed for autonomous peer messaging.
|
||||||
if (!args.skipPermConfirm) {
|
if (!args.skipPermConfirm) {
|
||||||
await confirmPermissions();
|
await confirmPermissions();
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export async function startMcpServer(): Promise<void> {
|
|||||||
|
|
||||||
const myName = config.displayName ?? "unnamed";
|
const myName = config.displayName ?? "unnamed";
|
||||||
const myGroups = (config.groups ?? []).map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ') || "none";
|
const myGroups = (config.groups ?? []).map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ') || "none";
|
||||||
|
const messageMode = config.messageMode ?? "push";
|
||||||
|
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
{ name: "claudemesh", version: "0.3.0" },
|
{ name: "claudemesh", version: "0.3.0" },
|
||||||
@@ -166,6 +167,26 @@ When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATEL
|
|||||||
| list_files(query?, from?) | Find files shared in the mesh. |
|
| list_files(query?, from?) | Find files shared in the mesh. |
|
||||||
| file_status(id) | Check who has accessed a file. |
|
| file_status(id) | Check who has accessed a file. |
|
||||||
| delete_file(id) | Remove a shared file from the mesh. |
|
| delete_file(id) | Remove a shared file from the mesh. |
|
||||||
|
| vector_store(collection, text, metadata?) | Store embedding in per-mesh Qdrant collection. |
|
||||||
|
| vector_search(collection, query, limit?) | Semantic search over stored embeddings. |
|
||||||
|
| vector_delete(collection, id) | Remove an embedding. |
|
||||||
|
| list_collections() | List vector collections in this mesh. |
|
||||||
|
| graph_query(cypher) | Read-only Cypher query on per-mesh Neo4j. |
|
||||||
|
| graph_execute(cypher) | Write Cypher query (CREATE, MERGE, DELETE). |
|
||||||
|
| mesh_query(sql) | Run a SELECT query on the per-mesh shared database. |
|
||||||
|
| mesh_execute(sql) | Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE). |
|
||||||
|
| mesh_schema() | List tables and columns in the per-mesh shared database. |
|
||||||
|
| create_stream(name) | Create a real-time data stream in the mesh. |
|
||||||
|
| publish(stream, data) | Push data to a stream. Subscribers receive it in real-time. |
|
||||||
|
| subscribe(stream) | Subscribe to a stream. Data pushes arrive as channel notifications. |
|
||||||
|
| list_streams() | List active streams in the mesh. |
|
||||||
|
| share_context(summary, files_read?, key_findings?, tags?) | Share session understanding with peers. |
|
||||||
|
| get_context(query) | Find context from peers who explored an area. |
|
||||||
|
| list_contexts() | See what all peers currently know. |
|
||||||
|
| create_task(title, assignee?, priority?, tags?) | Create a work item. |
|
||||||
|
| claim_task(id) | Claim an unclaimed task. |
|
||||||
|
| complete_task(id, result?) | Mark task done with optional result. |
|
||||||
|
| list_tasks(status?, assignee?) | List tasks filtered by status/assignee. |
|
||||||
|
|
||||||
If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
|
If multiple meshes are joined, prefix \`to\` with \`<mesh-slug>:\` to disambiguate (e.g. \`dev-team:Alice\`).
|
||||||
|
|
||||||
@@ -192,13 +213,37 @@ Persistent knowledge that survives across sessions. Use remember(content, tags?)
|
|||||||
share_file for persistent references, send_message(file:) for ephemeral attachments.
|
share_file for persistent references, send_message(file:) for ephemeral attachments.
|
||||||
Tags on shared files make them searchable. Use list_files to find what peers shared.
|
Tags on shared files make them searchable. Use list_files to find what peers shared.
|
||||||
|
|
||||||
|
## Vectors
|
||||||
|
Store and search semantic embeddings. Use vector_store to index content, vector_search to find similar content.
|
||||||
|
|
||||||
|
## Graph
|
||||||
|
Build and query entity relationship graphs. Use graph_execute for writes (CREATE, MERGE), graph_query for reads (MATCH).
|
||||||
|
|
||||||
|
## Mesh Database
|
||||||
|
Per-mesh PostgreSQL database. Use mesh_execute for DDL/DML (CREATE TABLE, INSERT), mesh_query for SELECT, mesh_schema to inspect tables. Schema auto-created on first use.
|
||||||
|
|
||||||
|
## Streams
|
||||||
|
Real-time data channels. create_stream to start one, publish to push data, subscribe to receive pushes. Use for build logs, deploy status, live metrics.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Share your session understanding with peers. Use share_context after exploring a codebase area. Check get_context before re-reading files another peer already analyzed.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
Create and claim work items. create_task to propose work, claim_task to take ownership, complete_task when done. Prevents duplicate effort.
|
||||||
|
|
||||||
## Priority
|
## Priority
|
||||||
- "now": interrupt immediately, even if recipient is in DND (use for urgent: broken deploy, blocking issue)
|
- "now": interrupt immediately, even if recipient is in DND (use for urgent: broken deploy, blocking issue)
|
||||||
- "next" (default): deliver when recipient goes idle (normal coordination)
|
- "next" (default): deliver when recipient goes idle (normal coordination)
|
||||||
- "low": pull-only via check_messages (FYI, non-blocking context)
|
- "low": pull-only via check_messages (FYI, non-blocking context)
|
||||||
|
|
||||||
## Coordination
|
## 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.`,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -400,11 +445,14 @@ Call list_peers at session start to understand who is online, their roles, and w
|
|||||||
if (!existsSync(filePath)) return text(`share_file: file not found: ${filePath}`, true);
|
if (!existsSync(filePath)) return text(`share_file: file not found: ${filePath}`, true);
|
||||||
const client = allClients()[0];
|
const client = allClients()[0];
|
||||||
if (!client) return text("share_file: not connected", true);
|
if (!client) return text("share_file: not connected", true);
|
||||||
const fileId = await client.uploadFile(filePath, client.meshId, client.meshSlug, {
|
try {
|
||||||
name: fileName, tags, persistent: 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})`);
|
return text(`Shared: ${fileName ?? filePath} (${fileId})`);
|
||||||
|
} catch (e) {
|
||||||
|
return text(`share_file: upload failed — ${e instanceof Error ? e.message : String(e)}`, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "get_file": {
|
case "get_file": {
|
||||||
@@ -455,6 +503,263 @@ Call list_peers at session start to understand who is online, their roles, and w
|
|||||||
return text(`Deleted: ${id}`);
|
return text(`Deleted: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Vectors ---
|
||||||
|
case "vector_store": {
|
||||||
|
const { collection, text: storeText, metadata } = (args ?? {}) as { collection?: string; text?: string; metadata?: Record<string, unknown> };
|
||||||
|
if (!collection || !storeText) return text("vector_store: `collection` and `text` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("vector_store: not connected", true);
|
||||||
|
const id = await client.vectorStore(collection, storeText, metadata);
|
||||||
|
return text(`Stored in ${collection}${id ? ` (${id})` : ""}`);
|
||||||
|
}
|
||||||
|
case "vector_search": {
|
||||||
|
const { collection, query, limit } = (args ?? {}) as { collection?: string; query?: string; limit?: number };
|
||||||
|
if (!collection || !query) return text("vector_search: `collection` and `query` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("vector_search: not connected", true);
|
||||||
|
const results = await client.vectorSearch(collection, query, limit);
|
||||||
|
if (results.length === 0) return text(`No results in ${collection} for "${query}".`);
|
||||||
|
const lines = results.map(r => `- [${r.id.slice(0, 8)}…] (score: ${r.score.toFixed(3)}) ${r.text.slice(0, 120)}${r.text.length > 120 ? "…" : ""}`);
|
||||||
|
return text(`${results.length} result(s) in ${collection}:\n${lines.join("\n")}`);
|
||||||
|
}
|
||||||
|
case "vector_delete": {
|
||||||
|
const { collection, id } = (args ?? {}) as { collection?: string; id?: string };
|
||||||
|
if (!collection || !id) return text("vector_delete: `collection` and `id` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("vector_delete: not connected", true);
|
||||||
|
await client.vectorDelete(collection, id);
|
||||||
|
return text(`Deleted ${id} from ${collection}`);
|
||||||
|
}
|
||||||
|
case "list_collections": {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("list_collections: not connected", true);
|
||||||
|
const collections = await client.listCollections();
|
||||||
|
if (collections.length === 0) return text("No vector collections.");
|
||||||
|
return text(`Collections:\n${collections.map(c => `- ${c}`).join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Graph ---
|
||||||
|
case "graph_query": {
|
||||||
|
const { cypher } = (args ?? {}) as { cypher?: string };
|
||||||
|
if (!cypher) return text("graph_query: `cypher` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("graph_query: not connected", true);
|
||||||
|
const rows = await client.graphQuery(cypher);
|
||||||
|
if (rows.length === 0) return text("No results.");
|
||||||
|
return text(JSON.stringify(rows, null, 2));
|
||||||
|
}
|
||||||
|
case "graph_execute": {
|
||||||
|
const { cypher } = (args ?? {}) as { cypher?: string };
|
||||||
|
if (!cypher) return text("graph_execute: `cypher` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("graph_execute: not connected", true);
|
||||||
|
const rows = await client.graphExecute(cypher);
|
||||||
|
return text(rows.length > 0 ? JSON.stringify(rows, null, 2) : "Executed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Context ---
|
||||||
|
case "share_context": {
|
||||||
|
const { summary, files_read, key_findings, tags } = (args ?? {}) as { summary?: string; files_read?: string[]; key_findings?: string[]; tags?: string[] };
|
||||||
|
if (!summary) return text("share_context: `summary` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("share_context: not connected", true);
|
||||||
|
await client.shareContext(summary, files_read, key_findings, tags);
|
||||||
|
return text(`Context shared: "${summary.slice(0, 80)}${summary.length > 80 ? "…" : ""}"`);
|
||||||
|
}
|
||||||
|
case "get_context": {
|
||||||
|
const { query } = (args ?? {}) as { query?: string };
|
||||||
|
if (!query) return text("get_context: `query` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("get_context: not connected", true);
|
||||||
|
const contexts = await client.getContext(query);
|
||||||
|
if (contexts.length === 0) return text(`No context found for "${query}".`);
|
||||||
|
const lines = contexts.map(c => {
|
||||||
|
const files = c.filesRead.length ? `\n Files: ${c.filesRead.join(", ")}` : "";
|
||||||
|
const findings = c.keyFindings.length ? `\n Findings: ${c.keyFindings.join("; ")}` : "";
|
||||||
|
return `- **${c.peerName}** (${c.updatedAt}): ${c.summary}${files}${findings}`;
|
||||||
|
});
|
||||||
|
return text(`${contexts.length} context(s):\n${lines.join("\n")}`);
|
||||||
|
}
|
||||||
|
case "list_contexts": {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("list_contexts: not connected", true);
|
||||||
|
const contexts = await client.listContexts();
|
||||||
|
if (contexts.length === 0) return text("No peer contexts shared yet.");
|
||||||
|
const lines = contexts.map(c => `- **${c.peerName}**: ${c.summary}${c.tags.length ? ` [${c.tags.join(", ")}]` : ""}`);
|
||||||
|
return text(`Peer contexts:\n${lines.join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tasks ---
|
||||||
|
case "create_task": {
|
||||||
|
const { title, assignee, priority, tags } = (args ?? {}) as { title?: string; assignee?: string; priority?: string; tags?: string[] };
|
||||||
|
if (!title) return text("create_task: `title` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("create_task: not connected", true);
|
||||||
|
const id = await client.createTask(title, assignee, priority, tags);
|
||||||
|
return text(`Task created${id ? ` (${id})` : ""}: "${title}"${assignee ? ` → ${assignee}` : ""}`);
|
||||||
|
}
|
||||||
|
case "claim_task": {
|
||||||
|
const { id } = (args ?? {}) as { id?: string };
|
||||||
|
if (!id) return text("claim_task: `id` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("claim_task: not connected", true);
|
||||||
|
await client.claimTask(id);
|
||||||
|
return text(`Claimed task: ${id}`);
|
||||||
|
}
|
||||||
|
case "complete_task": {
|
||||||
|
const { id, result } = (args ?? {}) as { id?: string; result?: string };
|
||||||
|
if (!id) return text("complete_task: `id` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("complete_task: not connected", true);
|
||||||
|
await client.completeTask(id, result);
|
||||||
|
return text(`Completed task: ${id}${result ? ` — ${result}` : ""}`);
|
||||||
|
}
|
||||||
|
case "list_tasks": {
|
||||||
|
const { status, assignee } = (args ?? {}) as { status?: string; assignee?: string };
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("list_tasks: not connected", true);
|
||||||
|
const tasks = await client.listTasks(status, assignee);
|
||||||
|
if (tasks.length === 0) return text("No tasks found.");
|
||||||
|
const lines = tasks.map(t => `- [${t.id.slice(0, 8)}…] **${t.title}** (${t.status}, ${t.priority}) ${t.assignee ? `→ ${t.assignee}` : "unassigned"} (by ${t.createdBy})`);
|
||||||
|
return text(`${tasks.length} task(s):\n${lines.join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mesh Database ---
|
||||||
|
case "mesh_query": {
|
||||||
|
const { sql: querySql } = (args ?? {}) as { sql?: string };
|
||||||
|
if (!querySql) return text("mesh_query: `sql` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("mesh_query: not connected", true);
|
||||||
|
const result = await client.meshQuery(querySql);
|
||||||
|
if (!result) return text("mesh_query: query failed or timed out", true);
|
||||||
|
if (result.rows.length === 0) return text(`Query returned 0 rows.`);
|
||||||
|
const header = `| ${result.columns.join(" | ")} |`;
|
||||||
|
const sep = `| ${result.columns.map(() => "---").join(" | ")} |`;
|
||||||
|
const rows = result.rows.map(r => `| ${result.columns.map(c => String(r[c] ?? "")).join(" | ")} |`);
|
||||||
|
return text(`${result.rowCount} row(s):\n${header}\n${sep}\n${rows.join("\n")}`);
|
||||||
|
}
|
||||||
|
case "mesh_execute": {
|
||||||
|
const { sql: execSql } = (args ?? {}) as { sql?: string };
|
||||||
|
if (!execSql) return text("mesh_execute: `sql` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("mesh_execute: not connected", true);
|
||||||
|
await client.meshExecute(execSql);
|
||||||
|
return text(`Executed.`);
|
||||||
|
}
|
||||||
|
case "mesh_schema": {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("mesh_schema: not connected", true);
|
||||||
|
const tables = await client.meshSchema();
|
||||||
|
if (!tables || tables.length === 0) return text("No tables in mesh database.");
|
||||||
|
const lines = tables.map(t => `**${t.name}**: ${t.columns.map(c => `${c.name} (${c.type}${c.nullable ? ", nullable" : ""})`).join(", ")}`);
|
||||||
|
return text(lines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Streams ---
|
||||||
|
case "create_stream": {
|
||||||
|
const { name: streamName } = (args ?? {}) as { name?: string };
|
||||||
|
if (!streamName) return text("create_stream: `name` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("create_stream: not connected", true);
|
||||||
|
const streamId = await client.createStream(streamName);
|
||||||
|
return text(`Stream created: ${streamName}${streamId ? ` (${streamId})` : ""}`);
|
||||||
|
}
|
||||||
|
case "publish": {
|
||||||
|
const { stream: pubStream, data: pubData } = (args ?? {}) as { stream?: string; data?: unknown };
|
||||||
|
if (!pubStream) return text("publish: `stream` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("publish: not connected", true);
|
||||||
|
await client.publish(pubStream, pubData);
|
||||||
|
return text(`Published to ${pubStream}.`);
|
||||||
|
}
|
||||||
|
case "subscribe": {
|
||||||
|
const { stream: subStream } = (args ?? {}) as { stream?: string };
|
||||||
|
if (!subStream) return text("subscribe: `stream` required", true);
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("subscribe: not connected", true);
|
||||||
|
await client.subscribe(subStream);
|
||||||
|
return text(`Subscribed to ${subStream}. Data pushes will arrive as channel notifications.`);
|
||||||
|
}
|
||||||
|
case "list_streams": {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("list_streams: not connected", true);
|
||||||
|
const streams = await client.listStreams();
|
||||||
|
if (streams.length === 0) return text("No active streams.");
|
||||||
|
const lines = streams.map(s => `- **${s.name}** (${s.id.slice(0, 8)}…) by ${s.createdBy}, ${s.subscriberCount} subscriber(s)`);
|
||||||
|
return text(lines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
case "mesh_info": {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("mesh_info: not connected", true);
|
||||||
|
const info = await client.meshInfo();
|
||||||
|
if (!info) return text("mesh_info: timed out", true);
|
||||||
|
const lines = [
|
||||||
|
`**Mesh**: ${info.mesh}`,
|
||||||
|
`**Peers**: ${info.peers}`,
|
||||||
|
`**Groups**: ${(info.groups as string[])?.join(", ") || "none"}`,
|
||||||
|
`**State keys**: ${(info.stateKeys as string[])?.join(", ") || "none"}`,
|
||||||
|
`**Memories**: ${info.memoryCount}`,
|
||||||
|
`**Files**: ${info.fileCount}`,
|
||||||
|
`**Tasks**: open=${(info.tasks as any)?.open ?? 0}, claimed=${(info.tasks as any)?.claimed ?? 0}, done=${(info.tasks as any)?.done ?? 0}`,
|
||||||
|
`**Streams**: ${(info.streams as string[])?.join(", ") || "none"}`,
|
||||||
|
`**Tables**: ${(info.tables as string[])?.join(", ") || "none"}`,
|
||||||
|
`**Your name**: ${info.yourName}`,
|
||||||
|
`**Your groups**: ${(info.yourGroups as any[])?.map((g: any) => `@${g.name}${g.role ? ':' + g.role : ''}`).join(", ") || "none"}`,
|
||||||
|
];
|
||||||
|
return text(lines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ping_mesh": {
|
||||||
|
const { priorities: pingPriorities } = (args ?? {}) as { priorities?: string[] };
|
||||||
|
const toTest = (pingPriorities ?? ["now", "next"]) as Priority[];
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client) return text("ping_mesh: not connected", true);
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
// Diagnostics: connection state
|
||||||
|
results.push(`WS status: ${client.status}`);
|
||||||
|
results.push(`Mesh: ${client.meshSlug}`);
|
||||||
|
|
||||||
|
// Check own peer status (explains priority gating)
|
||||||
|
const peers = await client.listPeers();
|
||||||
|
const selfPeer = peers.find(p => p.displayName === myName);
|
||||||
|
results.push(`Your status: ${selfPeer?.status ?? "not found in peer list"}`);
|
||||||
|
results.push(`Peers online: ${peers.length}`);
|
||||||
|
results.push(`Push buffer: ${client.pushHistory.length} buffered`);
|
||||||
|
|
||||||
|
// Test send→ack latency per priority (doesn't need round-trip)
|
||||||
|
for (const prio of toTest) {
|
||||||
|
const sendTime = Date.now();
|
||||||
|
// Send to a peer if one exists, otherwise broadcast
|
||||||
|
const target = peers.find(p => p.displayName !== myName);
|
||||||
|
const sendResult = await client.send(
|
||||||
|
target?.pubkey ?? "*",
|
||||||
|
`__ping__ ${prio} from ${myName} at ${new Date().toISOString()}`,
|
||||||
|
prio,
|
||||||
|
);
|
||||||
|
const ackTime = Date.now();
|
||||||
|
|
||||||
|
if (!sendResult.ok) {
|
||||||
|
results.push(`[${prio}] SEND FAILED: ${sendResult.error}`);
|
||||||
|
} else {
|
||||||
|
results.push(`[${prio}] send→ack: ${ackTime - sendTime}ms (msgId: ${sendResult.messageId?.slice(0, 12)})`);
|
||||||
|
if (prio !== "now" && selfPeer?.status === "working") {
|
||||||
|
results.push(` ⚠ peer status is "working" — broker holds "${prio}" until idle`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if notification pipeline works
|
||||||
|
results.push("");
|
||||||
|
results.push("Pipeline check:");
|
||||||
|
results.push(` onPush handlers: active`);
|
||||||
|
results.push(` messageMode: ${messageMode}`);
|
||||||
|
results.push(` server.notification: ${messageMode === "off" ? "disabled (mode=off)" : "enabled"}`);
|
||||||
|
|
||||||
|
return text(results.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return text(`Unknown tool: ${name}`, true);
|
return text(`Unknown tool: ${name}`, true);
|
||||||
}
|
}
|
||||||
@@ -470,12 +775,33 @@ Call list_peers at session start to understand who is online, their roles, and w
|
|||||||
// any mesh's broker connection becomes a <channel source="claudemesh">
|
// any mesh's broker connection becomes a <channel source="claudemesh">
|
||||||
// system reminder injected into Claude Code's context.
|
// system reminder injected into Claude Code's context.
|
||||||
for (const client of allClients()) {
|
for (const client of allClients()) {
|
||||||
|
// Event-driven push: WS onPush fires immediately when a message arrives.
|
||||||
|
// Claude Code's setNotificationHandler → enqueue → React useEffect pipeline
|
||||||
|
// processes notifications instantly (no polling needed on Claude's side).
|
||||||
|
// The old poll-based approach was an overcorrection — Claude Code source
|
||||||
|
// confirms event-driven notification processing.
|
||||||
client.onPush(async (msg) => {
|
client.onPush(async (msg) => {
|
||||||
|
if (messageMode === "off") return;
|
||||||
|
|
||||||
const fromPubkey = msg.senderPubkey || "";
|
const fromPubkey = msg.senderPubkey || "";
|
||||||
// Resolve sender's display name from the cached peer list.
|
|
||||||
const fromName = fromPubkey
|
const fromName = fromPubkey
|
||||||
? await resolvePeerName(client, fromPubkey)
|
? await resolvePeerName(client, fromPubkey)
|
||||||
: "unknown";
|
: "unknown";
|
||||||
|
|
||||||
|
if (messageMode === "inbox") {
|
||||||
|
try {
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content: `[inbox] New message from ${fromName}. Use check_messages to read.`,
|
||||||
|
meta: { kind: "inbox_notification", from_name: fromName },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push mode — full content
|
||||||
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
|
||||||
try {
|
try {
|
||||||
await server.notification({
|
await server.notification({
|
||||||
@@ -494,11 +820,28 @@ Call list_peers at session start to understand who is online, their roles, and w
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
process.stderr.write(`[claudemesh] pushed: from=${fromName} content=${content.slice(0, 60)}\n`);
|
||||||
/* channel push is best-effort; check_messages is the fallback */
|
} catch (pushErr) {
|
||||||
|
process.stderr.write(`[claudemesh] push FAILED: ${pushErr}\n`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.onStreamData(async (evt) => {
|
||||||
|
try {
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content: `[stream:${evt.stream}] from ${evt.publishedBy}: ${JSON.stringify(evt.data)}`,
|
||||||
|
meta: {
|
||||||
|
kind: "stream_data",
|
||||||
|
stream: evt.stream,
|
||||||
|
published_by: evt.publishedBy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
});
|
||||||
|
|
||||||
client.onStateChange(async (change) => {
|
client.onStateChange(async (change) => {
|
||||||
try {
|
try {
|
||||||
await server.notification({
|
await server.notification({
|
||||||
@@ -516,7 +859,42 @@ Call list_peers at session start to understand who is online, their roles, and w
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Welcome notification: give Claude immediate context on connect.
|
||||||
|
// Triggers Claude to call mesh_info/list_peers without user input.
|
||||||
|
setTimeout(async () => {
|
||||||
|
const client = allClients()[0];
|
||||||
|
if (!client || client.status !== "open") return;
|
||||||
|
try {
|
||||||
|
const peers = await client.listPeers();
|
||||||
|
const peerNames = peers
|
||||||
|
.filter(p => p.displayName !== myName)
|
||||||
|
.map(p => p.displayName)
|
||||||
|
.join(", ") || "none";
|
||||||
|
await server.notification({
|
||||||
|
method: "notifications/claude/channel",
|
||||||
|
params: {
|
||||||
|
content: `[system] Connected as ${myName} to mesh ${client.meshSlug}. ${peers.length} peer(s) online: ${peerNames}. Call mesh_info for full details or set_summary to announce yourself.`,
|
||||||
|
meta: { kind: "welcome", mesh_slug: client.meshSlug },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch { /* best effort */ }
|
||||||
|
}, 3_000); // 3s delay: let WS connect + hello_ack complete first
|
||||||
|
|
||||||
|
// Event loop keepalive: Node.js stdout to a pipe is buffered. Without
|
||||||
|
// periodic event loop activity, stdout.write() from WS callbacks may not
|
||||||
|
// flush until the next I/O event. This 1s interval keeps the event loop
|
||||||
|
// ticking so channel notifications flush promptly — same pattern that made
|
||||||
|
// claude-intercom's push delivery reliable (its 1s HTTP poll had this
|
||||||
|
// effect as a side effect). The interval does nothing except prevent the
|
||||||
|
// event loop from settling.
|
||||||
|
const keepalive = setInterval(() => {
|
||||||
|
// Intentionally empty — the interval itself keeps the event loop active.
|
||||||
|
// Do NOT call .unref() — that would defeat the purpose.
|
||||||
|
}, 1_000);
|
||||||
|
void keepalive; // suppress unused warning
|
||||||
|
|
||||||
const shutdown = (): void => {
|
const shutdown = (): void => {
|
||||||
|
clearInterval(keepalive);
|
||||||
stopAll();
|
stopAll();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -269,4 +269,307 @@ export const TOOLS: Tool[] = [
|
|||||||
required: ["id"],
|
required: ["id"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- Vector tools ---
|
||||||
|
{
|
||||||
|
name: "vector_store",
|
||||||
|
description:
|
||||||
|
"Store an embedding in a per-mesh Qdrant collection. Auto-creates the collection on first use.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
collection: { type: "string", description: "Collection name" },
|
||||||
|
text: { type: "string", description: "Text to embed and store" },
|
||||||
|
metadata: {
|
||||||
|
type: "object",
|
||||||
|
description: "Optional metadata to attach",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["collection", "text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vector_search",
|
||||||
|
description: "Semantic search over stored embeddings in a collection.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
collection: { type: "string", description: "Collection name" },
|
||||||
|
query: { type: "string", description: "Search query text" },
|
||||||
|
limit: {
|
||||||
|
type: "number",
|
||||||
|
description: "Max results (default: 10)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["collection", "query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vector_delete",
|
||||||
|
description: "Remove an embedding from a collection.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
collection: { type: "string", description: "Collection name" },
|
||||||
|
id: { type: "string", description: "Embedding ID to delete" },
|
||||||
|
},
|
||||||
|
required: ["collection", "id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_collections",
|
||||||
|
description: "List vector collections in this mesh.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Graph tools ---
|
||||||
|
{
|
||||||
|
name: "graph_query",
|
||||||
|
description:
|
||||||
|
"Run a read-only Cypher query on the per-mesh Neo4j database.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
cypher: { type: "string", description: "Cypher MATCH query" },
|
||||||
|
},
|
||||||
|
required: ["cypher"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "graph_execute",
|
||||||
|
description:
|
||||||
|
"Run a write Cypher query (CREATE, MERGE, DELETE) on the per-mesh Neo4j database.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
cypher: { type: "string", description: "Cypher write query" },
|
||||||
|
},
|
||||||
|
required: ["cypher"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Mesh Database tools ---
|
||||||
|
{
|
||||||
|
name: "mesh_query",
|
||||||
|
description:
|
||||||
|
"Run a SELECT query on the per-mesh shared database.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sql: { type: "string", description: "SQL SELECT query" },
|
||||||
|
},
|
||||||
|
required: ["sql"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mesh_execute",
|
||||||
|
description:
|
||||||
|
"Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE).",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sql: { type: "string", description: "SQL statement" },
|
||||||
|
},
|
||||||
|
required: ["sql"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mesh_schema",
|
||||||
|
description:
|
||||||
|
"List tables and columns in the per-mesh shared database.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Stream tools ---
|
||||||
|
{
|
||||||
|
name: "create_stream",
|
||||||
|
description:
|
||||||
|
"Create a real-time data stream in the mesh.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string", description: "Stream name" },
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "publish",
|
||||||
|
description:
|
||||||
|
"Push data to a stream. Subscribers receive it in real-time.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
stream: { type: "string", description: "Stream name" },
|
||||||
|
data: { description: "Any JSON data to publish" },
|
||||||
|
},
|
||||||
|
required: ["stream", "data"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subscribe",
|
||||||
|
description:
|
||||||
|
"Subscribe to a stream. Data pushes arrive as channel notifications.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
stream: { type: "string", description: "Stream name" },
|
||||||
|
},
|
||||||
|
required: ["stream"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_streams",
|
||||||
|
description:
|
||||||
|
"List active streams in the mesh.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Context tools ---
|
||||||
|
{
|
||||||
|
name: "share_context",
|
||||||
|
description:
|
||||||
|
"Share your session understanding with the mesh. Call after exploring a codebase area.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
summary: {
|
||||||
|
type: "string",
|
||||||
|
description: "Summary of what you explored/learned",
|
||||||
|
},
|
||||||
|
files_read: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "File paths you read",
|
||||||
|
},
|
||||||
|
key_findings: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Key findings or insights",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Tags for categorization",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["summary"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_context",
|
||||||
|
description:
|
||||||
|
"Find context from peers who explored an area. Check before re-reading files another peer already analyzed.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "Search query (file path, topic, etc.)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_contexts",
|
||||||
|
description: "See what all peers currently know about the codebase.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Task tools ---
|
||||||
|
{
|
||||||
|
name: "create_task",
|
||||||
|
description: "Create a work item for the mesh.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
title: { type: "string", description: "Task title" },
|
||||||
|
assignee: {
|
||||||
|
type: "string",
|
||||||
|
description: "Peer name to assign (optional)",
|
||||||
|
},
|
||||||
|
priority: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["low", "normal", "high", "urgent"],
|
||||||
|
description: "Priority level (default: normal)",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Tags for categorization",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["title"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claim_task",
|
||||||
|
description: "Claim an unclaimed task to take ownership.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "Task ID" },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complete_task",
|
||||||
|
description: "Mark a task as done with an optional result summary.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "Task ID" },
|
||||||
|
result: {
|
||||||
|
type: "string",
|
||||||
|
description: "Summary of what was done",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_tasks",
|
||||||
|
description: "List tasks filtered by status and/or assignee.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["open", "claimed", "completed"],
|
||||||
|
description: "Filter by status",
|
||||||
|
},
|
||||||
|
assignee: {
|
||||||
|
type: "string",
|
||||||
|
description: "Filter by assignee name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Mesh info ---
|
||||||
|
{
|
||||||
|
name: "mesh_info",
|
||||||
|
description:
|
||||||
|
"Get a complete overview of the mesh: peers, groups, state, memory, files, tasks, streams, tables. Call on session start for full situational awareness.",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Diagnostics ---
|
||||||
|
{
|
||||||
|
name: "ping_mesh",
|
||||||
|
description:
|
||||||
|
"Send test messages through the full pipeline and measure round-trip timing per priority. Diagnoses push delivery issues.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
priorities: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string", enum: ["now", "next", "low"] },
|
||||||
|
description: "Priorities to test (default: [\"now\", \"next\"])",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface Config {
|
|||||||
meshes: JoinedMesh[];
|
meshes: JoinedMesh[];
|
||||||
displayName?: string; // per-session override, written by `claudemesh launch --name`
|
displayName?: string; // per-session override, written by `claudemesh launch --name`
|
||||||
groups?: GroupEntry[];
|
groups?: GroupEntry[];
|
||||||
|
messageMode?: "push" | "inbox" | "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
||||||
@@ -53,7 +54,7 @@ export function loadConfig(): Config {
|
|||||||
if (!parsed || !Array.isArray(parsed.meshes)) {
|
if (!parsed || !Array.isArray(parsed.meshes)) {
|
||||||
return { version: 1, meshes: [] };
|
return { version: 1, meshes: [] };
|
||||||
}
|
}
|
||||||
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups };
|
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, groups: parsed.groups, messageMode: parsed.messageMode };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
|||||||
@@ -415,6 +415,19 @@ export class BrokerClient {
|
|||||||
private fileUrlResolvers: Array<(result: { url: string; name: string } | null) => void> = [];
|
private fileUrlResolvers: Array<(result: { url: string; name: string } | null) => void> = [];
|
||||||
private fileListResolvers: Array<(files: Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) => void> = [];
|
private fileListResolvers: Array<(files: Array<{ id: string; name: string; size: number; tags: string[]; uploadedBy: string; uploadedAt: string; persistent: boolean }>) => void> = [];
|
||||||
private fileStatusResolvers: Array<(accesses: Array<{ peerName: string; accessedAt: string }>) => void> = [];
|
private fileStatusResolvers: Array<(accesses: Array<{ peerName: string; accessedAt: string }>) => void> = [];
|
||||||
|
private vectorStoredResolvers: Array<(id: string | null) => void> = [];
|
||||||
|
private vectorResultsResolvers: Array<(results: Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>) => void> = [];
|
||||||
|
private collectionListResolvers: Array<(collections: string[]) => void> = [];
|
||||||
|
private graphResultResolvers: Array<(rows: Array<Record<string, unknown>>) => void> = [];
|
||||||
|
private contextListResolvers: Array<(contexts: Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>) => void> = [];
|
||||||
|
private contextResultsResolvers: Array<(contexts: Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>) => void> = [];
|
||||||
|
private taskCreatedResolvers: Array<(id: string | null) => void> = [];
|
||||||
|
private taskListResolvers: Array<(tasks: Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>) => void> = [];
|
||||||
|
private meshQueryResolvers: Array<(result: { columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number } | null) => void> = [];
|
||||||
|
private meshSchemaResolvers: Array<(tables: Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>) => void> = [];
|
||||||
|
private streamCreatedResolvers: Array<(id: string | null) => void> = [];
|
||||||
|
private streamListResolvers: Array<(streams: Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>) => void> = [];
|
||||||
|
private streamDataHandlers = new Set<(data: { stream: string; data: unknown; publishedBy: string }) => void>();
|
||||||
|
|
||||||
async messageStatus(messageId: string): Promise<{ messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null> {
|
async messageStatus(messageId: string): Promise<{ messageId: string; targetSpec: string; delivered: boolean; deliveredAt: string | null; recipients: Array<{ name: string; pubkey: string; status: string }> } | null> {
|
||||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
@@ -513,8 +526,246 @@ export class BrokerClient {
|
|||||||
body: data,
|
body: data,
|
||||||
signal: AbortSignal.timeout(30_000),
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
const body = await res.json() as { ok?: boolean; fileId?: string };
|
const body = await res.json() as { ok?: boolean; fileId?: string; error?: string };
|
||||||
return body.fileId ?? null;
|
if (!res.ok || !body.fileId) {
|
||||||
|
throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
return body.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vectors ---
|
||||||
|
|
||||||
|
/** Store an embedding in a per-mesh Qdrant collection. */
|
||||||
|
async vectorStore(collection: string, text: string, metadata?: Record<string, unknown>): Promise<string | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.vectorStoredResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "vector_store", collection, text, metadata }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.vectorStoredResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.vectorStoredResolvers.splice(idx, 1); resolve(null); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Semantic search over stored embeddings. */
|
||||||
|
async vectorSearch(collection: string, query: string, limit?: number): Promise<Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.vectorResultsResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "vector_search", collection, query, limit }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.vectorResultsResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.vectorResultsResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove an embedding from a collection. */
|
||||||
|
async vectorDelete(collection: string, id: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "vector_delete", collection, id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List vector collections in this mesh. */
|
||||||
|
async listCollections(): Promise<string[]> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.collectionListResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "list_collections" }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.collectionListResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.collectionListResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Graph ---
|
||||||
|
|
||||||
|
/** Run a read query on the per-mesh Neo4j database. */
|
||||||
|
async graphQuery(cypher: string): Promise<Array<Record<string, unknown>>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.graphResultResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "graph_query", cypher }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.graphResultResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.graphResultResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run a write query (CREATE, MERGE, DELETE) on the per-mesh Neo4j database. */
|
||||||
|
async graphExecute(cypher: string): Promise<Array<Record<string, unknown>>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.graphResultResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "graph_execute", cypher }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.graphResultResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.graphResultResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Context ---
|
||||||
|
|
||||||
|
/** Share session understanding with the mesh. */
|
||||||
|
async shareContext(summary: string, filesRead?: string[], keyFindings?: string[], tags?: string[]): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "share_context", summary, filesRead, keyFindings, tags }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find context from peers who explored an area. */
|
||||||
|
async getContext(query: string): Promise<Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.contextResultsResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "get_context", query }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.contextResultsResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.contextResultsResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See what all peers currently know. */
|
||||||
|
async listContexts(): Promise<Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.contextListResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "list_contexts" }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.contextListResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.contextListResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tasks ---
|
||||||
|
|
||||||
|
/** Create a work item. */
|
||||||
|
async createTask(title: string, assignee?: string, priority?: string, tags?: string[]): Promise<string | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.taskCreatedResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "create_task", title, assignee, priority, tags }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.taskCreatedResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.taskCreatedResolvers.splice(idx, 1); resolve(null); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Claim an unclaimed task. */
|
||||||
|
async claimTask(id: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "claim_task", id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mark a task done with optional result. */
|
||||||
|
async completeTask(id: string, result?: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "complete_task", id, result }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List tasks filtered by status/assignee. */
|
||||||
|
async listTasks(status?: string, assignee?: string): Promise<Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.taskListResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "list_tasks", status, assignee }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.taskListResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.taskListResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mesh Database ---
|
||||||
|
|
||||||
|
/** Run a SELECT query on the per-mesh shared database. */
|
||||||
|
async meshQuery(sql: string): Promise<{ columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number } | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.meshQueryResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "mesh_query", sql }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.meshQueryResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.meshQueryResolvers.splice(idx, 1); resolve(null); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run DDL/DML on the per-mesh database (CREATE TABLE, INSERT, UPDATE, DELETE). */
|
||||||
|
async meshExecute(sql: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "mesh_execute", sql }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List tables and columns in the per-mesh shared database. */
|
||||||
|
async meshSchema(): Promise<Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.meshSchemaResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "mesh_schema" }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.meshSchemaResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.meshSchemaResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Streams ---
|
||||||
|
|
||||||
|
/** Create a real-time data stream in the mesh. */
|
||||||
|
async createStream(name: string): Promise<string | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.streamCreatedResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "create_stream", name }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.streamCreatedResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.streamCreatedResolvers.splice(idx, 1); resolve(null); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Push data to a stream. Subscribers receive it in real-time. */
|
||||||
|
async publish(stream: string, data: unknown): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "publish", stream, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subscribe to a stream. Data pushes arrive via onStreamData handler. */
|
||||||
|
async subscribe(stream: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "subscribe", stream }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unsubscribe from a stream. */
|
||||||
|
async unsubscribe(stream: string): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return;
|
||||||
|
this.ws.send(JSON.stringify({ type: "unsubscribe", stream }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List active streams in the mesh. */
|
||||||
|
async listStreams(): Promise<Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.streamListResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "list_streams" }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.streamListResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.streamListResolvers.splice(idx, 1); resolve([]); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subscribe to stream data pushes. Returns an unsubscribe function. */
|
||||||
|
onStreamData(handler: (data: { stream: string; data: unknown; publishedBy: string }) => void): () => void {
|
||||||
|
this.streamDataHandlers.add(handler);
|
||||||
|
return () => this.streamDataHandlers.delete(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Subscribe to state change notifications. Returns an unsubscribe function. */
|
/** Subscribe to state change notifications. Returns an unsubscribe function. */
|
||||||
@@ -523,6 +774,21 @@ export class BrokerClient {
|
|||||||
return () => this.stateChangeHandlers.delete(handler);
|
return () => this.stateChangeHandlers.delete(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mesh info ---
|
||||||
|
private meshInfoResolvers: Array<(result: Record<string, unknown> | null) => void> = [];
|
||||||
|
|
||||||
|
async meshInfo(): Promise<Record<string, unknown> | null> {
|
||||||
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.meshInfoResolvers.push(resolve);
|
||||||
|
this.ws!.send(JSON.stringify({ type: "mesh_info" }));
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = this.meshInfoResolvers.indexOf(resolve);
|
||||||
|
if (idx !== -1) { this.meshInfoResolvers.splice(idx, 1); resolve(null); }
|
||||||
|
}, 5_000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
if (this.helloTimer) clearTimeout(this.helloTimer);
|
if (this.helloTimer) clearTimeout(this.helloTimer);
|
||||||
@@ -698,6 +964,100 @@ export class BrokerClient {
|
|||||||
if (resolver) resolver(accesses);
|
if (resolver) resolver(accesses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === "vector_stored") {
|
||||||
|
const resolver = this.vectorStoredResolvers.shift();
|
||||||
|
if (resolver) resolver(msg.id ? String(msg.id) : null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "vector_results") {
|
||||||
|
const results = (msg.results as Array<{ id: string; text: string; score: number; metadata?: Record<string, unknown> }>) ?? [];
|
||||||
|
const resolver = this.vectorResultsResolvers.shift();
|
||||||
|
if (resolver) resolver(results);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "collection_list") {
|
||||||
|
const collections = (msg.collections as string[]) ?? [];
|
||||||
|
const resolver = this.collectionListResolvers.shift();
|
||||||
|
if (resolver) resolver(collections);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "graph_result") {
|
||||||
|
const rows = (msg.rows as Array<Record<string, unknown>>) ?? [];
|
||||||
|
const resolver = this.graphResultResolvers.shift();
|
||||||
|
if (resolver) resolver(rows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "context_list") {
|
||||||
|
const contexts = (msg.contexts as Array<{ peerName: string; summary: string; tags: string[]; updatedAt: string }>) ?? [];
|
||||||
|
const resolver = this.contextListResolvers.shift();
|
||||||
|
if (resolver) resolver(contexts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "context_results") {
|
||||||
|
const contexts = (msg.contexts as Array<{ peerName: string; summary: string; filesRead: string[]; keyFindings: string[]; tags: string[]; updatedAt: string }>) ?? [];
|
||||||
|
const resolver = this.contextResultsResolvers.shift();
|
||||||
|
if (resolver) resolver(contexts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "task_created") {
|
||||||
|
const resolver = this.taskCreatedResolvers.shift();
|
||||||
|
if (resolver) resolver(msg.id ? String(msg.id) : null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "task_list") {
|
||||||
|
const tasks = (msg.tasks as Array<{ id: string; title: string; assignee: string; status: string; priority: string; createdBy: string }>) ?? [];
|
||||||
|
const resolver = this.taskListResolvers.shift();
|
||||||
|
if (resolver) resolver(tasks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "mesh_query_result") {
|
||||||
|
const resolver = this.meshQueryResolvers.shift();
|
||||||
|
if (resolver) {
|
||||||
|
if (msg.columns) {
|
||||||
|
resolver({
|
||||||
|
columns: (msg.columns as string[]) ?? [],
|
||||||
|
rows: (msg.rows as Array<Record<string, unknown>>) ?? [],
|
||||||
|
rowCount: (msg.rowCount as number) ?? 0,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolver(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "mesh_schema_result") {
|
||||||
|
const tables = (msg.tables as Array<{ name: string; columns: Array<{ name: string; type: string; nullable: boolean }> }>) ?? [];
|
||||||
|
const resolver = this.meshSchemaResolvers.shift();
|
||||||
|
if (resolver) resolver(tables);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "stream_created") {
|
||||||
|
const resolver = this.streamCreatedResolvers.shift();
|
||||||
|
if (resolver) resolver(msg.id ? String(msg.id) : null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "stream_list") {
|
||||||
|
const streams = (msg.streams as Array<{ id: string; name: string; createdBy: string; subscriberCount: number }>) ?? [];
|
||||||
|
const resolver = this.streamListResolvers.shift();
|
||||||
|
if (resolver) resolver(streams);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "stream_data") {
|
||||||
|
const evt = {
|
||||||
|
stream: String(msg.stream ?? ""),
|
||||||
|
data: msg.data,
|
||||||
|
publishedBy: String(msg.publishedBy ?? ""),
|
||||||
|
};
|
||||||
|
for (const h of this.streamDataHandlers) {
|
||||||
|
try { h(evt); } catch { /* handler errors are not the transport's problem */ }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "mesh_info_result") {
|
||||||
|
const resolver = this.meshInfoResolvers.shift();
|
||||||
|
if (resolver) resolver(msg as Record<string, unknown>);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg.type === "error") {
|
if (msg.type === "error") {
|
||||||
this.debug(`broker error: ${msg.code} ${msg.message}`);
|
this.debug(`broker error: ${msg.code} ${msg.message}`);
|
||||||
const id = msg.id ? String(msg.id) : null;
|
const id = msg.id ? String(msg.id) : null;
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ ENV NEXT_PUBLIC_URL=$NEXT_PUBLIC_URL
|
|||||||
ENV NEXT_PUBLIC_PRODUCT_NAME=$NEXT_PUBLIC_PRODUCT_NAME
|
ENV NEXT_PUBLIC_PRODUCT_NAME=$NEXT_PUBLIC_PRODUCT_NAME
|
||||||
ENV NEXT_PUBLIC_DEFAULT_LOCALE=$NEXT_PUBLIC_DEFAULT_LOCALE
|
ENV NEXT_PUBLIC_DEFAULT_LOCALE=$NEXT_PUBLIC_DEFAULT_LOCALE
|
||||||
|
|
||||||
|
# TURBOPACK=0 forces webpack for production build — Payload CMS's
|
||||||
|
# richtext-lexical CSS imports fail under Turbopack.
|
||||||
|
ENV TURBOPACK=0
|
||||||
RUN npx turbo run build --filter=web...
|
RUN npx turbo run build --filter=web...
|
||||||
|
|
||||||
# Stage 2: runtime — standalone output only
|
# Stage 2: runtime — standalone output only
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const { withPayload } = require("@payloadcms/next/withPayload");
|
||||||
|
|
||||||
import env from "./env.config";
|
import env from "./env.config";
|
||||||
|
|
||||||
const INTERNAL_PACKAGES = [
|
const INTERNAL_PACKAGES = [
|
||||||
@@ -130,4 +133,4 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
|||||||
enabled: env.ANALYZE,
|
enabled: env.ANALYZE,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withBundleAnalyzer(config);
|
export default withPayload(withBundleAnalyzer(config));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build --no-turbopack",
|
||||||
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
|||||||
14
apps/web/src/app/(payload)/layout.tsx
Normal file
14
apps/web/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import "@payloadcms/next/css";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "CMS — claudemesh",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PayloadLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
apps/web/src/app/(payload)/payload/[[...segments]]/page.tsx
Normal file
16
apps/web/src/app/(payload)/payload/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck — Payload generates these types at build time
|
||||||
|
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||||
|
import { importMap } from "../importMap";
|
||||||
|
import config from "@payload-config";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
type Args = { params: Promise<{ segments: string[] }> };
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params }: Args) =>
|
||||||
|
generatePageMetadata({ config, params });
|
||||||
|
|
||||||
|
export default function Page({ params }: Args) {
|
||||||
|
return <RootPage config={config} params={params} importMap={importMap} />;
|
||||||
|
}
|
||||||
51
apps/web/src/app/(payload)/payload/importMap.js
Normal file
51
apps/web/src/app/(payload)/payload/importMap.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||||
|
|
||||||
|
export const importMap = {
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||||
|
}
|
||||||
@@ -4,27 +4,46 @@ import { Reveal, SectionIcon } from "./_reveal";
|
|||||||
|
|
||||||
const FEATURES = [
|
const FEATURES = [
|
||||||
{
|
{
|
||||||
key: "onboard",
|
key: "groups",
|
||||||
tab: "Onboarding",
|
tab: "Groups",
|
||||||
title: "Bootstrap any teammate",
|
title: "Peers self-organize through @groups",
|
||||||
body: "New hire's Claude inherits the team's context library on day one. No hand-holding, no week-long repo tour.",
|
body: "Name a group. Assign roles. Route messages to @frontend, @reviewers, or @all. The lead gathers; members contribute. No hardcoded pipelines — conventions in system prompts.",
|
||||||
|
code: `claudemesh launch --name Alice --role dev \\
|
||||||
|
--groups "frontend:lead,reviewers" -y`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "handoff",
|
key: "state",
|
||||||
tab: "Hand-offs",
|
tab: "Shared state",
|
||||||
title: "Work travels with context",
|
title: "Live facts the whole mesh can read",
|
||||||
body: "Pass an investigation to your teammate's session with full history — hypotheses, logs, files touched, commands run.",
|
body: "Set a value, every peer sees the change immediately. \"Is the deploy frozen?\" becomes a state read, not a conversation. Sprint number, PR queue, feature flags — shared operational truth.",
|
||||||
|
code: `set_state("deploy_frozen", true)
|
||||||
|
set_state("sprint", "2026-W14")
|
||||||
|
get_state("deploy_frozen") → true`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "refactor",
|
key: "memory",
|
||||||
tab: "Refactors",
|
tab: "Memory",
|
||||||
title: "Coordinate cross-cutting changes",
|
title: "The mesh gets smarter over time",
|
||||||
body: "Rename a type, rotate a secret, bump a schema — once. Every other agent picks up the change from its own repo.",
|
body: "New peers join with zero context. Memory stores institutional knowledge — decisions, incidents, lessons. Full-text searchable. Survives across sessions. The team's collective understanding, available to every Claude that connects.",
|
||||||
|
code: `remember("Payments API rate-limits at 100 req/s
|
||||||
|
after March incident", tags: ["payments"])
|
||||||
|
recall("rate limit") → ranked results`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "coordinate",
|
||||||
|
tab: "Coordination",
|
||||||
|
title: "Five patterns, zero orchestrator",
|
||||||
|
body: "Lead-gather: one lead collects from the group. Chain review: work passes through each member. Delegation: lead assigns subtasks. Voting: members set state, lead tallies. Flood: everyone responds. All through system prompts — no broker code.",
|
||||||
|
code: `send_message(to: "@frontend",
|
||||||
|
message: "auth API changed, update hooks")
|
||||||
|
send_message(to: "@pm",
|
||||||
|
message: "auth v2 done, 3 points, no blockers")`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Features = () => {
|
export const Features = () => {
|
||||||
const [active, setActive] = useState(0);
|
const [active, setActive] = useState(0);
|
||||||
|
const feature = FEATURES[active]!;
|
||||||
return (
|
return (
|
||||||
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
||||||
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
||||||
@@ -36,40 +55,19 @@ export const Features = () => {
|
|||||||
className="mx-auto max-w-4xl text-center text-[clamp(2rem,4.5vw,3.25rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
className="mx-auto max-w-4xl text-center text-[clamp(2rem,4.5vw,3.25rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
What could your mesh do?
|
What your mesh can do today
|
||||||
</h2>
|
</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={2} className="mt-10 flex justify-center">
|
<Reveal delay={2}>
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] px-4 py-3 text-[13px] text-[var(--cm-fg-secondary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
|
||||||
>
|
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
|
||||||
<span>curl -fsSL claudemesh.com/install | bash</span>
|
|
||||||
<button
|
|
||||||
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
|
||||||
aria-label="Copy"
|
|
||||||
>
|
|
||||||
copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
<Reveal delay={3}>
|
|
||||||
<p
|
<p
|
||||||
className="mt-4 text-center text-sm text-[var(--cm-fg-tertiary)]"
|
className="mx-auto mt-4 max-w-xl text-center text-sm text-[var(--cm-fg-tertiary)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
Free forever for solo developers · Or read the{" "}
|
30+ MCP tools. Groups, state, memory, messaging — all shipped.
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
|
||||||
>
|
|
||||||
documentation
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={4}>
|
<Reveal delay={3}>
|
||||||
<div className="mt-16 flex justify-center gap-2">
|
<div className="mt-12 flex flex-wrap justify-center gap-2">
|
||||||
{FEATURES.map((f, i) => (
|
{FEATURES.map((f, i) => (
|
||||||
<button
|
<button
|
||||||
key={f.key}
|
key={f.key}
|
||||||
@@ -86,19 +84,29 @@ export const Features = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-10 max-w-3xl rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-10 text-center">
|
<div className="mx-auto mt-8 max-w-3xl overflow-hidden rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)]">
|
||||||
<h3
|
<div className="p-8 pb-4">
|
||||||
className="mb-4 text-[28px] font-medium leading-tight text-[var(--cm-fg)]"
|
<h3
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
className="mb-3 text-[24px] font-medium leading-tight text-[var(--cm-fg)]"
|
||||||
>
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
{FEATURES[active]?.title}
|
>
|
||||||
</h3>
|
{feature.title}
|
||||||
<p
|
</h3>
|
||||||
className="text-[15px] leading-[1.65] text-[var(--cm-fg-secondary)]"
|
<p
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
className="text-[14px] leading-[1.65] text-[var(--cm-fg-secondary)]"
|
||||||
>
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
{FEATURES[active]?.body}
|
>
|
||||||
</p>
|
{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>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ export const Hero = () => {
|
|||||||
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
Peer mesh for Claude Code. Connect your sessions across repos and
|
Your Claude Code sessions form a team. They message each other,
|
||||||
machines. Messages are end-to-end encrypted, delivered mid-turn
|
share state, build collective memory, and self-organize through
|
||||||
as {"`<channel>`"} reminders. Your Claudes talk to each other; the
|
groups — all end-to-end encrypted. One command to launch. The broker
|
||||||
broker never sees plaintext.
|
routes ciphertext; it never reads your messages.
|
||||||
<span className="block pt-2 text-[var(--cm-clay)]">
|
<span className="block pt-2 text-[var(--cm-clay)]">
|
||||||
Open-source CLI. Free during public beta.
|
Open-source CLI. Free during public beta.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -229,31 +229,31 @@ type UseCase = {
|
|||||||
|
|
||||||
const USE_CASES: UseCase[] = [
|
const USE_CASES: UseCase[] = [
|
||||||
{
|
{
|
||||||
tag: "solo · multi-machine",
|
tag: "team · groups",
|
||||||
title: "One dev, three machines",
|
title: "Five agents, one sprint",
|
||||||
before:
|
before:
|
||||||
"Laptop, desktop, cloud dev box — each Claude session an island. You re-explain what you're doing every time you switch machines.",
|
"Each Claude works alone. When the frontend agent finishes auth, nobody tells the backend agent. You relay by hand. The PM asks for a status update; you copy-paste from three terminals.",
|
||||||
now: "Your desktop's Claude asks your laptop's Claude what it was touching. Context travels with you. The machine stops mattering.",
|
now: "Launch five sessions with --name and --groups. The @frontend lead finishes auth and messages @backend directly. The PM's Claude reads shared state: sprint number, PR queue, deploy status. Nobody relays anything.",
|
||||||
limits:
|
limits:
|
||||||
"Both peers have to be online. It shares live conversational context — not git state, not open files.",
|
"Peers must be online to receive direct messages. Group messages queue until delivery. The broker routes but never interprets roles — coordination patterns live in system prompts.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: "team · cross-repo",
|
tag: "knowledge · memory",
|
||||||
title: "Bug Alice fixed, Bob rediscovers",
|
title: "New hire's Claude knows the codebase",
|
||||||
before:
|
before:
|
||||||
"Alice in payments-api fixes a Stripe signature bug. Two weeks later, Bob in checkout-frontend hits the same thing. Alice's fix is buried in a PR thread. Bob re-solves it for three hours.",
|
"Alice in payments-api fixes a Stripe rate-limit bug. Three weeks later, a new hire hits the same wall. The fix is buried in a PR thread. They re-solve it for hours.",
|
||||||
now: "Bob's Claude asks the mesh: who's seen this? Alice's Claude volunteers with context. Bob solves in ten minutes. Alice isn't interrupted — her Claude shares the history on its own.",
|
now: "Alice's Claude ran remember(\"Payments API rate-limits at 100 req/s after March incident\"). The new hire's Claude runs recall(\"rate limit\") and gets ranked results. Ten minutes, not three hours.",
|
||||||
limits:
|
limits:
|
||||||
"Each Claude stays inside its own repo. Nobody's reading anyone else's files. Information flows at the agent layer, with a human still on the PR.",
|
"Memory stores text, not code diffs. Each Claude stays inside its own repo. Knowledge flows at the agent layer — the human still reviews the PR.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: "mobile · oversight",
|
tag: "coordination · state",
|
||||||
title: "CI fails at 3am",
|
title: "\"Is the deploy frozen?\" answered in zero messages",
|
||||||
before:
|
before:
|
||||||
"Alert on your phone. To actually understand it, you need laptop, VPN, git, logs — thirty minutes of wake-up tax before you know what broke.",
|
"You ask in Slack. Someone answers twenty minutes later. Meanwhile two PRs merge. The deploy breaks. Nobody knew it was frozen.",
|
||||||
now: "WhatsApp gateway peer forwards the alert. You ask the ops-server Claude what triggered it. It answers. You say roll it back. Done from bed.",
|
now: "set_state(\"deploy_frozen\", true). Every peer sees the change instantly. get_state(\"deploy_frozen\") returns true. No conversation needed. Shared operational facts, not shared opinions.",
|
||||||
limits:
|
limits:
|
||||||
"The WhatsApp/phone gateway is on the v0.2 roadmap — the protocol is ready, the bot isn't shipped yet. Someone could build it in a weekend.",
|
"State is operational — it lives as long as the mesh. Use memory for permanent knowledge. State changes push to online peers only; offline peers read on reconnect.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,41 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
|
qdrant:
|
||||||
|
image: qdrant/qdrant
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- qdrant-data:/qdrant/storage
|
||||||
|
expose:
|
||||||
|
- "6333"
|
||||||
|
networks:
|
||||||
|
- claudemesh-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:6333/readyz"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
neo4j:
|
||||||
|
image: neo4j:5
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-changeme}
|
||||||
|
NEO4J_PLUGINS: '[]'
|
||||||
|
volumes:
|
||||||
|
- neo4j-data:/data
|
||||||
|
expose:
|
||||||
|
- "7687"
|
||||||
|
- "7474"
|
||||||
|
networks:
|
||||||
|
- claudemesh-internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "${NEO4J_PASSWORD:-changeme}", "RETURN 1"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 30s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
broker:
|
broker:
|
||||||
image: ${BROKER_IMAGE:-claudemesh-broker:latest}
|
image: ${BROKER_IMAGE:-claudemesh-broker:latest}
|
||||||
restart: always
|
restart: always
|
||||||
@@ -64,6 +99,10 @@ services:
|
|||||||
MINIO_ACCESS_KEY: claudemesh
|
MINIO_ACCESS_KEY: claudemesh
|
||||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-changeme}
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-changeme}
|
||||||
MINIO_USE_SSL: "false"
|
MINIO_USE_SSL: "false"
|
||||||
|
QDRANT_URL: http://qdrant:6333
|
||||||
|
NEO4J_URL: bolt://neo4j:7687
|
||||||
|
NEO4J_USER: neo4j
|
||||||
|
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-changeme}
|
||||||
expose:
|
expose:
|
||||||
- "7900"
|
- "7900"
|
||||||
networks:
|
networks:
|
||||||
@@ -72,6 +111,10 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
minio:
|
minio:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
qdrant:
|
||||||
|
condition: service_healthy
|
||||||
|
neo4j:
|
||||||
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "bun", "-e", "fetch('http://localhost:7900/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
|
test: ["CMD", "bun", "-e", "fetch('http://localhost:7900/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@@ -114,6 +157,8 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
minio-data:
|
minio-data:
|
||||||
|
qdrant-data:
|
||||||
|
neo4j-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
# Coolify's shared Traefik network — must already exist on the host
|
# Coolify's shared Traefik network — must already exist on the host
|
||||||
|
|||||||
33
packages/db/migrations/0010_add-context-and-tasks.sql
Normal file
33
packages/db/migrations/0010_add-context-and-tasks.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
CREATE TABLE "mesh"."context" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"mesh_id" text NOT NULL,
|
||||||
|
"presence_id" text,
|
||||||
|
"peer_name" text,
|
||||||
|
"summary" text NOT NULL,
|
||||||
|
"files_read" text[] DEFAULT '{}',
|
||||||
|
"key_findings" text[] DEFAULT '{}',
|
||||||
|
"tags" text[] DEFAULT '{}',
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "mesh"."task" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"mesh_id" text NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"assignee" text,
|
||||||
|
"claimed_by_name" text,
|
||||||
|
"claimed_by_presence" text,
|
||||||
|
"priority" text DEFAULT 'normal' NOT NULL,
|
||||||
|
"status" text DEFAULT 'open' NOT NULL,
|
||||||
|
"tags" text[] DEFAULT '{}',
|
||||||
|
"result" text,
|
||||||
|
"created_by_name" text,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"claimed_at" timestamp,
|
||||||
|
"completed_at" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."context" ADD CONSTRAINT "context_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."context" ADD CONSTRAINT "context_presence_id_presence_id_fk" FOREIGN KEY ("presence_id") REFERENCES "mesh"."presence"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."task" ADD CONSTRAINT "task_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."task" ADD CONSTRAINT "task_claimed_by_presence_presence_id_fk" FOREIGN KEY ("claimed_by_presence") REFERENCES "mesh"."presence"("id") ON DELETE no action ON UPDATE no action;
|
||||||
10
packages/db/migrations/0011_add-streams.sql
Normal file
10
packages/db/migrations/0011_add-streams.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE "mesh"."stream" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"mesh_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"created_by_name" text,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "mesh"."stream" ADD CONSTRAINT "stream_mesh_id_mesh_id_fk" FOREIGN KEY ("mesh_id") REFERENCES "mesh"."mesh"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "stream_mesh_name_idx" ON "mesh"."stream" USING btree ("mesh_id","name");
|
||||||
3467
packages/db/migrations/meta/0010_snapshot.json
Normal file
3467
packages/db/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3548
packages/db/migrations/meta/0011_snapshot.json
Normal file
3548
packages/db/migrations/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,20 @@
|
|||||||
"when": 1775480008546,
|
"when": 1775480008546,
|
||||||
"tag": "0009_add-file-tables",
|
"tag": "0009_add-file-tables",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1775480729014,
|
||||||
|
"tag": "0010_add-context-and-tasks",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1775481222701,
|
||||||
|
"tag": "0011_add-streams",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -327,6 +327,68 @@ export const meshFileAccess = meshSchema.table("file_access", {
|
|||||||
accessedAt: timestamp().defaultNow().notNull(),
|
accessedAt: timestamp().defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-peer context snapshot. Each peer (presence) has at most one context
|
||||||
|
* entry per mesh, upserted on each share_context call. Allows peers to
|
||||||
|
* discover what others are working on, which files they've read, and
|
||||||
|
* key findings — without sending a direct message.
|
||||||
|
*/
|
||||||
|
export const meshContext = meshSchema.table("context", {
|
||||||
|
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||||
|
meshId: text()
|
||||||
|
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
presenceId: text().references(() => presence.id, { onDelete: "cascade" }),
|
||||||
|
peerName: text(),
|
||||||
|
summary: text().notNull(),
|
||||||
|
filesRead: text().array().default([]),
|
||||||
|
keyFindings: text().array().default([]),
|
||||||
|
tags: text().array().default([]),
|
||||||
|
updatedAt: timestamp().defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mesh-scoped task board. Peers can create tasks, claim them, and mark
|
||||||
|
* them done. Lightweight project management for multi-agent workflows.
|
||||||
|
*/
|
||||||
|
export const meshTask = meshSchema.table("task", {
|
||||||
|
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||||
|
meshId: text()
|
||||||
|
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
title: text().notNull(),
|
||||||
|
assignee: text(),
|
||||||
|
claimedByName: text(),
|
||||||
|
claimedByPresence: text().references(() => presence.id),
|
||||||
|
priority: text().notNull().default("normal"),
|
||||||
|
status: text().notNull().default("open"),
|
||||||
|
tags: text().array().default([]),
|
||||||
|
result: text(),
|
||||||
|
createdByName: text(),
|
||||||
|
createdAt: timestamp().defaultNow().notNull(),
|
||||||
|
claimedAt: timestamp(),
|
||||||
|
completedAt: timestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Named real-time data channels within a mesh. One peer publishes, all
|
||||||
|
* subscribers receive. No message history — streams are live.
|
||||||
|
* Use cases: build logs, deploy status, monitoring data, live code diffs.
|
||||||
|
*/
|
||||||
|
export const meshStream = meshSchema.table(
|
||||||
|
"stream",
|
||||||
|
{
|
||||||
|
id: text().primaryKey().notNull().$defaultFn(generateId),
|
||||||
|
meshId: text()
|
||||||
|
.references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
name: text().notNull(),
|
||||||
|
createdByName: text(),
|
||||||
|
createdAt: timestamp().defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex("stream_mesh_name_idx").on(table.meshId, table.name)],
|
||||||
|
);
|
||||||
|
|
||||||
export const meshRelations = relations(mesh, ({ one, many }) => ({
|
export const meshRelations = relations(mesh, ({ one, many }) => ({
|
||||||
owner: one(user, {
|
owner: one(user, {
|
||||||
fields: [mesh.ownerUserId],
|
fields: [mesh.ownerUserId],
|
||||||
@@ -469,3 +531,45 @@ export type SelectMeshFile = typeof meshFile.$inferSelect;
|
|||||||
export type InsertMeshFile = typeof meshFile.$inferInsert;
|
export type InsertMeshFile = typeof meshFile.$inferInsert;
|
||||||
export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect;
|
export type SelectMeshFileAccess = typeof meshFileAccess.$inferSelect;
|
||||||
export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert;
|
export type InsertMeshFileAccess = typeof meshFileAccess.$inferInsert;
|
||||||
|
export const selectMeshContextSchema = createSelectSchema(meshContext);
|
||||||
|
export const insertMeshContextSchema = createInsertSchema(meshContext);
|
||||||
|
export const selectMeshTaskSchema = createSelectSchema(meshTask);
|
||||||
|
export const insertMeshTaskSchema = createInsertSchema(meshTask);
|
||||||
|
export type SelectMeshContext = typeof meshContext.$inferSelect;
|
||||||
|
export type InsertMeshContext = typeof meshContext.$inferInsert;
|
||||||
|
export type SelectMeshTask = typeof meshTask.$inferSelect;
|
||||||
|
export type InsertMeshTask = typeof meshTask.$inferInsert;
|
||||||
|
|
||||||
|
export const meshContextRelations = relations(meshContext, ({ one }) => ({
|
||||||
|
mesh: one(mesh, {
|
||||||
|
fields: [meshContext.meshId],
|
||||||
|
references: [mesh.id],
|
||||||
|
}),
|
||||||
|
presence: one(presence, {
|
||||||
|
fields: [meshContext.presenceId],
|
||||||
|
references: [presence.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const meshTaskRelations = relations(meshTask, ({ one }) => ({
|
||||||
|
mesh: one(mesh, {
|
||||||
|
fields: [meshTask.meshId],
|
||||||
|
references: [mesh.id],
|
||||||
|
}),
|
||||||
|
claimedPresence: one(presence, {
|
||||||
|
fields: [meshTask.claimedByPresence],
|
||||||
|
references: [presence.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const meshStreamRelations = relations(meshStream, ({ one }) => ({
|
||||||
|
mesh: one(mesh, {
|
||||||
|
fields: [meshStream.meshId],
|
||||||
|
references: [mesh.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const selectMeshStreamSchema = createSelectSchema(meshStream);
|
||||||
|
export const insertMeshStreamSchema = createInsertSchema(meshStream);
|
||||||
|
export type SelectMeshStream = typeof meshStream.$inferSelect;
|
||||||
|
export type InsertMeshStream = typeof meshStream.$inferInsert;
|
||||||
|
|||||||
246
pnpm-lock.yaml
generated
246
pnpm-lock.yaml
generated
@@ -111,6 +111,9 @@ importers:
|
|||||||
|
|
||||||
apps/broker:
|
apps/broker:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@qdrant/js-client-rest':
|
||||||
|
specifier: 1.17.0
|
||||||
|
version: 1.17.0(typescript@5.9.3)
|
||||||
'@turbostarter/db':
|
'@turbostarter/db':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/db
|
version: link:../../packages/db
|
||||||
@@ -123,6 +126,12 @@ importers:
|
|||||||
libsodium-wrappers:
|
libsodium-wrappers:
|
||||||
specifier: 0.7.15
|
specifier: 0.7.15
|
||||||
version: 0.7.15
|
version: 0.7.15
|
||||||
|
minio:
|
||||||
|
specifier: 8.0.7
|
||||||
|
version: 8.0.7
|
||||||
|
neo4j-driver:
|
||||||
|
specifier: 6.0.1
|
||||||
|
version: 6.0.1
|
||||||
ws:
|
ws:
|
||||||
specifier: 8.20.0
|
specifier: 8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
@@ -4582,6 +4591,16 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.8
|
'@opentelemetry/api': ^1.8
|
||||||
|
|
||||||
|
'@qdrant/js-client-rest@1.17.0':
|
||||||
|
resolution: {integrity: sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==}
|
||||||
|
engines: {node: '>=18.17.0', pnpm: '>=8'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.7'
|
||||||
|
|
||||||
|
'@qdrant/openapi-typescript-fetch@1.2.6':
|
||||||
|
resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==}
|
||||||
|
engines: {node: '>=18.0.0', pnpm: '>=8'}
|
||||||
|
|
||||||
'@radix-ui/colors@3.0.0':
|
'@radix-ui/colors@3.0.0':
|
||||||
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
|
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
|
||||||
|
|
||||||
@@ -7843,6 +7862,9 @@ packages:
|
|||||||
async-limiter@1.0.1:
|
async-limiter@1.0.1:
|
||||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||||
|
|
||||||
|
async@3.2.6:
|
||||||
|
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||||
|
|
||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
@@ -8034,6 +8056,9 @@ packages:
|
|||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
|
block-stream2@2.1.0:
|
||||||
|
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -8068,6 +8093,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
browser-or-node@2.1.1:
|
||||||
|
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.1:
|
||||||
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -8079,12 +8107,19 @@ packages:
|
|||||||
bson-objectid@2.0.4:
|
bson-objectid@2.0.4:
|
||||||
resolution: {integrity: sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==}
|
resolution: {integrity: sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==}
|
||||||
|
|
||||||
|
buffer-crc32@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
busboy@1.6.0:
|
busboy@1.6.0:
|
||||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||||
engines: {node: '>=10.16.0'}
|
engines: {node: '>=10.16.0'}
|
||||||
@@ -9271,6 +9306,9 @@ packages:
|
|||||||
eventemitter3@4.0.7:
|
eventemitter3@4.0.7:
|
||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4:
|
||||||
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
events@3.3.0:
|
events@3.3.0:
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
@@ -9486,10 +9524,17 @@ packages:
|
|||||||
fast-uri@3.0.6:
|
fast-uri@3.0.6:
|
||||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||||
|
|
||||||
|
fast-xml-builder@1.1.4:
|
||||||
|
resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==}
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
fast-xml-parser@5.5.10:
|
||||||
|
resolution: {integrity: sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||||
|
|
||||||
@@ -11220,6 +11265,10 @@ packages:
|
|||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
|
minio@8.0.7:
|
||||||
|
resolution: {integrity: sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==}
|
||||||
|
engines: {node: ^16 || ^18 || >=20}
|
||||||
|
|
||||||
minipass@7.1.2:
|
minipass@7.1.2:
|
||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
@@ -11329,6 +11378,16 @@ packages:
|
|||||||
neo-async@2.6.2:
|
neo-async@2.6.2:
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
|
|
||||||
|
neo4j-driver-bolt-connection@6.0.1:
|
||||||
|
resolution: {integrity: sha512-1KyG73TO+CwnYJisdHD0sjUw9yR+P5q3JFcmVPzsHT4/whzCjuXSMpmY4jZcHH2PdY2cBUq4l/6WcDiPMxW2UA==}
|
||||||
|
|
||||||
|
neo4j-driver-core@6.0.1:
|
||||||
|
resolution: {integrity: sha512-5I2KxICAvcHxnWdJyDqwu8PBAQvWVTlQH2ve3VQmtVdJScPqWhpXN1PiX5IIl+cRF3pFpz9GQF53B5n6s0QQUQ==}
|
||||||
|
|
||||||
|
neo4j-driver@6.0.1:
|
||||||
|
resolution: {integrity: sha512-8DDF2MwEJNz7y7cp97x4u8fmVIP4CWS8qNBxdwxTG0fWtsS+2NdeC+7uXwmmuFOpHvkfXqv63uWY73bfDtOH8Q==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
nested-error-stacks@2.0.1:
|
nested-error-stacks@2.0.1:
|
||||||
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
|
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
|
||||||
|
|
||||||
@@ -11717,6 +11776,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
|
path-expression-matcher@1.2.1:
|
||||||
|
resolution: {integrity: sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
path-is-absolute@1.0.1:
|
path-is-absolute@1.0.1:
|
||||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -13111,6 +13174,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
|
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
|
||||||
|
stream-chain@2.2.5:
|
||||||
|
resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==}
|
||||||
|
|
||||||
|
stream-json@1.9.1:
|
||||||
|
resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
|
||||||
|
|
||||||
streamsearch@1.1.0:
|
streamsearch@1.1.0:
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -13208,6 +13277,9 @@ packages:
|
|||||||
strnum@2.1.1:
|
strnum@2.1.1:
|
||||||
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
||||||
|
|
||||||
|
strnum@2.2.2:
|
||||||
|
resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==}
|
||||||
|
|
||||||
strtok3@10.3.5:
|
strtok3@10.3.5:
|
||||||
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -13379,6 +13451,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
|
|
||||||
through@2.3.8:
|
through@2.3.8:
|
||||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
|
|
||||||
@@ -13644,8 +13719,8 @@ packages:
|
|||||||
undici-types@7.8.0:
|
undici-types@7.8.0:
|
||||||
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||||
|
|
||||||
undici@6.21.3:
|
undici@6.24.1:
|
||||||
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
undici@7.24.4:
|
undici@7.24.4:
|
||||||
@@ -14173,6 +14248,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
|
resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
xmlbuilder@11.0.1:
|
xmlbuilder@11.0.1:
|
||||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -16649,7 +16728,7 @@ snapshots:
|
|||||||
structured-headers: 0.4.1
|
structured-headers: 0.4.1
|
||||||
tar: 7.5.2
|
tar: 7.5.2
|
||||||
terminal-link: 2.1.1
|
terminal-link: 2.1.1
|
||||||
undici: 6.21.3
|
undici: 6.24.1
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
ws: 8.20.0
|
ws: 8.20.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -18379,6 +18458,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@qdrant/js-client-rest@1.17.0(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||||
|
typescript: 5.9.3
|
||||||
|
undici: 6.24.1
|
||||||
|
|
||||||
|
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||||
|
|
||||||
'@radix-ui/colors@3.0.0': {}
|
'@radix-ui/colors@3.0.0': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
@@ -20879,7 +20966,7 @@ snapshots:
|
|||||||
'@sentry/bundler-plugin-core': 4.6.1(encoding@0.1.13)
|
'@sentry/bundler-plugin-core': 4.6.1(encoding@0.1.13)
|
||||||
unplugin: 1.0.1
|
unplugin: 1.0.1
|
||||||
uuid: 9.0.1
|
uuid: 9.0.1
|
||||||
webpack: 5.100.2
|
webpack: 5.100.2(esbuild@0.25.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -22249,7 +22336,7 @@ snapshots:
|
|||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
acorn-import-attributes@1.9.5(acorn@8.16.0):
|
acorn-import-attributes@1.9.5(acorn@8.16.0):
|
||||||
@@ -22491,6 +22578,8 @@ snapshots:
|
|||||||
|
|
||||||
async-limiter@1.0.1: {}
|
async-limiter@1.0.1: {}
|
||||||
|
|
||||||
|
async@3.2.6: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
@@ -22724,6 +22813,10 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
block-stream2@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -22772,6 +22865,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
|
|
||||||
|
browser-or-node@2.1.1: {}
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001727
|
caniuse-lite: 1.0.30001727
|
||||||
@@ -22785,6 +22880,8 @@ snapshots:
|
|||||||
|
|
||||||
bson-objectid@2.0.4: {}
|
bson-objectid@2.0.4: {}
|
||||||
|
|
||||||
|
buffer-crc32@1.0.0: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
@@ -22792,6 +22889,11 @@ snapshots:
|
|||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
busboy@1.6.0:
|
busboy@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
streamsearch: 1.1.0
|
streamsearch: 1.1.0
|
||||||
@@ -23356,8 +23458,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
|
|
||||||
decode-uri-component@0.2.2:
|
decode-uri-component@0.2.2: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -24095,6 +24196,8 @@ snapshots:
|
|||||||
|
|
||||||
eventemitter3@4.0.7: {}
|
eventemitter3@4.0.7: {}
|
||||||
|
|
||||||
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
eventsource-parser@3.0.6: {}
|
eventsource-parser@3.0.6: {}
|
||||||
@@ -24385,10 +24488,20 @@ snapshots:
|
|||||||
|
|
||||||
fast-uri@3.0.6: {}
|
fast-uri@3.0.6: {}
|
||||||
|
|
||||||
|
fast-xml-builder@1.1.4:
|
||||||
|
dependencies:
|
||||||
|
path-expression-matcher: 1.2.1
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 2.1.1
|
strnum: 2.1.1
|
||||||
|
|
||||||
|
fast-xml-parser@5.5.10:
|
||||||
|
dependencies:
|
||||||
|
fast-xml-builder: 1.1.4
|
||||||
|
path-expression-matcher: 1.2.1
|
||||||
|
strnum: 2.2.2
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.1.0
|
reusify: 1.1.0
|
||||||
@@ -24454,8 +24567,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
|
|
||||||
filter-obj@1.1.0:
|
filter-obj@1.1.0: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
finalhandler@1.1.2:
|
finalhandler@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -26613,6 +26725,22 @@ snapshots:
|
|||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
|
minio@8.0.7:
|
||||||
|
dependencies:
|
||||||
|
async: 3.2.6
|
||||||
|
block-stream2: 2.1.0
|
||||||
|
browser-or-node: 2.1.1
|
||||||
|
buffer-crc32: 1.0.0
|
||||||
|
eventemitter3: 5.0.4
|
||||||
|
fast-xml-parser: 5.5.10
|
||||||
|
ipaddr.js: 2.2.0
|
||||||
|
lodash: 4.17.21
|
||||||
|
mime-types: 2.1.35
|
||||||
|
query-string: 7.1.3
|
||||||
|
stream-json: 1.9.1
|
||||||
|
through2: 4.0.2
|
||||||
|
xml2js: 0.6.2
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
minizlib@3.1.0:
|
minizlib@3.1.0:
|
||||||
@@ -26704,6 +26832,20 @@ snapshots:
|
|||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
|
neo4j-driver-bolt-connection@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
buffer: 6.0.3
|
||||||
|
neo4j-driver-core: 6.0.1
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
|
||||||
|
neo4j-driver-core@6.0.1: {}
|
||||||
|
|
||||||
|
neo4j-driver@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
neo4j-driver-bolt-connection: 6.0.1
|
||||||
|
neo4j-driver-core: 6.0.1
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
nested-error-stacks@2.0.1:
|
nested-error-stacks@2.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -26813,7 +26955,7 @@ snapshots:
|
|||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
styled-jsx: 5.1.6(react@19.2.3)
|
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 16.2.2
|
'@next/swc-darwin-arm64': 16.2.2
|
||||||
'@next/swc-darwin-x64': 16.2.2
|
'@next/swc-darwin-x64': 16.2.2
|
||||||
@@ -27246,6 +27388,8 @@ snapshots:
|
|||||||
|
|
||||||
path-exists@5.0.0: {}
|
path-exists@5.0.0: {}
|
||||||
|
|
||||||
|
path-expression-matcher@1.2.1: {}
|
||||||
|
|
||||||
path-is-absolute@1.0.1: {}
|
path-is-absolute@1.0.1: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
@@ -27712,7 +27856,6 @@ snapshots:
|
|||||||
filter-obj: 1.1.0
|
filter-obj: 1.1.0
|
||||||
split-on-first: 1.1.0
|
split-on-first: 1.1.0
|
||||||
strict-uri-encode: 2.0.0
|
strict-uri-encode: 2.0.0
|
||||||
optional: true
|
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
@@ -28714,8 +28857,7 @@ snapshots:
|
|||||||
immutable: 4.3.8
|
immutable: 4.3.8
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
sax@1.4.1:
|
sax@1.4.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
saxes@6.0.0:
|
saxes@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -29133,8 +29275,7 @@ snapshots:
|
|||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
split-on-first@1.1.0:
|
split-on-first@1.1.0: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
split2@4.2.0: {}
|
split2@4.2.0: {}
|
||||||
|
|
||||||
@@ -29179,10 +29320,15 @@ snapshots:
|
|||||||
stream-buffers@2.2.0:
|
stream-buffers@2.2.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
stream-chain@2.2.5: {}
|
||||||
|
|
||||||
|
stream-json@1.9.1:
|
||||||
|
dependencies:
|
||||||
|
stream-chain: 2.2.5
|
||||||
|
|
||||||
streamsearch@1.1.0: {}
|
streamsearch@1.1.0: {}
|
||||||
|
|
||||||
strict-uri-encode@2.0.0:
|
strict-uri-encode@2.0.0: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -29294,6 +29440,8 @@ snapshots:
|
|||||||
|
|
||||||
strnum@2.1.1: {}
|
strnum@2.1.1: {}
|
||||||
|
|
||||||
|
strnum@2.2.2: {}
|
||||||
|
|
||||||
strtok3@10.3.5:
|
strtok3@10.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
@@ -29329,12 +29477,6 @@ snapshots:
|
|||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
optional: true
|
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.2.3):
|
|
||||||
dependencies:
|
|
||||||
client-only: 0.0.1
|
|
||||||
react: 19.2.3
|
|
||||||
|
|
||||||
styleq@0.1.3:
|
styleq@0.1.3:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -29492,15 +29634,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
esbuild: 0.25.0
|
esbuild: 0.25.0
|
||||||
|
|
||||||
terser-webpack-plugin@5.3.14(webpack@5.100.2):
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
jest-worker: 27.5.1
|
|
||||||
schema-utils: 4.3.2
|
|
||||||
serialize-javascript: 6.0.2
|
|
||||||
terser: 5.43.1
|
|
||||||
webpack: 5.100.2
|
|
||||||
|
|
||||||
terser@5.43.1:
|
terser@5.43.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/source-map': 0.3.10
|
'@jridgewell/source-map': 0.3.10
|
||||||
@@ -29532,6 +29665,10 @@ snapshots:
|
|||||||
|
|
||||||
throttleit@2.1.0: {}
|
throttleit@2.1.0: {}
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
@@ -29723,7 +29860,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.2
|
||||||
|
|
||||||
typed-array-buffer@1.0.3:
|
typed-array-buffer@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -29794,8 +29931,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.8.0: {}
|
undici-types@7.8.0: {}
|
||||||
|
|
||||||
undici@6.21.3:
|
undici@6.24.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
undici@7.24.4: {}
|
undici@7.24.4: {}
|
||||||
|
|
||||||
@@ -30200,38 +30336,6 @@ snapshots:
|
|||||||
|
|
||||||
webpack-virtual-modules@0.5.0: {}
|
webpack-virtual-modules@0.5.0: {}
|
||||||
|
|
||||||
webpack@5.100.2:
|
|
||||||
dependencies:
|
|
||||||
'@types/eslint-scope': 3.7.7
|
|
||||||
'@types/estree': 1.0.8
|
|
||||||
'@types/json-schema': 7.0.15
|
|
||||||
'@webassemblyjs/ast': 1.14.1
|
|
||||||
'@webassemblyjs/wasm-edit': 1.14.1
|
|
||||||
'@webassemblyjs/wasm-parser': 1.14.1
|
|
||||||
acorn: 8.16.0
|
|
||||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
|
||||||
browserslist: 4.25.1
|
|
||||||
chrome-trace-event: 1.0.4
|
|
||||||
enhanced-resolve: 5.18.3
|
|
||||||
es-module-lexer: 1.7.0
|
|
||||||
eslint-scope: 5.1.1
|
|
||||||
events: 3.3.0
|
|
||||||
glob-to-regexp: 0.4.1
|
|
||||||
graceful-fs: 4.2.11
|
|
||||||
json-parse-even-better-errors: 2.3.1
|
|
||||||
loader-runner: 4.3.0
|
|
||||||
mime-types: 2.1.35
|
|
||||||
neo-async: 2.6.2
|
|
||||||
schema-utils: 4.3.2
|
|
||||||
tapable: 2.2.2
|
|
||||||
terser-webpack-plugin: 5.3.14(webpack@5.100.2)
|
|
||||||
watchpack: 2.4.4
|
|
||||||
webpack-sources: 3.3.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@swc/core'
|
|
||||||
- esbuild
|
|
||||||
- uglify-js
|
|
||||||
|
|
||||||
webpack@5.100.2(esbuild@0.25.0):
|
webpack@5.100.2(esbuild@0.25.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint-scope': 3.7.7
|
'@types/eslint-scope': 3.7.7
|
||||||
@@ -30406,8 +30510,12 @@ snapshots:
|
|||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
xmlbuilder@11.0.1:
|
xml2js@0.6.2:
|
||||||
optional: true
|
dependencies:
|
||||||
|
sax: 1.4.1
|
||||||
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1: {}
|
||||||
|
|
||||||
xmlbuilder@15.1.1:
|
xmlbuilder@15.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|||||||
Reference in New Issue
Block a user