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/
|
node_modules/
|
||||||
.state/
|
.state/
|
||||||
conversations/
|
conversations/
|
||||||
|
data/
|
||||||
*.log
|
*.log
|
||||||
|
*.db
|
||||||
|
|||||||
194
README.md
194
README.md
@ -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
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 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`);
|
||||||
});
|
});
|
||||||
|
|||||||
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",
|
"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",
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user