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 +1 -1
- package/src/config.mjs +2 -0
- package/src/constants.mjs +1 -1
- package/src/services/message-responder.mjs +142 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.5.
|
|
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.
|
|
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
|
-
'
|
|
33
|
-
'
|
|
59
|
+
'deploy', 'deployment', 'ci', 'cd', 'cicd',
|
|
60
|
+
'docker', 'kubernetes', 'k8s',
|
|
34
61
|
'git', 'commit', 'merge', 'pull request', 'pr', 'branch',
|
|
35
|
-
'debug', 'debugger', 'refactor', 'typescript',
|
|
36
|
-
'
|
|
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', '
|
|
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',
|
|
62
|
-
'content', 'post', 'newsletter', '
|
|
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', '
|
|
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', '
|
|
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 (
|
|
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 =
|
|
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}`,
|