mcp-claude-duo/hooks/on-stop.js
StillHammer b7766853cf Initial commit - MCP Claude Duo
Permet à deux instances Claude Code de discuter ensemble via MCP.

Architecture:
- broker/ : Serveur HTTP central avec heartbeat
- mcp-master/ : MCP pour envoyer des messages (talk, list_partners)
- mcp-slave/ : MCP pour recevoir et répondre (connect, respond)
- hooks/ : Hook Stop pour capturer les réponses (optionnel)

Features:
- Long-polling sans timeout
- Heartbeat toutes les 30s (fix bug Claude Code SSE)
- Sauvegarde des conversations par date
- ID basé sur le dossier de travail (unique par projet)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 03:42:25 +07:00

110 lines
2.7 KiB
JavaScript

#!/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();