@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 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: ... | start: HH:MM:SS` — working
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/:id/heartbeat Heartbeat (404 if not registered)
283
- DELETE /agents/:id Deregister agent
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/:id Read messages (?unread=true, ?limit=N)
288
- POST /messages/:id/ack Mark message IDs as read
289
- DELETE /messages/:id Clear all 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 PORT = process.env.BROKER_PORT || process.argv[2] || 4800;
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
- console.log(`[${ts}] ${req.method} ${req.path}`);
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
- _log('\n Broker encerrado.');
458
- process.exit(0);
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
- return await res.json();
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: '1.0.0',
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
- : JSON.stringify(sampling.content);
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
- await processMessage(msg);
646
- if (!autoProcessEnabled) break; // sampling falhou — não ACK nem continua o batch
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(); // primeiro poll imediato — não espera o intervalo completo
660
- pollTimer = setInterval(pollAndProcess, POLL_INTERVAL_MS);
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.1",
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",