nothumanallowed 15.1.51 → 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.51",
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.51';
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 — always respects user's lang setting
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 userLang = LANG_MAP[(config?.language || config?.lang || 'en').slice(0,2)] || 'English';
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: Always respond in ${userLang}.`;
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
@@ -521,7 +528,7 @@ export function register(router) {
521
528
  }
522
529
  };
523
530
  const toolContext = toolResults.map(t => `[${t.action} result]:\n${cleanResult(t.action, t.result)}`).join('\n\n---\n\n');
524
- 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- 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.`;
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.`;
525
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.`;
526
533
  sse('tool_synthesis', {});
527
534
  // Keep the pre-synthesis prose around. If the synthesis call returns
@@ -627,8 +634,10 @@ export function register(router) {
627
634
  let enrichedPrompt = chatSystemPrompt;
628
635
  try { const ic = await getImapAccountsContext(); if (ic) enrichedPrompt += ic; } catch {}
629
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' };
630
- const userLang = LANG_MAP[(config?.language || config?.lang || 'en').slice(0,2)] || 'English';
631
- enrichedPrompt += `\n\nIMPORTANT: Always respond in ${userLang}.`;
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}.`;
632
641
 
633
642
  let response;
634
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 words = text.toLowerCase().replace(/[^a-zàáâãäèéêëìíîïòóôõöùúûüýñçàèìòù\s]/g, ' ').split(/\s+/).filter(w => w.length > 1);
158
- if (words.length < 2) return null;
159
-
160
- let it = 0, es = 0, fr = 0, de = 0, pt = 0, en = 0;
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
- const threshold = Math.max(2, words.length * 0.15); // at least 15% of words or 2
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';