nothumanallowed 13.5.180 → 13.5.181

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.5.180",
3
+ "version": "13.5.181",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.mjs CHANGED
@@ -228,6 +228,8 @@ export function setConfigValue(key, value) {
228
228
  'gemini-key': 'llm.geminiKey',
229
229
  'deepseek-key': 'llm.deepseekKey',
230
230
  'grok-key': 'llm.grokKey',
231
+ 'groq-key': 'llm.groqKey',
232
+ 'groqKey': 'llm.groqKey',
231
233
  'mistral-key': 'llm.mistralKey',
232
234
  'cohere-key': 'llm.cohereKey',
233
235
  'model': 'llm.model',
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '13.5.180';
8
+ export const VERSION = '13.5.181';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -17,6 +17,32 @@ import { URL } from 'url';
17
17
  // ── Agent Routing (keyword-based, zero LLM calls) ───────────────────────────
18
18
 
19
19
  const ROUTING_TABLE = [
20
+ {
21
+ // HERALD first — most common daily use case (email, calendar, weather, news)
22
+ // Italian keywords included — Telegram users speak Italian
23
+ agent: 'herald',
24
+ keywords: [
25
+ // Calendar/scheduling EN
26
+ 'schedule', 'scheduling', 'meeting', 'meetings', 'calendar',
27
+ 'appointment', 'event', 'agenda', 'reminder', 'remind',
28
+ 'reschedule', 'book', 'booking', 'slot', 'availability',
29
+ 'tomorrow', 'next week', 'today', 'this week',
30
+ // Calendar/scheduling IT
31
+ 'calendario', 'appuntamento', 'appuntamenti', 'riunione', 'riunioni',
32
+ 'promemoria', 'ricordami', 'prenotazione', 'evento', 'eventi',
33
+ 'disponibilità', 'domani', 'settimana', 'oggi', 'questa settimana',
34
+ 'prossima settimana', 'orario', 'orari', 'stamattina', 'stasera',
35
+ // Email EN+IT
36
+ 'email', 'emails', 'mail', 'inbox', 'unread', 'posta',
37
+ 'non lette', 'da leggere', 'controlla', 'controllare',
38
+ 'verifica', 'verificare', 'leggi', 'guarda',
39
+ // Weather EN+IT
40
+ 'weather', 'temperature', 'forecast',
41
+ 'meteo', 'tempo', 'temperatura', 'previsioni', 'piove', 'sole', 'pioggia',
42
+ // News/summary EN+IT
43
+ 'news', 'summary', 'briefing', 'notizie', 'riassunto', 'riepilogo',
44
+ ],
45
+ },
20
46
  {
21
47
  agent: 'saber',
22
48
  keywords: [
@@ -24,16 +50,17 @@ const ROUTING_TABLE = [
24
50
  'pentest', 'penetration', 'cve', 'owasp', 'xss', 'sql injection',
25
51
  'firewall', 'malware', 'phishing', 'ransomware', 'encryption',
26
52
  'authentication', 'auth', 'csrf', 'ssrf', 'rce', 'injection',
53
+ 'sicurezza', 'vulnerabilità', 'attacco', 'hacking',
27
54
  ],
28
55
  },
29
56
  {
30
57
  agent: 'forge',
31
58
  keywords: [
32
- 'code', 'coding', 'deploy', 'deployment', 'ci', 'cd', 'cicd',
33
- 'pipeline', 'build', 'compile', 'docker', 'kubernetes', 'k8s',
59
+ 'deploy', 'deployment', 'ci', 'cd', 'cicd',
60
+ 'docker', 'kubernetes', 'k8s',
34
61
  'git', 'commit', 'merge', 'pull request', 'pr', 'branch',
35
- 'debug', 'debugger', 'refactor', 'typescript', 'javascript',
36
- 'python', 'rust', 'golang', 'java', 'react', 'node', 'npm',
62
+ 'debug', 'debugger', 'refactor', 'typescript',
63
+ 'rust', 'golang', 'java', 'react', 'npm',
37
64
  ],
38
65
  },
39
66
  {
@@ -41,32 +68,25 @@ const ROUTING_TABLE = [
41
68
  keywords: [
42
69
  'data', 'analysis', 'analyze', 'analytics', 'stats', 'statistics',
43
70
  'metric', 'metrics', 'chart', 'graph', 'dashboard', 'report',
44
- 'trend', 'forecast', 'predict', 'prediction', 'dataset',
71
+ 'trend', 'predict', 'prediction', 'dataset',
45
72
  'database', 'query', 'sql', 'aggregate', 'visualization',
46
- ],
47
- },
48
- {
49
- agent: 'herald',
50
- keywords: [
51
- 'schedule', 'scheduling', 'meeting', 'meetings', 'calendar',
52
- 'appointment', 'event', 'agenda', 'reminder', 'remind',
53
- 'reschedule', 'cancel meeting', 'book', 'booking', 'slot',
54
- 'availability', 'free time', 'when', 'tomorrow', 'next week',
73
+ 'analisi', 'dati', 'grafico', 'statistiche',
55
74
  ],
56
75
  },
57
76
  {
58
77
  agent: 'scheherazade',
59
78
  keywords: [
60
79
  'write', 'writing', 'draft', 'blog', 'article', 'essay',
61
- 'documentation', 'docs', 'readme', 'copywriting', 'copy',
62
- 'content', 'post', 'newsletter', 'email draft', 'template',
80
+ 'documentation', 'docs', 'readme', 'copywriting',
81
+ 'content', 'post', 'newsletter', 'template',
63
82
  'summarize', 'summary', 'outline', 'creative', 'story',
83
+ 'scrivi', 'scrivere', 'bozza', 'articolo', 'testo', 'riassumi',
64
84
  ],
65
85
  },
66
86
  {
67
87
  agent: 'athena',
68
88
  keywords: [
69
- 'audit', 'review', 'compliance', 'policy', 'governance',
89
+ 'audit', 'compliance', 'policy', 'governance',
70
90
  'risk', 'assessment', 'standard', 'regulation', 'gdpr',
71
91
  'hipaa', 'soc2', 'iso', 'framework', 'benchmark',
72
92
  ],
@@ -75,7 +95,7 @@ const ROUTING_TABLE = [
75
95
  agent: 'sauron',
76
96
  keywords: [
77
97
  'monitor', 'monitoring', 'alert', 'alerting', 'uptime',
78
- 'downtime', 'health check', 'status', 'incident', 'outage',
98
+ 'downtime', 'health check', 'incident', 'outage',
79
99
  'prometheus', 'grafana', 'log', 'logs', 'logging', 'trace',
80
100
  ],
81
101
  },
@@ -251,9 +271,71 @@ class TelegramResponder {
251
271
  }
252
272
  }
253
273
 
274
+ async _transcribeVoice(fileId) {
275
+ // Download OGG voice note from Telegram and transcribe with Groq or OpenAI Whisper
276
+ // Step 1: get file path
277
+ const fileInfo = await this._telegramCall('getFile', { file_id: fileId });
278
+ const filePath = fileInfo.result?.file_path;
279
+ if (!filePath) throw new Error('Could not get file path from Telegram');
280
+
281
+ // Step 2: download OGG bytes
282
+ const token = this.token;
283
+ const audioRes = await fetch(`https://api.telegram.org/file/bot${token}/${filePath}`);
284
+ if (!audioRes.ok) throw new Error(`Download failed: ${audioRes.status}`);
285
+ const audioBuffer = Buffer.from(await audioRes.arrayBuffer());
286
+
287
+ // Step 3: transcribe — prefer Groq (free, fast), fallback to OpenAI
288
+ const groqKey = this.config.llm?.groqKey;
289
+ const openaiKey = this.config.llm?.openaiKey || (this.config.llm?.provider === 'openai' ? this.config.llm?.apiKey : null);
290
+
291
+ const boundary = '----NHAVoice' + Date.now().toString(36);
292
+ const crlf = '\r\n';
293
+ const filename = 'voice.ogg';
294
+ const header = Buffer.from(
295
+ `--${boundary}${crlf}` +
296
+ `Content-Disposition: form-data; name="file"; filename="${filename}"${crlf}` +
297
+ `Content-Type: audio/ogg${crlf}${crlf}`
298
+ );
299
+ const modelPart = Buffer.from(
300
+ `${crlf}--${boundary}${crlf}` +
301
+ `Content-Disposition: form-data; name="model"${crlf}${crlf}` +
302
+ `whisper-large-v3-turbo${crlf}--${boundary}--${crlf}`
303
+ );
304
+ const body = Buffer.concat([header, audioBuffer, modelPart]);
305
+
306
+ if (groqKey) {
307
+ const r = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', {
308
+ method: 'POST',
309
+ headers: { 'Authorization': `Bearer ${groqKey}`, 'Content-Type': `multipart/form-data; boundary=${boundary}` },
310
+ body,
311
+ });
312
+ if (!r.ok) throw new Error(`Groq Whisper ${r.status}: ${await r.text()}`);
313
+ const d = await r.json();
314
+ return d.text || '';
315
+ }
316
+
317
+ if (openaiKey) {
318
+ const modelPartOAI = Buffer.from(
319
+ `${crlf}--${boundary}${crlf}` +
320
+ `Content-Disposition: form-data; name="model"${crlf}${crlf}` +
321
+ `whisper-1${crlf}--${boundary}--${crlf}`
322
+ );
323
+ const bodyOAI = Buffer.concat([header, audioBuffer, modelPartOAI]);
324
+ const r = await fetch('https://api.openai.com/v1/audio/transcriptions', {
325
+ method: 'POST',
326
+ headers: { 'Authorization': `Bearer ${openaiKey}`, 'Content-Type': `multipart/form-data; boundary=${boundary}` },
327
+ body: bodyOAI,
328
+ });
329
+ if (!r.ok) throw new Error(`OpenAI Whisper ${r.status}: ${await r.text()}`);
330
+ const d = await r.json();
331
+ return d.text || '';
332
+ }
333
+
334
+ throw new Error('No Groq or OpenAI key for voice transcription. Add groqKey to config.');
335
+ }
336
+
254
337
  async _handleMessage(message) {
255
338
  const chatId = message.chat.id;
256
- const text = message.text;
257
339
  const fromUser = message.from?.first_name || message.from?.username || 'Unknown';
258
340
 
259
341
  // Chat ID allowlist check
@@ -261,25 +343,61 @@ class TelegramResponder {
261
343
  return;
262
344
  }
263
345
 
346
+ let rawText = message.text || '';
347
+ let isVoice = false;
348
+
349
+ // Handle voice notes — transcribe with Whisper (Groq or OpenAI)
350
+ if (message.voice || message.audio) {
351
+ const fileId = (message.voice || message.audio).file_id;
352
+ isVoice = true;
353
+ try {
354
+ await this._telegramCall('sendChatAction', { chat_id: chatId, action: 'typing' });
355
+ rawText = await this._transcribeVoice(fileId);
356
+ if (!rawText.trim()) {
357
+ await this._telegramCall('sendMessage', { chat_id: chatId, text: 'Non ho capito il vocale. Riprova.' });
358
+ return;
359
+ }
360
+ this.log(`[Telegram] Voice transcribed for ${fromUser}: "${rawText.slice(0, 80)}"`);
361
+ } catch (err) {
362
+ this.log(`[Telegram] Voice transcription failed: ${err.message}`);
363
+ await this._telegramCall('sendMessage', {
364
+ chat_id: chatId,
365
+ text: `Non riesco a trascrivere il vocale: ${err.message}\n\nAggiungi una chiave Groq (gratuita) con: nha config set groqKey gsk-...`,
366
+ });
367
+ return;
368
+ }
369
+ }
370
+
371
+ if (!rawText) return;
372
+
264
373
  // Skip bot commands that aren't directed at us
265
- if (text.startsWith('/') && !text.startsWith('/ask') && !text.startsWith('/nha')) {
374
+ if (rawText.startsWith('/') && !rawText.startsWith('/ask') && !rawText.startsWith('/nha')) {
266
375
  return;
267
376
  }
268
377
 
269
378
  // Strip /ask or /nha prefix if present
270
- const cleanText = text.replace(/^\/(ask|nha)\s*/i, '').trim();
379
+ const cleanText = rawText.replace(/^\/(ask|nha)\s*/i, '').trim();
271
380
  if (!cleanText) return;
272
381
 
382
+ // If voice: show transcription so user knows what was understood
383
+ if (isVoice) {
384
+ await this._telegramCall('sendMessage', {
385
+ chat_id: chatId,
386
+ text: `🎤 _"${cleanText}"_`,
387
+ parse_mode: 'Markdown',
388
+ }).catch(() => {});
389
+ }
390
+
273
391
  this.pendingRequests++;
274
392
  try {
275
393
  const agent = routeMessage(cleanText, this.autoRoute);
276
- this.log(`[Telegram] ${fromUser} (chat ${chatId}): routed to ${agent.toUpperCase()}`);
394
+ this.log(`[Telegram] ${fromUser} (chat ${chatId}): routed to ${agent.toUpperCase()}${isVoice ? ' [voice]' : ''}`);
277
395
 
278
396
  // Broadcast event
279
397
  this.wsBroadcast({
280
398
  type: 'responder_message',
281
399
  timestamp: new Date().toISOString(),
282
- data: { platform: 'telegram', from: fromUser, chatId, agent, text: cleanText.slice(0, 120) },
400
+ data: { platform: 'telegram', from: fromUser, chatId, agent, text: cleanText.slice(0, 120), isVoice },
283
401
  });
284
402
 
285
403
  // Send typing indicator
@@ -296,7 +414,7 @@ class TelegramResponder {
296
414
  ? response.slice(0, 3950) + '\n\n... [truncated]'
297
415
  : response;
298
416
 
299
- // Send response
417
+ // Send response as text (voice reply TTS requires separate TTS service)
300
418
  await this._telegramCall('sendMessage', {
301
419
  chat_id: chatId,
302
420
  text: `[${agent.toUpperCase()}]\n\n${truncated}`,