nothumanallowed 15.1.51 → 15.1.53

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.53",
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.53';
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,22 @@ 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';
206
- if (!enrichedPrompt.toLowerCase().includes('respond in') && !enrichedPrompt.toLowerCase().includes('rispondi in')) {
207
- enrichedPrompt += `\n\nIMPORTANT: Always respond in ${userLang}.`;
210
+ const settingLang = LANG_MAP[(config?.language || config?.lang || 'en').slice(0,2)] || 'English';
211
+ const detectedFromMsg = detectLanguage(effectiveMsg);
212
+ const userLang = detectedFromMsg || settingLang;
213
+ // Prepend the language directive at the TOP of the system prompt — many
214
+ // models give the highest weight to early instructions. Also append a
215
+ // reinforcement at the end. Belt and suspenders for language stickiness.
216
+ const langHeader = `[LANGUAGE — HIGHEST PRIORITY]\nRespond ENTIRELY in ${userLang}. From the very first word to the last word, every sentence must be in ${userLang}. Never start in English then switch — that includes intro fillers like "Let me…", "I'll…", "Sure,…". If you find yourself about to write in English, stop and write the same sentence in ${userLang} instead.\n\n`;
217
+ enrichedPrompt = langHeader + enrichedPrompt;
218
+ if (!enrichedPrompt.toLowerCase().endsWith('respond in ' + userLang.toLowerCase() + '.')) {
219
+ enrichedPrompt += `\n\nIMPORTANT: Output language is ${userLang}. Do NOT switch languages mid-response.`;
208
220
  }
209
221
 
210
222
  // Rolling context window
@@ -227,7 +239,11 @@ export function register(router) {
227
239
  for (const t of rawHistory.slice(-RECENT)) {
228
240
  parts.push(`${t.role === 'user' ? '[User]' : '[Assistant]'} ${t.content.slice(0, 2000)}`);
229
241
  }
230
- parts.push(`[User] ${effectiveMsg}`);
242
+ // Prefix the last user turn with an explicit per-message language tag.
243
+ // System prompts can lose effectiveness over long conversations; the
244
+ // per-turn tag is the closest hint to the model's first generated token
245
+ // and is the most reliable trigger for the right language.
246
+ parts.push(`[User · respond in ${userLang}] ${effectiveMsg}`);
231
247
  const userMessage = parts.join('\n\n');
232
248
 
233
249
  // Attachments — handle non-streaming
@@ -521,8 +537,8 @@ export function register(router) {
521
537
  }
522
538
  };
523
539
  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.`;
525
- 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.`;
540
+ 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.`;
541
+ const synthesisMsg = `[LANGUAGE: respond entirely in ${userLang}, every sentence] ${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. EVERY sentence must be in ${userLang}.`;
526
542
  sse('tool_synthesis', {});
527
543
  // Keep the pre-synthesis prose around. If the synthesis call returns
528
544
  // empty (provider error, content filter, model bailed), we fall back
@@ -627,8 +643,10 @@ export function register(router) {
627
643
  let enrichedPrompt = chatSystemPrompt;
628
644
  try { const ic = await getImapAccountsContext(); if (ic) enrichedPrompt += ic; } catch {}
629
645
  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}.`;
646
+ const settingLang = LANG_MAP[(config?.language || config?.lang || 'en').slice(0,2)] || 'English';
647
+ const detectedFromMsg = detectLanguage(body.message);
648
+ const userLang = detectedFromMsg || settingLang;
649
+ enrichedPrompt += `\n\nIMPORTANT: The user wrote their message in ${userLang}. Respond in ${userLang}.`;
632
650
 
633
651
  let response;
634
652
 
@@ -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';