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.
|
|
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.
|
|
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 —
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
631
|
-
|
|
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
|
|
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';
|