v2.0 - Architecture unifiée avec SQLite

- MCP unifié : mcp-partner remplace mcp-master et mcp-slave
- Messages bufferisés : SQLite stocke tout, pas besoin d'être connecté en permanence
- Tools simplifiés : register, talk, check_messages, listen, reply, list_partners, history
- Suppression du hook Stop (plus nécessaire avec reply explicite)
- Heartbeat 30s pour éviter les déconnexions idle
- ID basé sur le nom du dossier (unique par projet)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
StillHammer 2026-01-24 04:05:01 +07:00
parent b7766853cf
commit 0bb8af199e
10 changed files with 1273 additions and 926 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
node_modules/ node_modules/
.state/ .state/
conversations/ conversations/
data/
*.log *.log
*.db

194
README.md
View File

@ -1,22 +1,21 @@
# MCP Claude Duo # MCP Claude Duo
Fait discuter deux instances Claude Code ensemble via MCP. MCP pour faire discuter plusieurs instances Claude Code ensemble.
## Architecture ## Architecture v2
``` ```
Terminal 1 (Master) Terminal 2 (Slave) ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
┌─────────────────┐ ┌─────────────────┐ │ Claude A │ │ Broker │ │ Claude B │
│ Claude Code │ │ Claude Code │ │ (projet-a) │◄───►│ HTTP + SQLite │◄───►│ (projet-b) │
│ + MCP Master │ ┌───────┐ │ + MCP Slave │ │ + mcp-partner │ │ │ │ + mcp-partner │
│ │ │Broker │ │ + Hook Stop │ └─────────────────┘ └─────────────────┘ └─────────────────┘
│ talk("yo") ────┼────────►│ HTTP │─────────►│ │
│ │ │ │ │ reçoit "yo" │
│ │ │ │ │ répond "salut" │
│ reçoit "salut"◄┼─────────│ │◄─────────┼── (hook envoie) │
└─────────────────┘ └───────┘ └─────────────────┘
``` ```
- **Un seul MCP unifié** : `mcp-partner` pour tout le monde
- **Messages bufferisés** : SQLite stocke les messages, pas besoin d'être connecté en permanence
- **Bidirectionnel** : tout le monde peut parler à tout le monde
## Installation ## Installation
```bash ```bash
@ -26,130 +25,93 @@ npm install
## Démarrage ## Démarrage
### 1. Lancer le broker (dans un terminal séparé) ### 1. Lancer le broker
```bash ```bash
npm run broker npm run broker
``` ```
Le broker tourne sur `http://localhost:3210` par défaut. Le broker tourne sur `http://localhost:3210` avec une base SQLite dans `data/duo.db`.
### 2. Configurer le Master (Terminal 1) ### 2. Configurer le MCP (global)
Ajoute dans ta config MCP Claude Code (`~/.claude.json` ou settings): ```bash
claude mcp add duo-partner -s user -e BROKER_URL=http://localhost:3210 -- node "CHEMIN/mcp-claude-duo/mcp-partner/index.js"
```json
{
"mcpServers": {
"duo-master": {
"command": "node",
"args": ["C:/Users/alexi/Documents/projects/mcp-claude-duo/mcp-master/index.js"],
"env": {
"BROKER_URL": "http://localhost:3210"
}
}
}
}
``` ```
### 3. Configurer le Slave (Terminal 2) Ou par projet :
```bash
Config MCP: cd mon-projet
claude mcp add duo-partner -s project -e BROKER_URL=http://localhost:3210 -e PARTNER_NAME="Mon Nom" -- node "CHEMIN/mcp-claude-duo/mcp-partner/index.js"
```json
{
"mcpServers": {
"duo-slave": {
"command": "node",
"args": ["C:/Users/alexi/Documents/projects/mcp-claude-duo/mcp-slave/index.js"],
"env": {
"BROKER_URL": "http://localhost:3210",
"SLAVE_NAME": "Bob"
}
}
}
}
```
Config Hook (dans `.claude/settings.json` du projet ou `~/.claude/settings.json`):
```json
{
"hooks": {
"Stop": [
{
"matcher": {},
"hooks": [
{
"type": "command",
"command": "node C:/Users/alexi/Documents/projects/mcp-claude-duo/hooks/on-stop.js"
}
]
}
]
}
}
```
## Utilisation
### Terminal 2 (Slave) - Se connecter
```
Toi: "Connecte-toi en tant que partenaire"
Claude: *utilise connect()* → "Connecté, en attente de messages..."
```
### Terminal 1 (Master) - Parler
```
Toi: "Parle à mon partenaire, dis lui salut"
Claude: *utilise talk("Salut !")*
→ attend la réponse...
→ "Partenaire: Hey ! Ça va ?"
```
### Terminal 2 (Slave) - Reçoit et répond
```
Claude: "Message reçu: Salut !"
Claude: "Je lui réponds..." → écrit sa réponse
→ Le hook capture et envoie au broker
Claude: *utilise connect()* → attend le prochain message
``` ```
## Tools disponibles ## Tools disponibles
### MCP Master
| Tool | Description | | Tool | Description |
|------|-------------| |------|-------------|
| `talk(message)` | Envoie un message et attend la réponse | | `register(name?)` | S'enregistrer sur le réseau |
| `list_partners()` | Liste les partenaires connectés | | `talk(message, to?)` | Envoyer un message et attendre la réponse |
| `check_messages(wait?)` | Vérifier les messages en attente |
| `listen()` | Écouter en temps réel (long-polling) |
| `reply(message)` | Répondre au dernier message reçu |
| `list_partners()` | Lister les partenaires connectés |
| `history(partnerId, limit?)` | Historique de conversation |
### MCP Slave ## Exemples
| Tool | Description |
|------|-------------|
| `connect(name?)` | Se connecte et attend les messages |
| `disconnect()` | Se déconnecte |
## Variables d'environnement ### Conversation simple
| Variable | Description | Défaut | **Claude A :**
|----------|-------------|--------| ```
| `BROKER_URL` | URL du broker | `http://localhost:3210` | register("Alice")
| `BROKER_PORT` | Port du broker | `3210` | talk("Salut, ça va ?")
| `SLAVE_NAME` | Nom du slave | `Partner` | → attend la réponse...
→ "Bob: Oui et toi ?"
```
## Flow détaillé **Claude B :**
```
register("Bob")
listen()
→ "Alice: Salut, ça va ?"
reply("Oui et toi ?")
```
1. **Slave** appelle `connect()` → s'enregistre au broker, attend (long-polling) ### Messages bufferisés
2. **Master** appelle `talk("message")` → envoie au broker
3. **Broker** transmet au slave → `connect()` retourne le message **Claude A envoie même si B n'est pas connecté :**
4. **Slave** Claude voit le message et répond naturellement ```
5. **Hook Stop** se déclenche → lit le transcript, extrait la réponse, envoie au broker talk("Hey, t'es là ?")
6. **Broker** retourne la réponse au master → message stocké en DB, attend la réponse...
7. **Master** `talk()` retourne avec la réponse ```
8. **Slave** rappelle `connect()` pour le prochain message
**Claude B se connecte plus tard :**
```
check_messages()
→ "Alice: Hey, t'es là ?"
reply("Oui, j'arrive !")
→ Claude A reçoit la réponse
```
## API Broker
| Endpoint | Description |
|----------|-------------|
| `POST /register` | S'enregistrer |
| `POST /talk` | Envoyer et attendre réponse |
| `GET /messages/:id` | Récupérer messages non lus |
| `GET /wait/:id` | Long-polling |
| `POST /respond` | Répondre à un message |
| `GET /partners` | Lister les partenaires |
| `GET /history/:a/:b` | Historique entre deux partenaires |
| `GET /health` | Status du broker |
## Base de données
SQLite dans `data/duo.db` :
- `partners` : ID, nom, status, dernière connexion
- `messages` : contenu, expéditeur, destinataire, timestamps
## License ## License

181
broker/db.js Normal file
View File

@ -0,0 +1,181 @@
import Database from "better-sqlite3";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { mkdirSync } from "fs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const dataDir = join(__dirname, "..", "data");
// Créer le dossier data
try {
mkdirSync(dataDir, { recursive: true });
} catch {}
const dbPath = join(dataDir, "duo.db");
const db = new Database(dbPath);
// Activer les foreign keys
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
// Créer les tables
db.exec(`
-- Partenaires
CREATE TABLE IF NOT EXISTS partners (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'online'
);
-- Messages
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_id TEXT NOT NULL,
to_id TEXT NOT NULL,
content TEXT NOT NULL,
request_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
delivered_at DATETIME,
response_to INTEGER REFERENCES messages(id),
FOREIGN KEY (from_id) REFERENCES partners(id),
FOREIGN KEY (to_id) REFERENCES partners(id)
);
-- Index pour les requêtes fréquentes
CREATE INDEX IF NOT EXISTS idx_messages_to_id ON messages(to_id, delivered_at);
CREATE INDEX IF NOT EXISTS idx_messages_request_id ON messages(request_id);
`);
// Prepared statements
const stmts = {
// Partners
upsertPartner: db.prepare(`
INSERT INTO partners (id, name, last_seen, status)
VALUES (?, ?, CURRENT_TIMESTAMP, 'online')
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
last_seen = CURRENT_TIMESTAMP,
status = 'online'
`),
getPartner: db.prepare(`SELECT * FROM partners WHERE id = ?`),
getAllPartners: db.prepare(`SELECT * FROM partners ORDER BY last_seen DESC`),
updatePartnerStatus: db.prepare(`
UPDATE partners SET status = ?, last_seen = CURRENT_TIMESTAMP WHERE id = ?
`),
// Messages
insertMessage: db.prepare(`
INSERT INTO messages (from_id, to_id, content, request_id)
VALUES (?, ?, ?, ?)
`),
getUndeliveredMessages: db.prepare(`
SELECT * FROM messages
WHERE to_id = ? AND delivered_at IS NULL
ORDER BY created_at ASC
`),
markDelivered: db.prepare(`
UPDATE messages SET delivered_at = CURRENT_TIMESTAMP WHERE id = ?
`),
getMessageByRequestId: db.prepare(`
SELECT * FROM messages WHERE request_id = ?
`),
insertResponse: db.prepare(`
INSERT INTO messages (from_id, to_id, content, response_to)
VALUES (?, ?, ?, ?)
`),
getResponse: db.prepare(`
SELECT * FROM messages WHERE response_to = ? AND delivered_at IS NULL
`),
markResponseDelivered: db.prepare(`
UPDATE messages SET delivered_at = CURRENT_TIMESTAMP WHERE response_to = ?
`),
// Conversations history
getConversation: db.prepare(`
SELECT * FROM messages
WHERE (from_id = ? AND to_id = ?) OR (from_id = ? AND to_id = ?)
ORDER BY created_at DESC
LIMIT ?
`),
};
// API
export const DB = {
// Partners
registerPartner(id, name) {
stmts.upsertPartner.run(id, name);
return stmts.getPartner.get(id);
},
getPartner(id) {
return stmts.getPartner.get(id);
},
getAllPartners() {
return stmts.getAllPartners.all();
},
setPartnerOffline(id) {
stmts.updatePartnerStatus.run("offline", id);
},
setPartnerOnline(id) {
stmts.updatePartnerStatus.run("online", id);
},
// Messages
sendMessage(fromId, toId, content, requestId = null) {
const result = stmts.insertMessage.run(fromId, toId, content, requestId);
return result.lastInsertRowid;
},
getUndeliveredMessages(toId) {
return stmts.getUndeliveredMessages.all(toId);
},
markDelivered(messageId) {
stmts.markDelivered.run(messageId);
},
// Pour talk() qui attend une réponse
sendAndWaitResponse(fromId, toId, content, requestId) {
stmts.insertMessage.run(fromId, toId, content, requestId);
},
getMessageByRequestId(requestId) {
return stmts.getMessageByRequestId.get(requestId);
},
sendResponse(fromId, toId, content, originalMessageId) {
stmts.insertResponse.run(fromId, toId, content, originalMessageId);
},
getResponse(originalMessageId) {
return stmts.getResponse.get(originalMessageId);
},
markResponseDelivered(originalMessageId) {
stmts.markResponseDelivered.run(originalMessageId);
},
// History
getConversation(partnerId1, partnerId2, limit = 50) {
return stmts.getConversation.all(partnerId1, partnerId2, partnerId2, partnerId1, limit);
},
// Raw access for complex queries
raw: db,
};
export default DB;

View File

@ -1,76 +1,22 @@
import express from "express"; import express from "express";
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "fs"; import { DB } from "./db.js";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
const PORT = process.env.BROKER_PORT || 3210; const PORT = process.env.BROKER_PORT || 3210;
// Dossier pour sauvegarder les conversations // Réponses en attente (pour talk qui attend une réponse)
const __dirname = dirname(fileURLToPath(import.meta.url)); // { requestId: { resolve, fromId, toId } }
const conversationsDir = join(__dirname, "..", "conversations");
try {
mkdirSync(conversationsDir, { recursive: true });
} catch {}
// Conversations actives: { visitorId: { date, messages: [] } }
const activeConversations = new Map();
/**
* Génère l'ID de conversation basé sur les deux partners et la date
*/
function getConversationId(partnerId) {
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
return `${partnerId}_${today}`;
}
/**
* Sauvegarde un message dans la conversation
*/
function saveMessage(partnerId, from, content) {
const convId = getConversationId(partnerId);
const convFile = join(conversationsDir, `${convId}.json`);
let conversation;
if (existsSync(convFile)) {
conversation = JSON.parse(readFileSync(convFile, "utf-8"));
} else {
conversation = {
id: convId,
partnerId,
startedAt: new Date().toISOString(),
messages: [],
};
}
conversation.messages.push({
from,
content,
timestamp: new Date().toISOString(),
});
writeFileSync(convFile, JSON.stringify(conversation, null, 2));
return conversation;
}
// Slaves connectés: { id: { name, connectedAt, waitingResponse } }
const partners = new Map();
// Messages en attente pour chaque slave: { partnerId: [{ from, content, timestamp }] }
const pendingMessages = new Map();
// Réponses en attente pour le master: { requestId: { resolve, timeout } }
const pendingResponses = new Map(); const pendingResponses = new Map();
// Long-polling requests en attente: { partnerId: { res, timeout } } // Long-polling en attente (pour check_messages)
// { partnerId: { res, heartbeat } }
const waitingPartners = new Map(); const waitingPartners = new Map();
/** /**
* Slave s'enregistre * S'enregistrer
* POST /register * POST /register
* Body: { partnerId, name }
*/ */
app.post("/register", (req, res) => { app.post("/register", (req, res) => {
const { partnerId, name } = req.body; const { partnerId, name } = req.body;
@ -79,46 +25,88 @@ app.post("/register", (req, res) => {
return res.status(400).json({ error: "partnerId required" }); return res.status(400).json({ error: "partnerId required" });
} }
partners.set(partnerId, { const partner = DB.registerPartner(partnerId, name || partnerId);
name: name || partnerId, console.log(`[BROKER] Registered: ${partner.name} (${partnerId})`);
connectedAt: Date.now(),
lastSeen: Date.now(),
status: "connected",
});
pendingMessages.set(partnerId, []); res.json({ success: true, partner });
console.log(`[BROKER] Slave registered: ${name || partnerId} (${partnerId})`);
res.json({ success: true, message: "Registered" });
}); });
/** /**
* Slave attend un message (long-polling) * Envoyer un message et attendre la réponse
* POST /talk
*/
app.post("/talk", (req, res) => {
const { fromId, toId, content } = req.body;
if (!fromId || !toId || !content) {
return res.status(400).json({ error: "fromId, toId, and content required" });
}
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Enregistrer le message en DB
const messageId = DB.sendMessage(fromId, toId, content, requestId);
console.log(`[BROKER] ${fromId} -> ${toId}: "${content.substring(0, 50)}..."`);
// Notifier le destinataire s'il est en attente
notifyWaitingPartner(toId);
// Attendre la réponse (pas de timeout)
const responsePromise = new Promise((resolve) => {
pendingResponses.set(requestId, { resolve, fromId, toId, messageId });
});
responsePromise.then((response) => {
res.json(response);
});
});
/**
* Récupérer les messages non lus
* GET /messages/:partnerId
*/
app.get("/messages/:partnerId", (req, res) => {
const { partnerId } = req.params;
const messages = DB.getUndeliveredMessages(partnerId);
// Marquer comme délivrés
for (const msg of messages) {
DB.markDelivered(msg.id);
}
res.json({ messages });
});
/**
* Attendre des messages (long-polling)
* GET /wait/:partnerId * GET /wait/:partnerId
*/ */
app.get("/wait/:partnerId", (req, res) => { app.get("/wait/:partnerId", (req, res) => {
const { partnerId } = req.params; const { partnerId } = req.params;
if (!partners.has(partnerId)) { // Mettre à jour le status
return res.status(404).json({ error: "Slave not registered" }); DB.setPartnerOnline(partnerId);
}
// Check s'il y a déjà un message en attente // Check s'il y a des messages en attente
const messages = pendingMessages.get(partnerId) || []; const messages = DB.getUndeliveredMessages(partnerId);
if (messages.length > 0) { if (messages.length > 0) {
const msg = messages.shift(); // Marquer comme délivrés
return res.json({ hasMessage: true, message: msg }); for (const msg of messages) {
DB.markDelivered(msg.id);
}
return res.json({ hasMessages: true, messages });
} }
// Annuler l'ancien waiting s'il existe // Annuler l'ancien waiting s'il existe
if (waitingPartners.has(partnerId)) { if (waitingPartners.has(partnerId)) {
const old = waitingPartners.get(partnerId); const old = waitingPartners.get(partnerId);
if (old.heartbeat) clearInterval(old.heartbeat); if (old.heartbeat) clearInterval(old.heartbeat);
old.res.json({ hasMessage: false, message: null, reason: "reconnect" }); old.res.json({ hasMessages: false, messages: [], reason: "reconnect" });
} }
// Heartbeat toutes les 30 secondes pour garder la connexion vivante (bug Claude Code) // Heartbeat toutes les 30s
const heartbeat = setInterval(() => { const heartbeat = setInterval(() => {
try { try {
res.write(": heartbeat\n\n"); res.write(": heartbeat\n\n");
@ -131,181 +119,89 @@ app.get("/wait/:partnerId", (req, res) => {
res.on("close", () => { res.on("close", () => {
clearInterval(heartbeat); clearInterval(heartbeat);
waitingPartners.delete(partnerId); waitingPartners.delete(partnerId);
// Marquer le partner comme déconnecté (mais garder dans la liste pour permettre reconnexion) DB.setPartnerOffline(partnerId);
if (partners.has(partnerId)) { console.log(`[BROKER] ${partnerId} disconnected`);
const info = partners.get(partnerId);
info.lastSeen = Date.now();
info.status = "disconnected";
}
console.log(`[BROKER] Connection closed for ${partnerId}`);
}); });
waitingPartners.set(partnerId, { res, heartbeat }); waitingPartners.set(partnerId, { res, heartbeat });
// Mettre à jour le status
if (partners.has(partnerId)) {
const info = partners.get(partnerId);
info.lastSeen = Date.now();
info.status = "waiting";
}
}); });
/** /**
* Master envoie un message à un slave * Répondre à un message
* POST /send
* Body: { partnerId, content, requestId }
*/
app.post("/send", (req, res) => {
const { partnerId, content, requestId } = req.body;
if (!partnerId || !content) {
return res.status(400).json({ error: "partnerId and content required" });
}
if (!partners.has(partnerId)) {
return res.status(404).json({ error: "Slave not found" });
}
const message = {
content,
requestId,
timestamp: Date.now(),
};
console.log(`[BROKER] Master -> ${partnerId}: "${content.substring(0, 50)}..."`);
// Sauvegarder le message du master
saveMessage(partnerId, "master", content);
// Si le slave est en attente (long-polling), lui envoyer directement
if (waitingPartners.has(partnerId)) {
const { res: partnerRes, heartbeat } = waitingPartners.get(partnerId);
if (heartbeat) clearInterval(heartbeat);
waitingPartners.delete(partnerId);
partnerRes.json({ hasMessage: true, message });
} else {
// Sinon, mettre en queue
const messages = pendingMessages.get(partnerId) || [];
messages.push(message);
pendingMessages.set(partnerId, messages);
}
// Attendre la réponse du slave (pas de timeout)
const responsePromise = new Promise((resolve) => {
pendingResponses.set(requestId, { resolve, timeout: null });
});
responsePromise.then((response) => {
res.json(response);
});
});
/**
* Slave envoie sa réponse (appelé par le hook Stop)
* POST /respond * POST /respond
* Body: { partnerId, requestId, content }
*/ */
app.post("/respond", (req, res) => { app.post("/respond", (req, res) => {
const { partnerId, requestId, content } = req.body; const { fromId, toId, content, requestId } = req.body;
console.log(`[BROKER] ${partnerId} responded: "${content.substring(0, 50)}..."`); console.log(`[BROKER] ${fromId} responded to ${toId}: "${content.substring(0, 50)}..."`);
// Sauvegarder la réponse du partner
if (partnerId && content) {
saveMessage(partnerId, partnerId, content);
}
// Trouver la requête en attente
if (requestId && pendingResponses.has(requestId)) { if (requestId && pendingResponses.has(requestId)) {
const { resolve, timeout } = pendingResponses.get(requestId); const { resolve, messageId } = pendingResponses.get(requestId);
clearTimeout(timeout);
pendingResponses.delete(requestId); pendingResponses.delete(requestId);
// Enregistrer la réponse en DB
DB.sendResponse(fromId, toId, content, messageId);
resolve({ success: true, content }); resolve({ success: true, content });
} else {
// Pas de requête en attente, juste enregistrer comme message normal
DB.sendMessage(fromId, toId, content, null);
notifyWaitingPartner(toId);
} }
res.json({ success: true }); res.json({ success: true });
}); });
/** /**
* Liste les partners connectés * Liste les partenaires
* GET /partners * GET /partners
*/ */
app.get("/partners", (req, res) => { app.get("/partners", (req, res) => {
const list = []; const partners = DB.getAllPartners();
for (const [id, info] of partners) { res.json({ partners });
list.push({ id, ...info });
}
res.json({ partners: list });
}); });
/** /**
* Slave se déconnecte * Historique de conversation
* POST /disconnect * GET /history/:partner1/:partner2
*/ */
app.post("/disconnect", (req, res) => { app.get("/history/:partner1/:partner2", (req, res) => {
const { partnerId } = req.body; const { partner1, partner2 } = req.params;
const limit = parseInt(req.query.limit) || 50;
if (partners.has(partnerId)) { const messages = DB.getConversation(partner1, partner2, limit);
partners.delete(partnerId); res.json({ messages });
pendingMessages.delete(partnerId);
if (waitingPartners.has(partnerId)) {
const { res: partnerRes, timeout } = waitingPartners.get(partnerId);
if (timeout) clearTimeout(timeout);
partnerRes.json({ hasMessage: false, disconnected: true });
waitingPartners.delete(partnerId);
}
console.log(`[BROKER] Slave disconnected: ${partnerId}`);
}
res.json({ success: true });
});
/**
* Liste les conversations sauvegardées
* GET /conversations
*/
app.get("/conversations", (req, res) => {
try {
const files = readdirSync(conversationsDir).filter((f) => f.endsWith(".json"));
const conversations = files.map((f) => {
const conv = JSON.parse(readFileSync(join(conversationsDir, f), "utf-8"));
return {
id: conv.id,
partnerId: conv.partnerId,
startedAt: conv.startedAt,
messageCount: conv.messages.length,
};
});
res.json({ conversations });
} catch (error) {
res.json({ conversations: [] });
}
});
/**
* Récupère une conversation spécifique
* GET /conversations/:id
*/
app.get("/conversations/:id", (req, res) => {
const { id } = req.params;
const convFile = join(conversationsDir, `${id}.json`);
if (!existsSync(convFile)) {
return res.status(404).json({ error: "Conversation not found" });
}
const conversation = JSON.parse(readFileSync(convFile, "utf-8"));
res.json(conversation);
}); });
/** /**
* Health check * Health check
*/ */
app.get("/health", (req, res) => { app.get("/health", (req, res) => {
res.json({ status: "ok", partners: partners.size }); const partners = DB.getAllPartners();
const online = partners.filter((p) => p.status === "online").length;
res.json({ status: "ok", partners: partners.length, online });
}); });
/**
* Notifie un partenaire en attente qu'il a des messages
*/
function notifyWaitingPartner(partnerId) {
if (waitingPartners.has(partnerId)) {
const { res, heartbeat } = waitingPartners.get(partnerId);
clearInterval(heartbeat);
waitingPartners.delete(partnerId);
const messages = DB.getUndeliveredMessages(partnerId);
for (const msg of messages) {
DB.markDelivered(msg.id);
}
res.json({ hasMessages: true, messages });
}
}
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`[BROKER] Claude Duo Broker running on http://localhost:${PORT}`); console.log(`[BROKER] Claude Duo Broker v2 running on http://localhost:${PORT}`);
console.log(`[BROKER] Database: data/duo.db`);
}); });

View File

@ -1,109 +0,0 @@
#!/usr/bin/env node
/**
* Hook "Stop" pour Claude Code
* Se déclenche quand Claude finit de répondre
* Lit le transcript et envoie la dernière réponse au broker
*/
import { readFileSync, readdirSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210";
const __dirname = dirname(fileURLToPath(import.meta.url));
const stateDir = join(__dirname, "..", ".state");
async function main() {
// Lire l'input du hook depuis stdin
let input = "";
for await (const chunk of process.stdin) {
input += chunk;
}
let hookData;
try {
hookData = JSON.parse(input);
} catch {
// Pas de données valides
process.exit(0);
}
const { transcript_path } = hookData;
if (!transcript_path) {
process.exit(0);
}
// Lire le fichier state unique
const stateFile = join(stateDir, "current.json");
let state = null;
try {
const content = readFileSync(stateFile, "utf-8");
state = JSON.parse(content);
} catch {
// Pas de state, pas de partner connecté
process.exit(0);
}
if (!state || !state.currentRequestId) {
process.exit(0);
}
// Lire le transcript
let transcript;
try {
transcript = JSON.parse(readFileSync(transcript_path, "utf-8"));
} catch {
process.exit(0);
}
// Trouver la dernière réponse de l'assistant
let lastAssistantMessage = null;
// Le transcript peut avoir différents formats, essayons de trouver les messages
const messages = transcript.messages || transcript;
if (Array.isArray(messages)) {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg.role === "assistant" && msg.content) {
// Extraire le texte du contenu
if (typeof msg.content === "string") {
lastAssistantMessage = msg.content;
} else if (Array.isArray(msg.content)) {
// Chercher les blocs de texte
const textBlocks = msg.content.filter((c) => c.type === "text");
if (textBlocks.length > 0) {
lastAssistantMessage = textBlocks.map((t) => t.text).join("\n");
}
}
break;
}
}
}
if (!lastAssistantMessage) {
process.exit(0);
}
// Envoyer la réponse au broker
try {
await fetch(`${BROKER_URL}/respond`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
partnerId: state.partnerId,
requestId: state.currentRequestId,
content: lastAssistantMessage,
}),
});
} catch (error) {
console.error(`[HOOK] Error sending response: ${error.message}`);
}
process.exit(0);
}
main();

View File

@ -1,194 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210";
/**
* Appel HTTP au broker
*/
async function brokerFetch(path, options = {}) {
const url = `${BROKER_URL}${path}`;
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
return response.json();
}
// Créer le serveur MCP
const server = new Server(
{
name: "mcp-claude-duo-master",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Liste des tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "talk",
description:
"Envoie un message à ton partenaire Claude et attend sa réponse. Utilise ceci pour avoir une conversation.",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "Le message à envoyer à ton partenaire",
},
partnerId: {
type: "string",
description:
"L'ID du partenaire (optionnel si un seul partenaire connecté)",
},
},
required: ["message"],
},
},
{
name: "list_partners",
description: "Liste tous les partenaires Claude connectés et disponibles",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});
// Handler des tools
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "talk": {
try {
// Récupérer la liste des slaves si pas de partnerId spécifié
let partnerId = args.partnerId;
if (!partnerId) {
const { partners } = await brokerFetch("/partners");
if (!partners || partners.length === 0) {
return {
content: [
{
type: "text",
text: "Aucun partenaire connecté. Demande à ton partenaire de se connecter d'abord.",
},
],
};
}
partnerId = partners[0].id;
}
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const response = await brokerFetch("/send", {
method: "POST",
body: JSON.stringify({
partnerId,
content: args.message,
requestId,
}),
});
if (response.error) {
return {
content: [{ type: "text", text: `Erreur: ${response.error}` }],
isError: true,
};
}
return {
content: [
{
type: "text",
text: `**Partenaire:** ${response.content}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Erreur de communication avec le broker: ${error.message}`,
},
],
isError: true,
};
}
}
case "list_partners": {
try {
const { partners } = await brokerFetch("/partners");
if (!partners || partners.length === 0) {
return {
content: [
{
type: "text",
text: "Aucun partenaire connecté pour le moment.",
},
],
};
}
let text = "**Partenaires connectés:**\n\n";
for (const partner of partners) {
const connectedSince = Math.round(
(Date.now() - partner.connectedAt) / 1000
);
text += `- **${partner.name}** (ID: ${partner.id}) - connecté depuis ${connectedSince}s\n`;
}
return {
content: [{ type: "text", text }],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Erreur: ${error.message}. Le broker est-il lancé ?`,
},
],
isError: true,
};
}
}
default:
return {
content: [{ type: "text", text: `Tool inconnu: ${name}` }],
isError: true,
};
}
});
// Démarrer
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[MCP-MASTER] Started");
}
main().catch(console.error);

482
mcp-partner/index.js Normal file
View File

@ -0,0 +1,482 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210";
const PARTNER_NAME = process.env.PARTNER_NAME || "Claude";
// ID basé sur le dossier de travail (unique par projet)
const cwd = process.cwd();
const projectName = cwd.split(/[/\\]/).pop().toLowerCase().replace(/[^a-z0-9]/g, "_");
const myId = projectName || "partner";
let isRegistered = false;
let lastReceivedRequestId = null; // Pour savoir à quel message répondre
/**
* Appel HTTP au broker
*/
async function brokerFetch(path, options = {}, timeoutMs = 0) {
const url = `${BROKER_URL}${path}`;
const fetchOptions = {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
};
if (timeoutMs > 0) {
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs);
fetchOptions.signal = controller.signal;
}
const response = await fetch(url, fetchOptions);
return response.json();
}
/**
* S'enregistrer auprès du broker
*/
async function ensureRegistered() {
if (!isRegistered) {
await brokerFetch("/register", {
method: "POST",
body: JSON.stringify({ partnerId: myId, name: PARTNER_NAME }),
});
isRegistered = true;
console.error(`[MCP-PARTNER] Registered as ${PARTNER_NAME} (${myId})`);
}
}
// Créer le serveur MCP
const server = new Server(
{
name: "mcp-claude-duo-partner",
version: "2.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Liste des tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "register",
description:
"S'enregistre auprès du réseau de conversation. Utilise au début pour te connecter.",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Ton nom/pseudo (optionnel)",
},
},
},
},
{
name: "talk",
description:
"Envoie un message à un partenaire et attend sa réponse. Pour initier ou continuer une conversation.",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "Le message à envoyer",
},
to: {
type: "string",
description: "L'ID du destinataire (optionnel si un seul partenaire)",
},
},
required: ["message"],
},
},
{
name: "check_messages",
description:
"Vérifie s'il y a des messages en attente. Les messages sont bufferisés, donc pas besoin d'écouter en permanence.",
inputSchema: {
type: "object",
properties: {
wait: {
type: "boolean",
description: "Si true, attend qu'un message arrive (long-polling). Sinon retourne immédiatement.",
},
},
},
},
{
name: "reply",
description:
"Répond au dernier message reçu. À utiliser après check_messages quand quelqu'un attend ta réponse.",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "Ta réponse",
},
},
required: ["message"],
},
},
{
name: "listen",
description:
"Écoute en temps réel les messages entrants (long-polling). Bloque jusqu'à ce qu'un message arrive.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_partners",
description: "Liste tous les partenaires connectés au réseau.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "history",
description: "Récupère l'historique de conversation avec un partenaire.",
inputSchema: {
type: "object",
properties: {
partnerId: {
type: "string",
description: "L'ID du partenaire",
},
limit: {
type: "number",
description: "Nombre de messages max (défaut: 20)",
},
},
required: ["partnerId"],
},
},
],
};
});
// Handler des tools
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "register": {
try {
const displayName = args.name || PARTNER_NAME;
await brokerFetch("/register", {
method: "POST",
body: JSON.stringify({ partnerId: myId, name: displayName }),
});
isRegistered = true;
return {
content: [
{
type: "text",
text: `Connecté en tant que **${displayName}** (ID: ${myId})`,
},
],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
case "talk": {
try {
await ensureRegistered();
// Trouver le destinataire
let toId = args.to;
if (!toId) {
const { partners } = await brokerFetch("/partners");
const other = partners?.find((p) => p.id !== myId);
if (!other) {
return {
content: [
{
type: "text",
text: "Aucun partenaire connecté. Attends qu'un autre Claude se connecte.",
},
],
};
}
toId = other.id;
}
const response = await brokerFetch("/talk", {
method: "POST",
body: JSON.stringify({
fromId: myId,
toId,
content: args.message,
}),
});
if (response.error) {
return {
content: [{ type: "text", text: `Erreur: ${response.error}` }],
isError: true,
};
}
return {
content: [
{
type: "text",
text: `**${toId}:** ${response.content}`,
},
],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
case "check_messages": {
try {
await ensureRegistered();
let response;
if (args.wait) {
// Long-polling
response = await brokerFetch(`/wait/${myId}`);
} else {
// Récupération immédiate
response = await brokerFetch(`/messages/${myId}`);
response = { messages: response.messages, hasMessages: response.messages?.length > 0 };
}
if (!response.hasMessages || !response.messages?.length) {
return {
content: [
{
type: "text",
text: "Pas de nouveaux messages.",
},
],
};
}
// Formater les messages
let text = `**${response.messages.length} message(s) reçu(s):**\n\n`;
for (const msg of response.messages) {
text += `**${msg.from_id}:** ${msg.content}\n`;
// Garder le request_id du dernier message pour pouvoir y répondre
if (msg.request_id) {
lastReceivedRequestId = msg.request_id;
}
}
if (lastReceivedRequestId) {
text += `\n_Utilise \`reply\` pour répondre._`;
}
return {
content: [{ type: "text", text }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
case "reply": {
try {
await ensureRegistered();
if (!lastReceivedRequestId) {
return {
content: [
{
type: "text",
text: "Aucun message en attente de réponse. Utilise `check_messages` d'abord.",
},
],
};
}
// Trouver le destinataire original
const { partners } = await brokerFetch("/partners");
const other = partners?.find((p) => p.id !== myId);
const toId = other?.id || "unknown";
await brokerFetch("/respond", {
method: "POST",
body: JSON.stringify({
fromId: myId,
toId,
content: args.message,
requestId: lastReceivedRequestId,
}),
});
lastReceivedRequestId = null;
return {
content: [
{
type: "text",
text: "Réponse envoyée.",
},
],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
case "listen": {
try {
await ensureRegistered();
// Long-polling - attend qu'un message arrive
console.error("[MCP-PARTNER] Listening...");
const response = await brokerFetch(`/wait/${myId}`);
if (!response.hasMessages || !response.messages?.length) {
return {
content: [
{
type: "text",
text: "Timeout. Rappelle `listen` pour continuer à écouter.",
},
],
};
}
// Formater les messages
let text = "";
for (const msg of response.messages) {
text += `**${msg.from_id}:** ${msg.content}\n`;
if (msg.request_id) {
lastReceivedRequestId = msg.request_id;
}
}
if (lastReceivedRequestId) {
text += `\n_Utilise \`reply\` pour répondre._`;
}
return {
content: [{ type: "text", text }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
case "list_partners": {
try {
const { partners } = await brokerFetch("/partners");
if (!partners?.length) {
return {
content: [{ type: "text", text: "Aucun partenaire enregistré." }],
};
}
let text = "**Partenaires:**\n\n";
for (const p of partners) {
const status = p.status === "online" ? "🟢" : "⚫";
const isMe = p.id === myId ? " (toi)" : "";
text += `${status} **${p.name}** (${p.id})${isMe}\n`;
}
return {
content: [{ type: "text", text }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
case "history": {
try {
const limit = args.limit || 20;
const response = await brokerFetch(
`/history/${myId}/${args.partnerId}?limit=${limit}`
);
if (!response.messages?.length) {
return {
content: [
{
type: "text",
text: `Pas d'historique avec ${args.partnerId}.`,
},
],
};
}
let text = `**Historique avec ${args.partnerId}:**\n\n`;
// Inverser pour avoir l'ordre chronologique
const messages = response.messages.reverse();
for (const msg of messages) {
const date = new Date(msg.created_at).toLocaleString();
text += `[${date}] **${msg.from_id}:** ${msg.content}\n\n`;
}
return {
content: [{ type: "text", text }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
default:
return {
content: [{ type: "text", text: `Tool inconnu: ${name}` }],
isError: true,
};
}
});
// Démarrer
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`[MCP-PARTNER] Started (ID: ${myId})`);
}
main().catch(console.error);

View File

@ -1,282 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { writeFileSync, mkdirSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const BROKER_URL = process.env.BROKER_URL || "http://localhost:3210";
const PARTNER_NAME = process.env.PARTNER_NAME || process.env.SLAVE_NAME || "Partner";
// ID persistant basé sur le dossier de travail (unique par projet)
const cwd = process.cwd();
const projectName = cwd.split(/[/\\]/).pop().toLowerCase().replace(/[^a-z0-9]/g, "_");
const partnerId = projectName || PARTNER_NAME.toLowerCase().replace(/[^a-z0-9]/g, "_");
// Fichier state unique pour le hook
const __dirname = dirname(fileURLToPath(import.meta.url));
const stateDir = join(__dirname, "..", ".state");
const stateFile = join(stateDir, "current.json"); // Fichier unique, pas basé sur l'ID
// Créer le dossier state
try {
mkdirSync(stateDir, { recursive: true });
} catch {}
let isConnected = false;
let currentRequestId = null;
/**
* Appel HTTP au broker
* @param {string} path - endpoint
* @param {object} options - fetch options
* @param {number} timeoutMs - timeout en ms (défaut: 30s, 0 = pas de timeout)
*/
async function brokerFetch(path, options = {}, timeoutMs = 30000) {
const url = `${BROKER_URL}${path}`;
const fetchOptions = {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
};
// Ajouter un AbortController pour le timeout si spécifié
if (timeoutMs > 0) {
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs);
fetchOptions.signal = controller.signal;
}
const response = await fetch(url, fetchOptions);
return response.json();
}
/**
* Sauvegarde l'état pour le hook
*/
function saveState() {
const state = {
partnerId,
partnerName: PARTNER_NAME,
currentRequestId,
brokerUrl: BROKER_URL,
};
writeFileSync(stateFile, JSON.stringify(state, null, 2));
}
// Créer le serveur MCP
const server = new Server(
{
name: "mcp-claude-duo-slave",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Liste des tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "connect",
description:
"Se connecte en tant que partenaire et attend les messages. Utilise cet outil pour te connecter puis pour attendre chaque nouveau message de ton interlocuteur.",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Ton nom/pseudo pour cette conversation (optionnel)",
},
},
},
},
{
name: "respond",
description:
"Envoie ta réponse à ton interlocuteur. Utilise cet outil après avoir reçu un message pour lui répondre.",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "Ta réponse à envoyer",
},
},
required: ["message"],
},
},
{
name: "disconnect",
description: "Se déconnecte de la conversation",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});
// Handler des tools
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "connect": {
try {
// S'enregistrer si pas encore fait
if (!isConnected) {
const name = args.name || PARTNER_NAME;
await brokerFetch("/register", {
method: "POST",
body: JSON.stringify({ partnerId, name }),
});
isConnected = true;
console.error(`[MCP-SLAVE] Registered as ${name} (${partnerId})`);
}
// Attendre un message (long-polling sans timeout)
console.error("[MCP-SLAVE] Waiting for message...");
const response = await brokerFetch(`/wait/${partnerId}`, {}, 0);
if (response.disconnected) {
isConnected = false;
return {
content: [{ type: "text", text: "Déconnecté." }],
};
}
if (!response.hasMessage) {
// Timeout, pas de message - réessayer
return {
content: [
{
type: "text",
text: "Pas de nouveau message. Rappelle `connect` pour continuer à attendre.",
},
],
};
}
// Message reçu !
currentRequestId = response.message.requestId;
saveState(); // Sauvegarder pour le hook
return {
content: [
{
type: "text",
text: `**Message reçu:** ${response.message.content}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Erreur de connexion au broker: ${error.message}. Le broker est-il lancé ?`,
},
],
isError: true,
};
}
}
case "respond": {
try {
if (!currentRequestId) {
return {
content: [
{
type: "text",
text: "Aucun message en attente de réponse. Utilise `connect` d'abord pour recevoir un message.",
},
],
};
}
// Envoyer la réponse au broker
await brokerFetch("/respond", {
method: "POST",
body: JSON.stringify({
partnerId,
requestId: currentRequestId,
content: args.message,
}),
});
const oldRequestId = currentRequestId;
currentRequestId = null;
saveState();
return {
content: [
{
type: "text",
text: `Réponse envoyée. Utilise \`connect\` pour attendre le prochain message.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Erreur d'envoi: ${error.message}`,
},
],
isError: true,
};
}
}
case "disconnect": {
try {
if (isConnected) {
await brokerFetch("/disconnect", {
method: "POST",
body: JSON.stringify({ partnerId }),
});
isConnected = false;
}
return {
content: [{ type: "text", text: "Déconnecté." }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Erreur: ${error.message}` }],
isError: true,
};
}
}
default:
return {
content: [{ type: "text", text: `Tool inconnu: ${name}` }],
isError: true,
};
}
});
// Démarrer
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`[MCP-SLAVE] Started (ID: ${partnerId})`);
}
main().catch(console.error);

412
package-lock.json generated
View File

@ -1,15 +1,16 @@
{ {
"name": "mcp-claude-duo", "name": "mcp-claude-duo",
"version": "1.0.0", "version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mcp-claude-duo", "name": "mcp-claude-duo",
"version": "1.0.0", "version": "2.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0", "@modelcontextprotocol/sdk": "^1.0.0",
"better-sqlite3": "^11.0.0",
"express": "^4.18.2" "express": "^4.18.2"
} }
}, },
@ -385,6 +386,57 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.4", "version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@ -436,6 +488,30 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -474,6 +550,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -550,6 +632,30 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -569,6 +675,15 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -598,6 +713,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -664,6 +788,15 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/express": { "node_modules/express": {
"version": "4.22.1", "version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@ -748,6 +881,12 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@ -784,6 +923,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -830,6 +975,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -912,12 +1063,38 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -1029,12 +1206,45 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -1044,6 +1254,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -1119,6 +1341,32 @@
"node": ">=16.20.0" "node": ">=16.20.0"
} }
}, },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1132,6 +1380,16 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.1", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
@ -1171,6 +1429,35 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/require-from-string": { "node_modules/require-from-string": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -1255,6 +1542,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": { "node_modules/send": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
@ -1399,6 +1698,51 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@ -1408,6 +1752,52 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -1417,6 +1807,18 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -1439,6 +1841,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -1,7 +1,7 @@
{ {
"name": "mcp-claude-duo", "name": "mcp-claude-duo",
"version": "1.0.0", "version": "2.0.0",
"description": "Deux instances Claude Code qui discutent ensemble via MCP", "description": "MCP pour faire discuter plusieurs instances Claude Code ensemble",
"type": "module", "type": "module",
"scripts": { "scripts": {
"broker": "node broker/index.js", "broker": "node broker/index.js",
@ -9,6 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0", "@modelcontextprotocol/sdk": "^1.0.0",
"better-sqlite3": "^11.0.0",
"express": "^4.18.2" "express": "^4.18.2"
}, },
"keywords": ["mcp", "claude", "duo", "conversation"], "keywords": ["mcp", "claude", "duo", "conversation"],