nothumanallowed 15.1.50 → 15.1.52
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": "15.1.
|
|
3
|
+
"version": "15.1.52",
|
|
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/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 = '15.1.
|
|
8
|
+
export const VERSION = '15.1.52';
|
|
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,7 @@ import {
|
|
|
17
17
|
import { callLLMStream, callLLM, callLLMVision, parseAgentFile } from '../../services/llm.mjs';
|
|
18
18
|
import { buildMemoryContext } from '../../services/memory.mjs';
|
|
19
19
|
import { parseActions, executeTool, buildSystemPrompt, stripOrphanFences } from '../../services/tool-executor.mjs';
|
|
20
|
+
import { detectLanguage } from '../../services/message-responder.mjs';
|
|
20
21
|
|
|
21
22
|
// Migrate on import (once)
|
|
22
23
|
migrateOldHistory();
|
|
@@ -200,11 +201,17 @@ export function register(router) {
|
|
|
200
201
|
try { const m = buildMemoryContext('chat', effectiveMsg); if (m) enrichedPrompt = enrichedPrompt + m; } catch {}
|
|
201
202
|
try { const ic = await getImapAccountsContext(); if (ic) enrichedPrompt += ic; } catch {}
|
|
202
203
|
|
|
203
|
-
// Inject language instruction —
|
|
204
|
+
// Inject language instruction — detect from the user's message first;
|
|
205
|
+
// fall back to config.language only when detection is inconclusive (very
|
|
206
|
+
// short messages, codes, etc.). Detected language is authoritative
|
|
207
|
+
// because users routinely interact in their natural language regardless
|
|
208
|
+
// of the dropdown setting.
|
|
204
209
|
const LANG_MAP = { it:'Italian', en:'English', es:'Spanish', fr:'French', de:'German', pt:'Portuguese', nl:'Dutch', pl:'Polish', ru:'Russian', zh:'Chinese', ja:'Japanese', ko:'Korean', ar:'Arabic', hi:'Hindi', tr:'Turkish', sv:'Swedish', da:'Danish', fi:'Finnish', cs:'Czech' };
|
|
205
|
-
const
|
|
210
|
+
const settingLang = LANG_MAP[(config?.language || config?.lang || 'en').slice(0,2)] || 'English';
|
|
211
|
+
const detectedFromMsg = detectLanguage(effectiveMsg);
|
|
212
|
+
const userLang = detectedFromMsg || settingLang;
|
|
206
213
|
if (!enrichedPrompt.toLowerCase().includes('respond in') && !enrichedPrompt.toLowerCase().includes('rispondi in')) {
|
|
207
|
-
enrichedPrompt += `\n\nIMPORTANT:
|
|
214
|
+
enrichedPrompt += `\n\nIMPORTANT: The user just wrote their message in ${userLang}. Respond in ${userLang} — match the user's language exactly, do not switch to a different language mid-response.`;
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
// Rolling context window
|
|
@@ -425,6 +432,69 @@ export function register(router) {
|
|
|
425
432
|
}
|
|
426
433
|
}
|
|
427
434
|
|
|
435
|
+
// ── Auto-chain: search → read → attachment_read ──────────────────────
|
|
436
|
+
// When the user wants to read an attachment of a specific email, the
|
|
437
|
+
// model often emits the first search but then gets stuck saying "Let
|
|
438
|
+
// me read the full email" without actually emitting the follow-up
|
|
439
|
+
// tool. We chain them server-side: parse the imap_search results,
|
|
440
|
+
// pick the best subject match, fetch it with imap_read, then read
|
|
441
|
+
// the first attachment if wantsReadAttachment is on.
|
|
442
|
+
if (wantsReadAttachment) {
|
|
443
|
+
try {
|
|
444
|
+
// Collect candidate messages from all imap_search results we already
|
|
445
|
+
// executed in this turn. Format of imap_search output:
|
|
446
|
+
// [id_xxx] [UNREAD] From: name | Subject | date\n preview...
|
|
447
|
+
const candidates = new Map(); // id → { id, subject }
|
|
448
|
+
for (const tr of toolResults) {
|
|
449
|
+
if (tr.action !== 'imap_search' || typeof tr.result !== 'string') continue;
|
|
450
|
+
const lineRe = /\[([a-zA-Z0-9_-]{6,})\][^\n]*?\|\s*([^|]{2,200})\s*\|/g;
|
|
451
|
+
let m;
|
|
452
|
+
while ((m = lineRe.exec(tr.result)) !== null) {
|
|
453
|
+
if (!candidates.has(m[1])) candidates.set(m[1], { id: m[1], subject: m[2].trim() });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (candidates.size > 0) {
|
|
457
|
+
// Rank by token-overlap with the identifiers we extracted from
|
|
458
|
+
// the user message.
|
|
459
|
+
const norm = (s) => String(s || '').toLowerCase()
|
|
460
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
461
|
+
.replace(/[^a-z0-9\s./_-]/g, ' ')
|
|
462
|
+
.split(/\s+/).filter(t => t.length > 2);
|
|
463
|
+
const userTokens = norm(msg);
|
|
464
|
+
const ranked = [...candidates.values()].map(c => {
|
|
465
|
+
const subjTokens = new Set(norm(c.subject));
|
|
466
|
+
const score = userTokens.filter(t => subjTokens.has(t)).length;
|
|
467
|
+
return { ...c, score };
|
|
468
|
+
}).sort((a, b) => b.score - a.score);
|
|
469
|
+
const best = ranked[0];
|
|
470
|
+
if (best && best.score >= 1) {
|
|
471
|
+
sse('tool', { action: 'imap_read', status: 'executing' });
|
|
472
|
+
try {
|
|
473
|
+
const readResult = await executeTool('imap_read', { messageId: best.id }, config);
|
|
474
|
+
toolResults.push({ action: 'imap_read', result: readResult });
|
|
475
|
+
sse('tool', { action: 'imap_read', status: 'done', result: String(readResult).slice(0, 500) });
|
|
476
|
+
// If the read result lists at least one attachment, fetch it.
|
|
477
|
+
const attMatch = String(readResult).match(/ATTACHMENTS \(\d+\)/);
|
|
478
|
+
if (attMatch) {
|
|
479
|
+
sse('tool', { action: 'imap_attachment_read', status: 'executing' });
|
|
480
|
+
try {
|
|
481
|
+
const attResult = await executeTool('imap_attachment_read', { messageId: best.id, index: 1 }, config);
|
|
482
|
+
toolResults.push({ action: 'imap_attachment_read', result: attResult });
|
|
483
|
+
sse('tool', { action: 'imap_attachment_read', status: 'done', result: String(attResult).slice(0, 500) });
|
|
484
|
+
} catch (e) {
|
|
485
|
+
toolResults.push({ action: 'imap_attachment_read', result: `Error: ${e.message}` });
|
|
486
|
+
sse('tool', { action: 'imap_attachment_read', status: 'error', error: e.message });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch (e) {
|
|
490
|
+
toolResults.push({ action: 'imap_read', result: `Error: ${e.message}` });
|
|
491
|
+
sse('tool', { action: 'imap_read', status: 'error', error: e.message });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch { /* chain best-effort — fall through to synthesis */ }
|
|
496
|
+
}
|
|
497
|
+
|
|
428
498
|
// Capture the pre-synthesis prose (what the model said in Round 1).
|
|
429
499
|
// We will combine this with the synthesis output so the user keeps
|
|
430
500
|
// BOTH — currently the UI was overwriting Round 1 with Round 2 alone,
|
|
@@ -458,8 +528,8 @@ export function register(router) {
|
|
|
458
528
|
}
|
|
459
529
|
};
|
|
460
530
|
const toolContext = toolResults.map(t => `[${t.action} result]:\n${cleanResult(t.action, t.result)}`).join('\n\n---\n\n');
|
|
461
|
-
const synthesisPrompt = `${enrichedPrompt}\n\n## DATA FROM TOOLS:\n${toolContext}\n\n## STRICT OUTPUT RULES:\n-
|
|
462
|
-
const synthesisMsg = `${effectiveMsg}\n\
|
|
531
|
+
const synthesisPrompt = `${enrichedPrompt}\n\n## DATA FROM TOOLS (already executed — do NOT plan to call them again):\n${toolContext}\n\n## STRICT OUTPUT RULES:\n- LANGUAGE: respond ENTIRELY in ${userLang}. Do not switch languages mid-answer, do not mix English and ${userLang}.\n- The tools above HAVE ALREADY RUN. Their output is the ground truth.\n- NEVER say "Let me read…", "I'll search…", "I will fetch…", "leggerò", "cercherò", "ti dirò" — those tools are already done.\n- Answer the user's question DIRECTLY using the data above. If a specific detail (delivery date, item, total, etc.) is in the data, quote it verbatim.\n- If the data does not contain the answer, state plainly what is missing — do not announce intent to look further.\n- Write ONLY plain prose or markdown (headers, bullets, bold). NEVER use \`\`\`json or any fenced code block.\n- Format numbers/prices as plain text. Be concise and human-readable.`;
|
|
532
|
+
const synthesisMsg = `${effectiveMsg}\n\nThe tools have already been executed and their results are in the system prompt. Answer the question DIRECTLY using that data. Do NOT announce further actions ("Let me…", "I'll…", "leggerò", "cercherò"). Plain prose/markdown — zero JSON, zero code blocks.`;
|
|
463
533
|
sse('tool_synthesis', {});
|
|
464
534
|
// Keep the pre-synthesis prose around. If the synthesis call returns
|
|
465
535
|
// empty (provider error, content filter, model bailed), we fall back
|
|
@@ -564,8 +634,10 @@ export function register(router) {
|
|
|
564
634
|
let enrichedPrompt = chatSystemPrompt;
|
|
565
635
|
try { const ic = await getImapAccountsContext(); if (ic) enrichedPrompt += ic; } catch {}
|
|
566
636
|
const LANG_MAP = { it:'Italian', en:'English', es:'Spanish', fr:'French', de:'German', pt:'Portuguese', nl:'Dutch', pl:'Polish', ru:'Russian', zh:'Chinese', ja:'Japanese', ko:'Korean', ar:'Arabic', hi:'Hindi', tr:'Turkish', sv:'Swedish', da:'Danish', fi:'Finnish', cs:'Czech' };
|
|
567
|
-
const
|
|
568
|
-
|
|
637
|
+
const settingLang = LANG_MAP[(config?.language || config?.lang || 'en').slice(0,2)] || 'English';
|
|
638
|
+
const detectedFromMsg = detectLanguage(body.message);
|
|
639
|
+
const userLang = detectedFromMsg || settingLang;
|
|
640
|
+
enrichedPrompt += `\n\nIMPORTANT: The user wrote their message in ${userLang}. Respond in ${userLang}.`;
|
|
569
641
|
|
|
570
642
|
let response;
|
|
571
643
|
|
|
@@ -146,32 +146,58 @@ function routeMessage(text, useAutoRoute = true) {
|
|
|
146
146
|
|
|
147
147
|
// ── Language detection from message text ─────────────────────────────────────
|
|
148
148
|
|
|
149
|
-
const IT_WORDS = new Set(['il','lo','la','le','gli','un','una','che','di','da','in','con','su','per','tra','fra','non','ma','se','come','dove','quando','chi','cosa','ho','hai','ha','sono','sei','siamo','avere','essere','fare','dire','andare','mi','ti','ci','si','vi','li','le','gli','mio','tuo','suo','nostro','vostro','loro','questo','quello','questi','quelli','anche','già','ancora','sempre','mai','oggi','domani','ieri','adesso','ora','poi','dopo','prima','qui','qua','lì','là','più','meno','molto','poco','bene','male','sì','no','grazie','prego','ciao','buongiorno','buonasera','appuntamenti','appuntamento','calendario','riunione','meteo','temperatura','email','posta','notizie','del','dello','della','degli','delle','nel','nello','nella','negli','nelle','dal','dallo','dalla','dagli','dalle','sul','sullo','sulla','sugli','sulle','col','coi','quello','quella','quelli','quelle','cancella','cancellare','elimina','eliminare','crea','creare','sposta','spostare','aggiungi','aggiungere','modifica','modificare','ricerca','trovami','trovare','mostra','mostrami','dimmi','rispondimi','aiutami','puoi','voglio','vorrei','devo','posso','giorno','giorni','mese','mesi','anno','anni','settimana','settimane','ore','minuto','minuti','mattina','pomeriggio','sera','notte','veterinario','medico','dentista','dottore','riunioni','scadenza','scadenze'
|
|
149
|
+
const IT_WORDS = new Set(['il','lo','la','le','gli','un','una','che','di','da','in','con','su','per','tra','fra','non','ma','se','come','dove','quando','chi','cosa','ho','hai','ha','sono','sei','siamo','avere','essere','fare','dire','andare','mi','ti','ci','si','vi','li','le','gli','mio','tuo','suo','nostro','vostro','loro','questo','quello','questi','quelli','anche','già','ancora','sempre','mai','oggi','domani','ieri','adesso','ora','poi','dopo','prima','qui','qua','lì','là','più','meno','molto','poco','bene','male','sì','no','grazie','prego','ciao','buongiorno','buonasera','appuntamenti','appuntamento','calendario','riunione','meteo','temperatura','email','posta','notizie','del','dello','della','degli','delle','nel','nello','nella','negli','nelle','dal','dallo','dalla','dagli','dalle','sul','sullo','sulla','sugli','sulle','col','coi','quello','quella','quelli','quelle','cancella','cancellare','elimina','eliminare','crea','creare','sposta','spostare','aggiungi','aggiungere','modifica','modificare','ricerca','trovami','trovare','mostra','mostrami','dimmi','rispondimi','aiutami','puoi','voglio','vorrei','devo','posso','giorno','giorni','mese','mesi','anno','anni','settimana','settimane','ore','minuto','minuti','mattina','pomeriggio','sera','notte','veterinario','medico','dentista','dottore','riunioni','scadenza','scadenze',
|
|
150
|
+
// Apostrophe-truncated forms produced when we strip apostrophes during tokenization (`dell'email` → tokens `dell`, `email`).
|
|
151
|
+
'dell','all','nell','sull','dall','un','nell','quell',
|
|
152
|
+
// High-frequency content words missing from the original list — common in IMAP / calendar / order use cases.
|
|
153
|
+
'leggi','leggere','dammi','dammelo','dammela','ricevuto','ricevuta','ricevute','allegato','allegati','allegata','alle','articolo','articoli','ordine','ordini','consegna','consegne','fattura','fatture','preventivo','preventivi','offerta','offerte','richiesta','richieste','documento','documenti','prezzo','prezzi','totale','spedizione','cliente','clienti','fornitore','fornitori','data','date','codice','codici']);
|
|
150
154
|
const ES_WORDS = new Set(['el','la','los','las','un','una','que','de','en','con','por','para','pero','como','donde','cuando','quien','qué','tengo','tienes','tiene','somos','soy','eres','hacer','decir','ir','me','te','se','nos','este','ese','estos','esos','también','ya','todavía','siempre','nunca','hoy','mañana','ayer','aquí','allí','más','menos','muy','bien','mal','sí','no','gracias','hola','buenos']);
|
|
151
155
|
const FR_WORDS = new Set(['le','la','les','un','une','des','que','de','en','avec','pour','par','mais','comme','où','quand','qui','je','tu','il','elle','nous','vous','ils','elles','avoir','être','faire','dire','aller','me','te','se','ce','cet','cette','ces','aussi','déjà','toujours','jamais','aujourd','demain','hier','ici','là','plus','moins','très','bien','mal','oui','non','merci','bonjour','bonsoir']);
|
|
152
156
|
const DE_WORDS = new Set(['der','die','das','ein','eine','und','oder','aber','nicht','mit','für','von','zu','an','auf','ist','sind','hat','haben','sein','werden','ich','du','er','sie','es','wir','ihr','mich','dich','sich','uns','euch','diesem','diesen','dieser','dieses','auch','schon','noch','immer','nie','heute','morgen','gestern','hier','dort','mehr','weniger','sehr','gut','schlecht','ja','nein','danke','hallo']);
|
|
153
157
|
const PT_WORDS = new Set(['o','a','os','as','um','uma','que','de','em','com','por','para','mas','como','onde','quando','quem','eu','tu','ele','ela','nós','vós','eles','elas','ter','ser','fazer','dizer','ir','me','te','se','nos','este','esse','isso','aquele','também','já','ainda','sempre','nunca','hoje','amanhã','ontem','aqui','lá','mais','menos','muito','bem','mal','sim','não','obrigado','olá']);
|
|
154
158
|
|
|
155
|
-
function detectLanguage(text) {
|
|
159
|
+
export function detectLanguage(text) {
|
|
156
160
|
if (!text || text.length < 6) return null;
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
const raw = text.toLowerCase();
|
|
162
|
+
// Bonus signals — language-distinctive patterns that survive even when the
|
|
163
|
+
// message is dominated by codes, identifiers, or proper nouns (which is the
|
|
164
|
+
// common case for "leggi l'allegato dell'email NCSARMEMAIL.08/05"-style
|
|
165
|
+
// queries). Each pattern adds to its language's score.
|
|
166
|
+
const bonus = { it: 0, es: 0, fr: 0, de: 0, pt: 0, en: 0 };
|
|
167
|
+
// IT-distinctive: articulated prepositions with apostrophe — these forms
|
|
168
|
+
// exist in Italian but NOT in French (French uses `du / au / aux`, not `dell' / all'`).
|
|
169
|
+
if (/\b(dell'|nell'|sull'|dall'|all'|un'[aeiou]|com'è|dov'è|qual'è)\w/.test(raw)) bonus.it += 3;
|
|
170
|
+
if (/\b(c'è|c'era|d'accordo|po'\s|più\s|però\s|perché\s|cioè\s)/.test(raw)) bonus.it += 2;
|
|
171
|
+
if (/[àèéìòù]/.test(raw) && !/\bj'|qu'/.test(raw)) bonus.it += 1;
|
|
172
|
+
// FR-distinctive: pronoun apostrophes that don't exist in Italian.
|
|
173
|
+
if (/\b(j'ai|n'est|n'a\s|c'est|qu'il|qu'elle|qu'on|n'ont)/.test(raw)) bonus.fr += 4;
|
|
174
|
+
if (/\b(qu'|j'|n'|s')\w/.test(raw)) bonus.fr += 2;
|
|
175
|
+
if (/\b(ñ|ll\w+)\b/.test(raw)) bonus.es += 1;
|
|
176
|
+
if (/[äöüß]/.test(raw)) bonus.de += 2;
|
|
177
|
+
if (/\b(ção|ões|nh)\w*/.test(raw)) bonus.pt += 1;
|
|
178
|
+
|
|
179
|
+
const words = raw.replace(/[^a-zàáâãäèéêëìíîïòóôõöùúûüýñçàèìòù\s]/g, ' ').split(/\s+/).filter(w => w.length > 1);
|
|
180
|
+
if (words.length < 2 && Object.values(bonus).every(b => b === 0)) return null;
|
|
181
|
+
|
|
182
|
+
let it = bonus.it, es = bonus.es, fr = bonus.fr, de = bonus.de, pt = bonus.pt, en = bonus.en;
|
|
161
183
|
for (const w of words) {
|
|
162
184
|
if (IT_WORDS.has(w)) it++;
|
|
163
185
|
if (ES_WORDS.has(w)) es++;
|
|
164
186
|
if (FR_WORDS.has(w)) fr++;
|
|
165
187
|
if (DE_WORDS.has(w)) de++;
|
|
166
188
|
if (PT_WORDS.has(w)) pt++;
|
|
167
|
-
// Basic English common words
|
|
168
189
|
if (['the','a','an','is','are','was','were','have','has','do','does','i','you','he','she','we','they','and','or','but','not','with','for','from','to','in','on','at','this','that','these','those','can','will','would','could','should','what','where','when','who','how'].includes(w)) en++;
|
|
169
190
|
}
|
|
170
191
|
|
|
171
192
|
const max = Math.max(it, es, fr, de, pt, en);
|
|
172
193
|
if (max === 0) return null;
|
|
173
|
-
|
|
194
|
+
// Threshold lowered for short or code-heavy messages — bonus signals are
|
|
195
|
+
// strong enough on their own. Still require a clear winner: if two
|
|
196
|
+
// languages tie, return null (caller falls back to setting).
|
|
197
|
+
const threshold = Math.max(2, Math.min(words.length * 0.12, 4));
|
|
174
198
|
if (max < threshold) return null;
|
|
199
|
+
const tieCount = [it, es, fr, de, pt, en].filter(s => s === max).length;
|
|
200
|
+
if (tieCount > 1) return null;
|
|
175
201
|
|
|
176
202
|
if (it === max) return 'Italian';
|
|
177
203
|
if (es === max) return 'Spanish';
|