nothumanallowed 13.5.195 → 13.5.197

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": "13.5.195",
3
+ "version": "13.5.197",
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 = '13.5.195';
8
+ export const VERSION = '13.5.197';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -179,12 +179,55 @@ function detectLanguage(text) {
179
179
  * Like chat.mjs but headless — no confirmation prompts, all tools auto-executed.
180
180
  * Returns a human-readable summary of what was done.
181
181
  */
182
+ // Detect if a message is a reaction/continuation (not a new independent request)
183
+ // Used to decide whether to use sticky agent context
184
+ function isContinuationMessage(text, lastCtx) {
185
+ if (!lastCtx) return false;
186
+ const lower = text.toLowerCase().trim();
187
+
188
+ // Explicit confirmations / reactions
189
+ const CONFIRMATIONS = ['sì','si','yes','ok','okay','procedi','fallo','vai','confermo','cancellalo',
190
+ 'eliminalo','mandalo','esegui','perfetto','giusto','corretto','fatto','bene','certo','esatto',
191
+ 'assolutamente','ovviamente','naturalmente','ciao','avanti','go','do it','proceed','confirm',
192
+ 'sure','yep','yup','please','per favore','grazie','thanks'];
193
+ if (CONFIRMATIONS.some(c => lower === c || lower.startsWith(c + ' ') || lower.endsWith(' ' + c))) return true;
194
+
195
+ // Negative reactions that refer to previous turn (not new requests)
196
+ const REACTIONS = ['no','nope','annulla','stop','lascia perdere','non farlo','aspetta',
197
+ 'sbagliato','non è quello','con cazzo','impossibile','stai scherzando','non ci credo',
198
+ 'ma va','davvero','sicuro','sei sicuro','ma sei sicuro','ancora','di nuovo','riprova'];
199
+ if (REACTIONS.some(r => lower === r || lower.startsWith(r + ' ') || lower.endsWith(' ' + r))) return true;
200
+
201
+ // Short messages (≤ 6 words) without clear new-request keywords are likely continuations
202
+ const words = lower.split(/\s+/);
203
+ if (words.length <= 6) {
204
+ const NEW_REQUEST_KEYWORDS = ['calendario','appuntamento','email','posta','meteo','tempo',
205
+ 'crea','aggiungi','cerca','trova','mostra','mandami','dimmi','quanto','quando','dove',
206
+ 'create','add','find','search','show','send','delete','cancel','weather','mail','event'];
207
+ const hasNewKeyword = NEW_REQUEST_KEYWORDS.some(k => lower.includes(k));
208
+ if (!hasNewKeyword) return true;
209
+ }
210
+
211
+ return false;
212
+ }
213
+
214
+ // Detect if the last agent response indicates a completed action (context should reset after)
215
+ function isCompletedAction(text) {
216
+ if (!text) return false;
217
+ const lower = text.toLowerCase();
218
+ const DONE_SIGNALS = ['cancellato con successo','eliminato con successo','evento eliminato',
219
+ 'evento cancellato','deleted successfully','removed successfully','email inviata','email sent',
220
+ 'draft created','bozza creata','aggiornato con successo','updated successfully',
221
+ 'task completato','task done','creato con successo','created successfully',
222
+ 'spostato con successo','moved successfully'];
223
+ return DONE_SIGNALS.some(s => lower.includes(s));
224
+ }
225
+
182
226
  async function callAgentWithTools(config, agentName, userMessage, languageOverride, preHistory) {
183
227
  const today = new Date().toISOString().split('T')[0];
184
228
  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
185
229
  const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
186
230
  const LANG_MAP = { en: 'English', it: 'Italian', es: 'Spanish', fr: 'French', de: 'German', pt: 'Portuguese', nl: 'Dutch', pl: 'Polish', ru: 'Russian', ja: 'Japanese', ko: 'Korean', zh: 'Chinese', ar: 'Arabic', hi: 'Hindi', tr: 'Turkish' };
187
- // Priority: explicit override (from message text detection) → config setting → system locale
188
231
  const language = languageOverride || config?.profile?.language || config?.language || LANG_MAP[locale.split('-')[0]] || 'English';
189
232
 
190
233
  // Use compact Liara prompt when provider is 'nha' (same logic as buildSystemPrompt)
@@ -194,17 +237,19 @@ async function callAgentWithTools(config, agentName, userMessage, languageOverri
194
237
  .replace('{{TODAY}}', today)
195
238
  .replace('{{TIMEZONE}}', tz)
196
239
  .replace(/\{\{LANGUAGE\}\}/g, language) +
197
- // For Telegram: user confirmation already happened in the chat execute destructive actions directly
198
- '\n\nIMPORTANT: This is a Telegram bot context. The user has already confirmed destructive actions (delete, send, etc.) via chat messages. Execute them immediately without asking again — do NOT describe what you are about to do, just do it and confirm when done.';
199
-
200
- // Multi-turn: serialize history as [User]/[Assistant] string (same pattern as chat.mjs)
201
- // preHistory: optional [{role, content}] from previous conversation turn (for sticky confirmations)
240
+ // Telegram context: execute destructive actions immediately, user already confirmed via chat
241
+ '\n\nTELEGRAM BOT RULES:\n' +
242
+ '- Execute ALL actions (including delete, cancel, send) IMMEDIATELY when the user confirms. Never say "I need an ID" if you can search for the event yourself using calendar_find.\n' +
243
+ '- When you find an event with calendar_find, include its eventId in your reply so the user sees it.\n' +
244
+ '- After completing an action, confirm it simply and clearly. Do not loop back asking for more info.\n' +
245
+ '- If the user says "procedi", "sì", "fallo", "cancellalo" etc. — they are confirming. Execute the action.\n' +
246
+ '- Never ask the user to provide an eventId manually — always search with calendar_find first.';
247
+
248
+ // preHistory: full conversation history from previous turn (for sticky confirmations)
202
249
  const history = preHistory ? [...preHistory] : [];
203
250
  let finalText = '';
204
- let lastToolResults = null; // saved for context return
205
251
 
206
- for (let round = 0; round < 4; round++) {
207
- // Build serialized message
252
+ for (let round = 0; round < 5; round++) {
208
253
  const parts = history.map(h => (h.role === 'user' ? '[User]' : '[Assistant]') + ' ' + h.content);
209
254
  parts.push('[User] ' + userMessage);
210
255
  const serialized = parts.join('\n\n');
@@ -213,31 +258,53 @@ async function callAgentWithTools(config, agentName, userMessage, languageOverri
213
258
  const { textParts, actions } = parseActions(response);
214
259
 
215
260
  if (actions.length === 0) {
216
- // Final round — no tools, just text for the user
217
261
  finalText = textParts.join('\n').trim();
218
262
  break;
219
263
  }
220
- // Intermediate round with tools — don't expose tool-call JSON to the user
221
264
 
222
- // Execute all tools and collect results
265
+ // Execute all tools
223
266
  const toolResults = [];
267
+ let authError = null;
224
268
  for (const { action, params } of actions) {
225
269
  try {
226
270
  const result = await executeTool(action, params, config);
227
271
  const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
228
272
  toolResults.push(`[${action}] ${resultStr}`);
229
273
  } catch (err) {
274
+ // Detect Google/Microsoft OAuth token expiry — give user a clear fix instruction
275
+ const msg = err.message || '';
276
+ const isAuthErr = /invalid.?credentials|token.*expired|unauthorized|401|invalid_grant|auth.*failed|authentication.*failed/i.test(msg);
277
+ if (isAuthErr) {
278
+ authError = action.startsWith('gmail') || action.startsWith('imap') || action.startsWith('calendar') || action.startsWith('contact') || action.startsWith('drive') || action.startsWith('gtask')
279
+ ? 'google' : 'microsoft';
280
+ }
230
281
  toolResults.push(`[${action}] Error: ${err.message}`);
231
282
  }
232
283
  }
233
- lastToolResults = toolResults;
234
284
 
235
- // Feed results back: append assistant response + tool results as next user turn
285
+ // If auth error detected, return a user-friendly message immediately don't pass to LLM
286
+ if (authError === 'google') {
287
+ return {
288
+ text: language === 'Italian'
289
+ ? 'Il token Google è scaduto. Esegui questo comando sul tuo computer per rinnovarlo:\n\nnha google auth\n\nDopo il login si rinnova tutto automaticamente.'
290
+ : 'Your Google token has expired. Run this command on your computer to renew it:\n\nnha google auth\n\nAfter logging in everything will work again.',
291
+ history,
292
+ };
293
+ }
294
+ if (authError === 'microsoft') {
295
+ return {
296
+ text: language === 'Italian'
297
+ ? 'Il token Microsoft è scaduto. Esegui:\n\nnha microsoft auth'
298
+ : 'Your Microsoft token has expired. Run:\n\nnha microsoft auth',
299
+ history,
300
+ };
301
+ }
302
+
236
303
  history.push({ role: 'assistant', content: response });
237
- userMessage = 'Tool results:\n' + toolResults.join('\n') + '\n\nNow give the user a concise confirmation in ' + language + '. Do NOT use HERALD format respond conversationally.';
304
+ userMessage = 'Tool results:\n' + toolResults.join('\n') + '\n\nNow give the user a short, clear confirmation in ' + language + '. Be direct no preamble, no HERALD format. If an action was completed, say so clearly.';
238
305
  }
239
306
 
240
- return { text: finalText || 'Done.', history, lastToolResults };
307
+ return { text: finalText || 'Fatto.', history };
241
308
  }
242
309
 
243
310
  // ── Telegram Bot (Long Polling via native fetch) ─────────────────────────────
@@ -582,37 +649,33 @@ class TelegramResponder {
582
649
 
583
650
  this.pendingRequests++;
584
651
  try {
585
- // Sticky agent: for short/ambiguous messages (< 6 words), reuse last agent for this chat
586
- // This handles confirmation messages like "Sì", "Ok", "Fallo", "Confermo", etc.
587
- const wordCount = cleanText.trim().split(/\s+/).length;
588
- const isAmbiguous = wordCount <= 5;
589
652
  const lastCtx = this._lastContextByChatId[chatId];
590
653
  const stickyAge = lastCtx ? (Date.now() - lastCtx.ts) : Infinity;
591
- const useStickyAgent = isAmbiguous && lastCtx && stickyAge < 5 * 60 * 1000; // sticky for 5 min
654
+ const withinStickyWindow = stickyAge < 5 * 60 * 1000; // 5 min
655
+
656
+ // Determine if this message is a continuation of the previous turn
657
+ // (confirmation, reaction, short reply) vs a new independent request
658
+ const isContinuation = withinStickyWindow && isContinuationMessage(cleanText, lastCtx);
659
+
660
+ // If last response was a completed action, don't carry history forward —
661
+ // the next message is a fresh request even if it looks like a reaction
662
+ const lastWasCompleted = lastCtx && isCompletedAction(lastCtx.agentReply);
592
663
 
593
664
  let agent;
594
665
  let enrichedMessage = cleanText;
595
666
  let preHistory = null;
596
- if (useStickyAgent) {
667
+
668
+ if (isContinuation && !lastWasCompleted) {
669
+ // Continue with same agent and inject full history for context
597
670
  agent = lastCtx.agent;
598
- // Re-inject the full conversation history from last turn so the agent
599
- // has the eventId/context already and can proceed directly (e.g. calendar_delete)
600
671
  if (lastCtx.history && lastCtx.history.length > 0) {
601
672
  preHistory = lastCtx.history;
602
- } else {
603
- // Fallback: text-only context injection
604
- const nl = '\n';
605
- enrichedMessage =
606
- '[Conversazione precedente]' + nl +
607
- 'Utente: ' + lastCtx.userMsg + nl +
608
- 'Tu (' + agent.toUpperCase() + '): ' + lastCtx.agentReply.slice(0, 600) + nl + nl +
609
- '[Nuovo messaggio utente — è una conferma/risposta al turno precedente]' + nl +
610
- cleanText;
611
673
  }
612
- this.log(`[Telegram] ${fromUser}: sticky agent ${agent.toUpperCase()} (ambiguous, last ctx ${Math.round(stickyAge/1000)}s ago, preHistory=${preHistory ? preHistory.length : 0} turns)`);
674
+ this.log(`[Telegram] ${fromUser}: continuation ${agent.toUpperCase()} (ctx ${Math.round(stickyAge/1000)}s ago, history=${preHistory ? preHistory.length : 0})`);
613
675
  } else {
676
+ // Fresh request — route normally
614
677
  agent = routeMessage(cleanText, this.autoRoute);
615
- this.log(`[Telegram] ${fromUser} (chat ${chatId}): routed to ${agent.toUpperCase()}${isVoice ? ' [voice]' : ''}`);
678
+ this.log(`[Telegram] ${fromUser}: new request ${agent.toUpperCase()}${isVoice ? ' [voice]' : ''}${lastWasCompleted ? ' [prev completed]' : ''}`);
616
679
  }
617
680
 
618
681
  // Broadcast event
@@ -625,48 +688,43 @@ class TelegramResponder {
625
688
  // Send typing indicator
626
689
  await this._telegramCall('sendChatAction', { chat_id: chatId, action: 'typing' });
627
690
 
628
- // Detect language from the message text overrides system locale
629
- // For sticky context, use lang from previous turn if current message is ambiguous
691
+ // Language: detect from message, fallback to previous turn's language
630
692
  const detectedLang = detectLanguage(cleanText) || (lastCtx ? detectLanguage(lastCtx.userMsg) : null);
631
693
 
632
- // Tool-capable agents use the full tool execution loop
633
- // Pure reasoning/analysis agents use the simple callAgent (no tools)
634
694
  const TOOL_AGENTS = new Set(['herald', 'hermes', 'edi', 'jarvis', 'flux', 'echo', 'mercury', 'pipe', 'navi', 'link', 'prometheus', 'tempest']);
635
695
  let responseText;
636
696
  let responseHistory = null;
697
+
637
698
  if (TOOL_AGENTS.has(agent)) {
638
699
  const result = await callAgentWithTools(this.config, agent, enrichedMessage, detectedLang, preHistory);
639
700
  responseText = result.text;
640
701
  responseHistory = result.history;
641
702
  } else {
642
- // For non-tool agents: inject language instruction into the message
643
703
  const langInstruction = detectedLang ? `[Respond in ${detectedLang}] ` : '';
644
704
  responseText = await callAgent(this.config, agent, langInstruction + enrichedMessage);
645
705
  }
646
706
 
647
- // Truncate if too long for Telegram (4096 char limit)
707
+ // Truncate to Telegram limit (4096 chars)
648
708
  const truncated = responseText.length > 4000
649
709
  ? responseText.slice(0, 3950) + '\n\n... [truncated]'
650
710
  : responseText;
651
711
 
652
- // Save context for sticky agent continuity (next short message reuses this agent)
653
- // Save full history so next confirmation turn has the eventId already resolved
712
+ // Save context if action was completed, mark it so next turn starts fresh
654
713
  this._lastContextByChatId[chatId] = {
655
714
  agent,
656
715
  userMsg: cleanText,
657
716
  agentReply: responseText,
658
- history: responseHistory,
717
+ history: isCompletedAction(responseText) ? null : responseHistory, // clear history after success
659
718
  ts: Date.now(),
660
719
  };
661
720
  this._lastAgentByChatId[chatId] = agent;
662
721
 
663
- // Send response as plain text — no parse_mode to avoid Markdown entity parse errors
664
722
  await this._telegramCall('sendMessage', {
665
723
  chat_id: chatId,
666
724
  text: `[${agent.toUpperCase()}]\n\n${truncated}`,
667
725
  });
668
726
 
669
- this.log(`[Telegram] Responded to ${fromUser} via ${agent.toUpperCase()} (${responseText.length} chars)`);
727
+ this.log(`[Telegram] Responded to ${fromUser} via ${agent.toUpperCase()} (${responseText.length} chars)${isCompletedAction(responseText) ? ' [action completed — context reset]' : ''}`);
670
728
  } catch (err) {
671
729
  this.log(`[Telegram] Agent call failed: ${err.message}`);
672
730
  // Send error message to user