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:
parent
b7766853cf
commit
0bb8af199e
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,6 @@
|
||||
node_modules/
|
||||
.state/
|
||||
conversations/
|
||||
data/
|
||||
*.log
|
||||
*.db
|
||||
|
||||
194
README.md
194
README.md
@ -1,22 +1,21 @@
|
||||
# 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 Code │ │ Claude Code │
|
||||
│ + MCP Master │ ┌───────┐ │ + MCP Slave │
|
||||
│ │ │Broker │ │ + Hook Stop │
|
||||
│ talk("yo") ────┼────────►│ HTTP │─────────►│ │
|
||||
│ │ │ │ │ reçoit "yo" │
|
||||
│ │ │ │ │ répond "salut" │
|
||||
│ reçoit "salut"◄┼─────────│ │◄─────────┼── (hook envoie) │
|
||||
└─────────────────┘ └───────┘ └─────────────────┘
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Claude A │ │ Broker │ │ Claude B │
|
||||
│ (projet-a) │◄───►│ HTTP + SQLite │◄───►│ (projet-b) │
|
||||
│ + mcp-partner │ │ │ │ + mcp-partner │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
- **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
|
||||
|
||||
```bash
|
||||
@ -26,130 +25,93 @@ npm install
|
||||
|
||||
## Démarrage
|
||||
|
||||
### 1. Lancer le broker (dans un terminal séparé)
|
||||
### 1. Lancer le broker
|
||||
|
||||
```bash
|
||||
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):
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```bash
|
||||
claude mcp add duo-partner -s user -e BROKER_URL=http://localhost:3210 -- node "CHEMIN/mcp-claude-duo/mcp-partner/index.js"
|
||||
```
|
||||
|
||||
### 3. Configurer le Slave (Terminal 2)
|
||||
|
||||
Config MCP:
|
||||
|
||||
```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
|
||||
Ou par projet :
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
## Tools disponibles
|
||||
|
||||
### MCP Master
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `talk(message)` | Envoie un message et attend la réponse |
|
||||
| `list_partners()` | Liste les partenaires connectés |
|
||||
| `register(name?)` | S'enregistrer sur le réseau |
|
||||
| `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
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `connect(name?)` | Se connecte et attend les messages |
|
||||
| `disconnect()` | Se déconnecte |
|
||||
## Exemples
|
||||
|
||||
## Variables d'environnement
|
||||
### Conversation simple
|
||||
|
||||
| Variable | Description | Défaut |
|
||||
|----------|-------------|--------|
|
||||
| `BROKER_URL` | URL du broker | `http://localhost:3210` |
|
||||
| `BROKER_PORT` | Port du broker | `3210` |
|
||||
| `SLAVE_NAME` | Nom du slave | `Partner` |
|
||||
**Claude A :**
|
||||
```
|
||||
register("Alice")
|
||||
talk("Salut, ça va ?")
|
||||
→ 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)
|
||||
2. **Master** appelle `talk("message")` → envoie au broker
|
||||
3. **Broker** transmet au slave → `connect()` retourne le message
|
||||
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
|
||||
6. **Broker** retourne la réponse au master
|
||||
7. **Master** `talk()` retourne avec la réponse
|
||||
8. **Slave** rappelle `connect()` pour le prochain message
|
||||
### Messages bufferisés
|
||||
|
||||
**Claude A envoie même si B n'est pas connecté :**
|
||||
```
|
||||
talk("Hey, t'es là ?")
|
||||
→ message stocké en DB, attend la réponse...
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
|
||||
181
broker/db.js
Normal file
181
broker/db.js
Normal 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;
|
||||
338
broker/index.js
338
broker/index.js
@ -1,76 +1,22 @@
|
||||
import express from "express";
|
||||
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { DB } from "./db.js";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const PORT = process.env.BROKER_PORT || 3210;
|
||||
|
||||
// Dossier pour sauvegarder les conversations
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
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 } }
|
||||
// Réponses en attente (pour talk qui attend une réponse)
|
||||
// { requestId: { resolve, fromId, toId } }
|
||||
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();
|
||||
|
||||
/**
|
||||
* Slave s'enregistre
|
||||
* S'enregistrer
|
||||
* POST /register
|
||||
* Body: { partnerId, name }
|
||||
*/
|
||||
app.post("/register", (req, res) => {
|
||||
const { partnerId, name } = req.body;
|
||||
@ -79,46 +25,88 @@ app.post("/register", (req, res) => {
|
||||
return res.status(400).json({ error: "partnerId required" });
|
||||
}
|
||||
|
||||
partners.set(partnerId, {
|
||||
name: name || partnerId,
|
||||
connectedAt: Date.now(),
|
||||
lastSeen: Date.now(),
|
||||
status: "connected",
|
||||
});
|
||||
const partner = DB.registerPartner(partnerId, name || partnerId);
|
||||
console.log(`[BROKER] Registered: ${partner.name} (${partnerId})`);
|
||||
|
||||
pendingMessages.set(partnerId, []);
|
||||
|
||||
console.log(`[BROKER] Slave registered: ${name || partnerId} (${partnerId})`);
|
||||
|
||||
res.json({ success: true, message: "Registered" });
|
||||
res.json({ success: true, partner });
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
app.get("/wait/:partnerId", (req, res) => {
|
||||
const { partnerId } = req.params;
|
||||
|
||||
if (!partners.has(partnerId)) {
|
||||
return res.status(404).json({ error: "Slave not registered" });
|
||||
}
|
||||
// Mettre à jour le status
|
||||
DB.setPartnerOnline(partnerId);
|
||||
|
||||
// Check s'il y a déjà un message en attente
|
||||
const messages = pendingMessages.get(partnerId) || [];
|
||||
// Check s'il y a des messages en attente
|
||||
const messages = DB.getUndeliveredMessages(partnerId);
|
||||
if (messages.length > 0) {
|
||||
const msg = messages.shift();
|
||||
return res.json({ hasMessage: true, message: msg });
|
||||
// Marquer comme délivrés
|
||||
for (const msg of messages) {
|
||||
DB.markDelivered(msg.id);
|
||||
}
|
||||
return res.json({ hasMessages: true, messages });
|
||||
}
|
||||
|
||||
// Annuler l'ancien waiting s'il existe
|
||||
if (waitingPartners.has(partnerId)) {
|
||||
const old = waitingPartners.get(partnerId);
|
||||
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(() => {
|
||||
try {
|
||||
res.write(": heartbeat\n\n");
|
||||
@ -131,181 +119,89 @@ app.get("/wait/:partnerId", (req, res) => {
|
||||
res.on("close", () => {
|
||||
clearInterval(heartbeat);
|
||||
waitingPartners.delete(partnerId);
|
||||
// Marquer le partner comme déconnecté (mais garder dans la liste pour permettre reconnexion)
|
||||
if (partners.has(partnerId)) {
|
||||
const info = partners.get(partnerId);
|
||||
info.lastSeen = Date.now();
|
||||
info.status = "disconnected";
|
||||
}
|
||||
console.log(`[BROKER] Connection closed for ${partnerId}`);
|
||||
DB.setPartnerOffline(partnerId);
|
||||
console.log(`[BROKER] ${partnerId} disconnected`);
|
||||
});
|
||||
|
||||
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
|
||||
* 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)
|
||||
* Répondre à un message
|
||||
* POST /respond
|
||||
* Body: { partnerId, requestId, content }
|
||||
*/
|
||||
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)}..."`);
|
||||
|
||||
// Sauvegarder la réponse du partner
|
||||
if (partnerId && content) {
|
||||
saveMessage(partnerId, partnerId, content);
|
||||
}
|
||||
console.log(`[BROKER] ${fromId} responded to ${toId}: "${content.substring(0, 50)}..."`);
|
||||
|
||||
// Trouver la requête en attente
|
||||
if (requestId && pendingResponses.has(requestId)) {
|
||||
const { resolve, timeout } = pendingResponses.get(requestId);
|
||||
clearTimeout(timeout);
|
||||
const { resolve, messageId } = pendingResponses.get(requestId);
|
||||
pendingResponses.delete(requestId);
|
||||
|
||||
// Enregistrer la réponse en DB
|
||||
DB.sendResponse(fromId, toId, content, messageId);
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
/**
|
||||
* Liste les partners connectés
|
||||
* Liste les partenaires
|
||||
* GET /partners
|
||||
*/
|
||||
app.get("/partners", (req, res) => {
|
||||
const list = [];
|
||||
for (const [id, info] of partners) {
|
||||
list.push({ id, ...info });
|
||||
}
|
||||
res.json({ partners: list });
|
||||
const partners = DB.getAllPartners();
|
||||
res.json({ partners });
|
||||
});
|
||||
|
||||
/**
|
||||
* Slave se déconnecte
|
||||
* POST /disconnect
|
||||
* Historique de conversation
|
||||
* GET /history/:partner1/:partner2
|
||||
*/
|
||||
app.post("/disconnect", (req, res) => {
|
||||
const { partnerId } = req.body;
|
||||
app.get("/history/:partner1/:partner2", (req, res) => {
|
||||
const { partner1, partner2 } = req.params;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
|
||||
if (partners.has(partnerId)) {
|
||||
partners.delete(partnerId);
|
||||
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);
|
||||
const messages = DB.getConversation(partner1, partner2, limit);
|
||||
res.json({ messages });
|
||||
});
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
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, () => {
|
||||
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`);
|
||||
});
|
||||
|
||||
109
hooks/on-stop.js
109
hooks/on-stop.js
@ -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();
|
||||
@ -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
482
mcp-partner/index.js
Normal 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);
|
||||
@ -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
412
package-lock.json
generated
@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "mcp-claude-duo",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mcp-claude-duo",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
},
|
||||
@ -385,6 +386,57 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"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": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
@ -436,6 +488,30 @@
|
||||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@ -474,6 +550,12 @@
|
||||
"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": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@ -550,6 +632,30 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@ -569,6 +675,15 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -598,6 +713,15 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@ -664,6 +788,15 @@
|
||||
"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": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
@ -748,6 +881,12 @@
|
||||
],
|
||||
"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": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@ -784,6 +923,12 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@ -830,6 +975,12 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -912,12 +1063,38 @@
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"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": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@ -1029,12 +1206,45 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"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": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@ -1044,6 +1254,18 @@
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@ -1119,6 +1341,32 @@
|
||||
"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": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -1132,6 +1380,16 @@
|
||||
"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": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
@ -1171,6 +1429,35 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@ -1255,6 +1542,18 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"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": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
@ -1399,6 +1698,51 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@ -1408,6 +1752,52 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@ -1417,6 +1807,18 @@
|
||||
"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": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@ -1439,6 +1841,12 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mcp-claude-duo",
|
||||
"version": "1.0.0",
|
||||
"description": "Deux instances Claude Code qui discutent ensemble via MCP",
|
||||
"version": "2.0.0",
|
||||
"description": "MCP pour faire discuter plusieurs instances Claude Code ensemble",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"broker": "node broker/index.js",
|
||||
@ -9,6 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"keywords": ["mcp", "claude", "duo", "conversation"],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user