@skvil/piertotum 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/broker.js +42 -4
- package/logo.png +0 -0
- package/mcp-server.js +37 -10
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="logo.png" width="150" alt="Skvil-Piertotum logo" />
|
|
2
|
+
<img src="https://raw.githubusercontent.com/LCGF00/skvil-piertotum/main/logo.png" width="150" alt="Skvil-Piertotum logo" />
|
|
3
3
|
|
|
4
4
|
# Skvil-Piertotum
|
|
5
5
|
|
|
@@ -238,7 +238,7 @@ Enable autonomous processing mode
|
|
|
238
238
|
|
|
239
239
|
Agents broadcast their availability via shared context under `{AGENT_ID}-status`:
|
|
240
240
|
- `idle` — ready to receive tasks
|
|
241
|
-
- `busy | task: ... |
|
|
241
|
+
- `busy | task: ... | início: HH:MM:SS` — working
|
|
242
242
|
- `offline` — gracefully shut down
|
|
243
243
|
|
|
244
244
|
> **Requirements:** The Claude Code client must support MCP Sampling. If it doesn't, the mode disables itself automatically and reports the reason in `sp_status`.
|
|
@@ -279,14 +279,14 @@ Full endpoint reference:
|
|
|
279
279
|
```
|
|
280
280
|
POST /agents/register Register an agent
|
|
281
281
|
GET /agents List agents
|
|
282
|
-
POST /agents/:
|
|
283
|
-
DELETE /agents/:
|
|
282
|
+
POST /agents/:agentId/heartbeat Heartbeat (404 if not registered)
|
|
283
|
+
DELETE /agents/:agentId Deregister agent
|
|
284
284
|
|
|
285
285
|
POST /messages/send Send to one agent
|
|
286
286
|
POST /messages/broadcast Send to all agents except sender
|
|
287
|
-
GET /messages/:
|
|
288
|
-
POST /messages/:
|
|
289
|
-
DELETE /messages/:
|
|
287
|
+
GET /messages/:agentId Read messages (?unread=true, ?limit=N)
|
|
288
|
+
POST /messages/:agentId/ack Mark message IDs as read
|
|
289
|
+
DELETE /messages/:agentId Clear all messages
|
|
290
290
|
|
|
291
291
|
POST /context Save context entry
|
|
292
292
|
GET /context List context keys
|
package/broker.js
CHANGED
|
@@ -33,7 +33,12 @@ process.on('unhandledRejection', (reason) => {
|
|
|
33
33
|
const app = express();
|
|
34
34
|
app.use(express.json({ limit: '5mb' }));
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const _rawPort = process.env.BROKER_PORT || process.argv[2] || 4800;
|
|
37
|
+
const PORT = Number(_rawPort);
|
|
38
|
+
if (!Number.isInteger(PORT) || PORT < 1 || PORT > 65535) {
|
|
39
|
+
_error(`[ERRO] Porta inválida: "${_rawPort}". Use um número entre 1 e 65535.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
// ══════════════════════════════════════════════
|
|
39
44
|
// Limites de recursos
|
|
@@ -102,7 +107,8 @@ app.use((req, res, next) => {
|
|
|
102
107
|
(req.method === 'POST' && req.path.endsWith('/heartbeat'));
|
|
103
108
|
if (!isPolling) {
|
|
104
109
|
const ts = new Date().toLocaleTimeString('pt-BR');
|
|
105
|
-
|
|
110
|
+
const safePath = req.originalUrl.replace(/[\x00-\x1f\x7f]/g, '');
|
|
111
|
+
console.log(`[${ts}] ${req.method} ${safePath}`);
|
|
106
112
|
}
|
|
107
113
|
next();
|
|
108
114
|
});
|
|
@@ -116,6 +122,9 @@ app.post('/agents/register', (req, res) => {
|
|
|
116
122
|
if (!agentId || !name) {
|
|
117
123
|
return res.status(400).json({ error: 'agentId e name são obrigatórios' });
|
|
118
124
|
}
|
|
125
|
+
if (typeof agentId !== 'string' || typeof name !== 'string') {
|
|
126
|
+
return res.status(400).json({ error: 'agentId e name devem ser strings' });
|
|
127
|
+
}
|
|
119
128
|
|
|
120
129
|
if (!agents.has(agentId) && agents.size >= MAX_AGENTS) {
|
|
121
130
|
return res.status(429).json({ error: `Limite de ${MAX_AGENTS} agentes atingido` });
|
|
@@ -190,6 +199,9 @@ app.post('/messages/send', (req, res) => {
|
|
|
190
199
|
if (!from || !to || !content) {
|
|
191
200
|
return res.status(400).json({ error: 'from, to e content são obrigatórios' });
|
|
192
201
|
}
|
|
202
|
+
if (typeof from !== 'string' || typeof to !== 'string' || typeof content !== 'string') {
|
|
203
|
+
return res.status(400).json({ error: 'from, to e content devem ser strings' });
|
|
204
|
+
}
|
|
193
205
|
|
|
194
206
|
if (!agents.has(from) && from !== 'broker') {
|
|
195
207
|
return res.status(400).json({ error: `Remetente "${from}" não registrado. Registre-se antes de enviar mensagens.` });
|
|
@@ -227,6 +239,9 @@ app.post('/messages/broadcast', (req, res) => {
|
|
|
227
239
|
if (!from || !content) {
|
|
228
240
|
return res.status(400).json({ error: 'from e content são obrigatórios' });
|
|
229
241
|
}
|
|
242
|
+
if (typeof from !== 'string' || typeof content !== 'string') {
|
|
243
|
+
return res.status(400).json({ error: 'from e content devem ser strings' });
|
|
244
|
+
}
|
|
230
245
|
|
|
231
246
|
if (!agents.has(from) && from !== 'broker') {
|
|
232
247
|
return res.status(400).json({ error: `Remetente "${from}" não registrado. Registre-se antes de enviar mensagens.` });
|
|
@@ -261,6 +276,9 @@ app.post('/messages/broadcast', (req, res) => {
|
|
|
261
276
|
// ?unread=true → apenas não lidas | ?limit=N → máximo N mensagens
|
|
262
277
|
// Não marca como lidas — use POST /messages/:agentId/ack para confirmar recebimento.
|
|
263
278
|
app.get('/messages/:agentId', (req, res) => {
|
|
279
|
+
if (!agents.has(req.params.agentId)) {
|
|
280
|
+
return res.status(404).json({ error: `Agente "${req.params.agentId}" não registrado` });
|
|
281
|
+
}
|
|
264
282
|
const queue = messages.get(req.params.agentId) || [];
|
|
265
283
|
const unreadOnly = req.query.unread === 'true';
|
|
266
284
|
const limit = req.query.limit ? Math.max(1, parseInt(req.query.limit, 10) || 50) : null;
|
|
@@ -274,6 +292,9 @@ app.get('/messages/:agentId', (req, res) => {
|
|
|
274
292
|
|
|
275
293
|
// ACK — marca mensagens específicas como lidas
|
|
276
294
|
app.post('/messages/:agentId/ack', (req, res) => {
|
|
295
|
+
if (!agents.has(req.params.agentId)) {
|
|
296
|
+
return res.status(404).json({ error: `Agente "${req.params.agentId}" não registrado` });
|
|
297
|
+
}
|
|
277
298
|
const { ids } = req.body;
|
|
278
299
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
279
300
|
return res.status(400).json({ error: 'ids deve ser um array não-vazio de message IDs' });
|
|
@@ -365,6 +386,15 @@ app.use((req, res) => {
|
|
|
365
386
|
res.status(404).json({ error: `Rota não encontrada: ${req.method} ${req.path}` });
|
|
366
387
|
});
|
|
367
388
|
|
|
389
|
+
// Error handler — retorna JSON em vez de HTML (ex: body JSON malformado)
|
|
390
|
+
app.use((err, req, res, _next) => {
|
|
391
|
+
if (err.type === 'entity.parse.failed') {
|
|
392
|
+
return res.status(400).json({ error: 'JSON inválido no body da requisição' });
|
|
393
|
+
}
|
|
394
|
+
_error(`[ERRO] ${err.stack || err.message}`);
|
|
395
|
+
res.status(err.status || 500).json({ error: 'Erro interno do servidor' });
|
|
396
|
+
});
|
|
397
|
+
|
|
368
398
|
// ══════════════════════════════════════════════
|
|
369
399
|
// Console interativo do operador
|
|
370
400
|
// ══════════════════════════════════════════════
|
|
@@ -454,8 +484,12 @@ function startConsole() {
|
|
|
454
484
|
});
|
|
455
485
|
|
|
456
486
|
rl.on('close', () => {
|
|
457
|
-
|
|
458
|
-
|
|
487
|
+
// Quando shutdown() chama rl.close(), o httpServer.close() cuida de sair.
|
|
488
|
+
// Só sai aqui se o readline fechou por conta própria (Ctrl+D).
|
|
489
|
+
if (!shuttingDown) {
|
|
490
|
+
_log('\n Broker encerrado.');
|
|
491
|
+
process.exit(0);
|
|
492
|
+
}
|
|
459
493
|
});
|
|
460
494
|
}
|
|
461
495
|
|
|
@@ -505,7 +539,11 @@ httpServer.on('error', (err) => {
|
|
|
505
539
|
// Shutdown gracioso (SIGTERM / SIGINT)
|
|
506
540
|
// ══════════════════════════════════════════════
|
|
507
541
|
|
|
542
|
+
let shuttingDown = false;
|
|
543
|
+
|
|
508
544
|
const shutdown = () => {
|
|
545
|
+
if (shuttingDown) return; // evita dupla execução (SIGTERM + SIGINT rápidos)
|
|
546
|
+
shuttingDown = true;
|
|
509
547
|
_log('\n 🛑 Broker encerrando...');
|
|
510
548
|
if (rl) rl.close();
|
|
511
549
|
httpServer.close(() => {
|
package/logo.png
ADDED
|
Binary file
|
package/mcp-server.js
CHANGED
|
@@ -19,6 +19,15 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
19
19
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
20
|
import { z } from 'zod';
|
|
21
21
|
import os from 'os';
|
|
22
|
+
import { readFileSync } from 'fs';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { dirname, join } from 'path';
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const PKG_VERSION = (() => {
|
|
28
|
+
try { return JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version; }
|
|
29
|
+
catch { return '0.0.0'; }
|
|
30
|
+
})();
|
|
22
31
|
|
|
23
32
|
// ══════════════════════════════════════════════
|
|
24
33
|
// Validação de configuração na inicialização
|
|
@@ -38,7 +47,7 @@ function validateBrokerUrl(raw) {
|
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
const BROKER_URL = validateBrokerUrl(process.env.BROKER_URL || 'http://localhost:4800');
|
|
50
|
+
const BROKER_URL = validateBrokerUrl(process.env.BROKER_URL || 'http://localhost:4800').replace(/\/+$/, '');
|
|
42
51
|
const AGENT_ID = process.env.AGENT_ID || os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
43
52
|
const AGENT_NAME = process.env.AGENT_NAME || `SP-${AGENT_ID}`;
|
|
44
53
|
const PROJECT_NAME = process.env.PROJECT_NAME || 'unknown';
|
|
@@ -99,7 +108,11 @@ async function brokerFetch(path, options = {}) {
|
|
|
99
108
|
try { body = await res.json(); } catch { body = {}; }
|
|
100
109
|
return { error: body.error || `HTTP ${res.status} ${res.statusText}` };
|
|
101
110
|
}
|
|
102
|
-
|
|
111
|
+
try {
|
|
112
|
+
return await res.json();
|
|
113
|
+
} catch {
|
|
114
|
+
return { error: `Resposta inválida do broker (não é JSON) em ${path}` };
|
|
115
|
+
}
|
|
103
116
|
} catch (err) {
|
|
104
117
|
if (err.name === 'TimeoutError') {
|
|
105
118
|
return { error: `Broker não respondeu em ${FETCH_TIMEOUT_MS / 1000}s` };
|
|
@@ -149,7 +162,7 @@ async function register() {
|
|
|
149
162
|
|
|
150
163
|
const server = new McpServer({
|
|
151
164
|
name: 'skvil-piertotum',
|
|
152
|
-
version:
|
|
165
|
+
version: PKG_VERSION,
|
|
153
166
|
description: 'Comunicação entre instâncias do Claude Code via broker central'
|
|
154
167
|
});
|
|
155
168
|
|
|
@@ -298,7 +311,10 @@ server.tool(
|
|
|
298
311
|
// Marca as mensagens lidas explicitamente (ACK)
|
|
299
312
|
const ids = result.messages.map(m => m.id).filter(Boolean);
|
|
300
313
|
if (ids.length > 0) {
|
|
301
|
-
await brokerPost(`/messages/${AGENT_ID}/ack`, { ids });
|
|
314
|
+
const ackResult = await brokerPost(`/messages/${AGENT_ID}/ack`, { ids });
|
|
315
|
+
if (ackResult.error) {
|
|
316
|
+
process.stderr.write(`⚠️ ACK falhou: ${ackResult.error}\n`);
|
|
317
|
+
}
|
|
302
318
|
}
|
|
303
319
|
|
|
304
320
|
const lines = result.messages.map(m =>
|
|
@@ -356,7 +372,7 @@ server.tool(
|
|
|
356
372
|
key: z.string().describe('Chave do contexto a ler (ex: "api-endpoints")')
|
|
357
373
|
},
|
|
358
374
|
async ({ key }) => {
|
|
359
|
-
const result = await brokerFetch(`/context/${key}`);
|
|
375
|
+
const result = await brokerFetch(`/context/${encodeURIComponent(key)}`);
|
|
360
376
|
|
|
361
377
|
if (result.error) {
|
|
362
378
|
return { content: [{ type: 'text', text: `❌ ${result.error}` }] };
|
|
@@ -583,7 +599,7 @@ async function processMessage(msg) {
|
|
|
583
599
|
|
|
584
600
|
const responseText = sampling.content.type === 'text'
|
|
585
601
|
? sampling.content.text
|
|
586
|
-
:
|
|
602
|
+
: `[resposta não-texto do tipo "${sampling.content.type}" — não suportada pelo modo autônomo]`;
|
|
587
603
|
|
|
588
604
|
// Envia resposta de volta ao remetente
|
|
589
605
|
if (canReply) {
|
|
@@ -642,11 +658,17 @@ async function pollAndProcess() {
|
|
|
642
658
|
|
|
643
659
|
// Processa uma mensagem por vez, em ordem; ACK individual após cada processamento
|
|
644
660
|
for (const msg of result.messages) {
|
|
645
|
-
|
|
646
|
-
|
|
661
|
+
try {
|
|
662
|
+
await processMessage(msg);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
// ACK mesmo em erro para evitar poison message loop (retry infinito)
|
|
665
|
+
process.stderr.write(`⚠️ Erro ao processar mensagem ${msg.id}: ${err.message}\n`);
|
|
666
|
+
}
|
|
667
|
+
// ACK sempre — inclusive se processMessage falhou (evita poison loop)
|
|
647
668
|
if (msg.id) {
|
|
648
669
|
await brokerPost(`/messages/${AGENT_ID}/ack`, { ids: [msg.id] });
|
|
649
670
|
}
|
|
671
|
+
if (!autoProcessEnabled) break; // sampling falhou — não continua o batch
|
|
650
672
|
}
|
|
651
673
|
} finally {
|
|
652
674
|
isProcessing = false;
|
|
@@ -656,8 +678,10 @@ async function pollAndProcess() {
|
|
|
656
678
|
function startAutonomousMode() {
|
|
657
679
|
if (pollTimer) return; // já rodando
|
|
658
680
|
process.stderr.write(`🤖 Modo autônomo ativado — polling a cada ${POLL_INTERVAL_MS / 1000}s\n`);
|
|
659
|
-
pollAndProcess()
|
|
660
|
-
pollTimer = setInterval(
|
|
681
|
+
pollAndProcess().catch(err => process.stderr.write(`⚠️ Erro no poll inicial: ${err.message}\n`));
|
|
682
|
+
pollTimer = setInterval(() => {
|
|
683
|
+
pollAndProcess().catch(err => process.stderr.write(`⚠️ Erro no poll: ${err.message}\n`));
|
|
684
|
+
}, POLL_INTERVAL_MS);
|
|
661
685
|
}
|
|
662
686
|
|
|
663
687
|
// ══════════════════════════════════════════════
|
|
@@ -709,7 +733,10 @@ async function main() {
|
|
|
709
733
|
}, 30000);
|
|
710
734
|
|
|
711
735
|
// Shutdown gracioso — aguarda processamento em andamento antes de sair
|
|
736
|
+
let shuttingDown = false;
|
|
712
737
|
const shutdown = async () => {
|
|
738
|
+
if (shuttingDown) return;
|
|
739
|
+
shuttingDown = true;
|
|
713
740
|
clearInterval(heartbeatTimer);
|
|
714
741
|
if (pollTimer) clearInterval(pollTimer);
|
|
715
742
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skvil/piertotum",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP + HTTP broker for multi-instance Claude Code communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/LCGF00/skvil-piertotum.git"
|
|
12
12
|
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/LCGF00/skvil-piertotum/issues"
|
|
15
|
+
},
|
|
13
16
|
"keywords": [
|
|
14
17
|
"mcp",
|
|
15
18
|
"claude",
|
|
@@ -28,7 +31,8 @@
|
|
|
28
31
|
"broker.js",
|
|
29
32
|
"mcp-server.js",
|
|
30
33
|
"README.md",
|
|
31
|
-
"LICENSE"
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"logo.png"
|
|
32
36
|
],
|
|
33
37
|
"bin": {
|
|
34
38
|
"skvil-piertotum-broker": "broker.js",
|