nothumanallowed 13.5.191 → 13.5.193

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.191",
3
+ "version": "13.5.193",
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.191';
8
+ export const VERSION = '13.5.193';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -207,9 +207,13 @@ async function callAgentWithTools(config, agentName, userMessage, languageOverri
207
207
 
208
208
  const response = await callLLM(config, systemPrompt, serialized);
209
209
  const { textParts, actions } = parseActions(response);
210
- finalText = textParts.join('\n').trim();
211
210
 
212
- if (actions.length === 0) break; // No tools — pure text response
211
+ if (actions.length === 0) {
212
+ // Final round — no tools, just text for the user
213
+ finalText = textParts.join('\n').trim();
214
+ break;
215
+ }
216
+ // Intermediate round with tools — don't expose tool-call JSON to the user
213
217
 
214
218
  // Execute all tools and collect results
215
219
  const toolResults = [];
@@ -313,6 +317,9 @@ class TelegramResponder {
313
317
  this.maxConcurrent = 3;
314
318
  this._updateCheckTimer = null;
315
319
  this._lastNotifiedVersion = null;
320
+ // Per-chat sticky agent: remembers last agent used, plus last turn context
321
+ this._lastAgentByChatId = {}; // chatId → agentName
322
+ this._lastContextByChatId = {}; // chatId → { agent, userMsg, agentReply, ts }
316
323
  }
317
324
 
318
325
  get enabled() {
@@ -359,11 +366,10 @@ class TelegramResponder {
359
366
  if (chatIds.length === 0) return;
360
367
 
361
368
  const msg =
362
- `🆕 *NHA v${latest} disponibile!*\n\n` +
369
+ `🆕 NHA v${latest} disponibile!\n\n` +
363
370
  `Una nuova versione di NotHumanAllowed è stata pubblicata.\n\n` +
364
- `Aggiorna con:\n` +
365
- `\`npm install -g nothumanallowed@latest\`\n\n` +
366
- `Poi riavvia il bot con: \`nha responder restart\``;
371
+ `Aggiorna con:\nnpm install -g nothumanallowed@latest\n\n` +
372
+ `Poi riavvia il bot con: nha ops stop && nha ops start`;
367
373
 
368
374
  this.log(`[Telegram] Broadcasting update notification v${latest} to ${chatIds.length} users`);
369
375
 
@@ -372,7 +378,6 @@ class TelegramResponder {
372
378
  await this._telegramCall('sendMessage', {
373
379
  chat_id: parseInt(chatId, 10),
374
380
  text: msg,
375
- parse_mode: 'Markdown',
376
381
  });
377
382
  } catch {
378
383
  // User blocked bot or chat no longer exists — ignore
@@ -572,8 +577,31 @@ class TelegramResponder {
572
577
 
573
578
  this.pendingRequests++;
574
579
  try {
575
- const agent = routeMessage(cleanText, this.autoRoute);
576
- this.log(`[Telegram] ${fromUser} (chat ${chatId}): routed to ${agent.toUpperCase()}${isVoice ? ' [voice]' : ''}`);
580
+ // Sticky agent: for short/ambiguous messages (< 6 words), reuse last agent for this chat
581
+ // This handles confirmation messages like "Sì", "Ok", "Fallo", "Confermo", etc.
582
+ const wordCount = cleanText.trim().split(/\s+/).length;
583
+ const isAmbiguous = wordCount <= 5;
584
+ const lastCtx = this._lastContextByChatId[chatId];
585
+ const stickyAge = lastCtx ? (Date.now() - lastCtx.ts) : Infinity;
586
+ const useStickyAgent = isAmbiguous && lastCtx && stickyAge < 5 * 60 * 1000; // sticky for 5 min
587
+
588
+ let agent;
589
+ let enrichedMessage = cleanText;
590
+ if (useStickyAgent) {
591
+ agent = lastCtx.agent;
592
+ // Inject previous turn context so agent understands what "Sì" refers to
593
+ const nl = '\n';
594
+ enrichedMessage =
595
+ '[Conversazione precedente]' + nl +
596
+ 'Utente: ' + lastCtx.userMsg + nl +
597
+ 'Tu (' + agent.toUpperCase() + '): ' + lastCtx.agentReply.slice(0, 400) + nl + nl +
598
+ '[Nuovo messaggio utente]' + nl +
599
+ cleanText;
600
+ this.log(`[Telegram] ${fromUser}: sticky agent ${agent.toUpperCase()} (ambiguous message, last ctx ${Math.round(stickyAge/1000)}s ago)`);
601
+ } else {
602
+ agent = routeMessage(cleanText, this.autoRoute);
603
+ this.log(`[Telegram] ${fromUser} (chat ${chatId}): routed to ${agent.toUpperCase()}${isVoice ? ' [voice]' : ''}`);
604
+ }
577
605
 
578
606
  // Broadcast event
579
607
  this.wsBroadcast({
@@ -586,18 +614,19 @@ class TelegramResponder {
586
614
  await this._telegramCall('sendChatAction', { chat_id: chatId, action: 'typing' });
587
615
 
588
616
  // Detect language from the message text — overrides system locale
589
- const detectedLang = detectLanguage(cleanText);
617
+ // For sticky context, use lang from previous turn if current message is ambiguous
618
+ const detectedLang = detectLanguage(cleanText) || (lastCtx ? detectLanguage(lastCtx.userMsg) : null);
590
619
 
591
620
  // Tool-capable agents use the full tool execution loop
592
621
  // Pure reasoning/analysis agents use the simple callAgent (no tools)
593
622
  const TOOL_AGENTS = new Set(['herald', 'hermes', 'edi', 'jarvis', 'flux', 'echo', 'mercury', 'pipe', 'navi', 'link', 'prometheus', 'tempest']);
594
623
  let response;
595
624
  if (TOOL_AGENTS.has(agent)) {
596
- response = await callAgentWithTools(this.config, agent, cleanText, detectedLang);
625
+ response = await callAgentWithTools(this.config, agent, enrichedMessage, detectedLang);
597
626
  } else {
598
627
  // For non-tool agents: inject language instruction into the message
599
628
  const langInstruction = detectedLang ? `[Respond in ${detectedLang}] ` : '';
600
- response = await callAgent(this.config, agent, langInstruction + cleanText);
629
+ response = await callAgent(this.config, agent, langInstruction + enrichedMessage);
601
630
  }
602
631
 
603
632
  // Truncate if too long for Telegram (4096 char limit)
@@ -605,6 +634,15 @@ class TelegramResponder {
605
634
  ? response.slice(0, 3950) + '\n\n... [truncated]'
606
635
  : response;
607
636
 
637
+ // Save context for sticky agent continuity (next short message reuses this agent)
638
+ this._lastContextByChatId[chatId] = {
639
+ agent,
640
+ userMsg: cleanText,
641
+ agentReply: response,
642
+ ts: Date.now(),
643
+ };
644
+ this._lastAgentByChatId[chatId] = agent;
645
+
608
646
  // Send response as plain text — no parse_mode to avoid Markdown entity parse errors
609
647
  await this._telegramCall('sendMessage', {
610
648
  chat_id: chatId,
@@ -21,6 +21,7 @@ import {
21
21
  getEventsForDate,
22
22
  createEvent,
23
23
  updateEvent,
24
+ deleteEvent,
24
25
  listEvents,
25
26
  markAsRead,
26
27
  markAsUnread,
@@ -74,6 +75,7 @@ export const DESTRUCTIVE_ACTIONS = new Set([
74
75
  'calendar_create',
75
76
  'calendar_move',
76
77
  'calendar_update',
78
+ 'calendar_delete',
77
79
  'contact_delete',
78
80
  'task_done',
79
81
  'task_delete',
@@ -171,7 +173,11 @@ TOOLS:
171
173
  Update ANY field of an existing calendar event: title, location, description, start time, end time.
172
174
  You MUST call calendar_find first to get the eventId. Only include fields that need to change. ALWAYS confirm before updating.
173
175
 
174
- 19. schedule_meeting(clientName: string, subject: string, location: string, durationMinutes: number, dateFrom: string, dateTo: string, workdayStart?: number, workdayEnd?: number)
176
+ 19. calendar_delete(eventId: string)
177
+ Delete (permanently remove) a calendar event by its eventId.
178
+ You MUST call calendar_find first to get the eventId. ALWAYS confirm with the user before deleting.
179
+
180
+ 20. schedule_meeting(clientName: string, subject: string, location: string, durationMinutes: number, dateFrom: string, dateTo: string, workdayStart?: number, workdayEnd?: number)
175
181
  Find optimal meeting slots considering existing calendar events, locations, and estimated travel time between appointments. Returns ranked slots with travel info. dateFrom and dateTo are YYYY-MM-DD.
176
182
 
177
183
  20. schedule_draft_email(clientName: string, subject: string, location: string, durationMinutes: number, dateFrom: string, dateTo: string)
@@ -600,7 +606,7 @@ Never output a JSON block as a suggestion — every block executes immediately.
600
606
  AVAILABLE TOOLS:
601
607
  gmail_list · gmail_read · gmail_send · gmail_draft · gmail_reply · gmail_mark_read · gmail_mark_unread · gmail_archive · gmail_delete · gmail_send_attach
602
608
  imap_accounts · imap_list · imap_read · imap_send · imap_sync · imap_labels · imap_mark_read · imap_reply · imap_thread · imap_search · imap_mark_starred · imap_trash · imap_draft · imap_send_template · imap_bulk_send
603
- calendar_today · calendar_tomorrow · calendar_date · calendar_upcoming · calendar_week · calendar_create · calendar_move · calendar_find · calendar_update · schedule_meeting · schedule_draft_email
609
+ calendar_today · calendar_tomorrow · calendar_date · calendar_upcoming · calendar_week · calendar_create · calendar_move · calendar_find · calendar_update · calendar_delete · schedule_meeting · schedule_draft_email
604
610
  task_list · task_add · task_done · task_move · task_delete · task_clear · task_edit
605
611
  contact_search · contact_add · contact_update · contact_delete
606
612
  gtask_list · gtask_add · gtask_complete
@@ -630,12 +636,19 @@ maps_directions · notify_remind · birthdays_upcoming · birthday_add · execut
630
636
  export function parseActions(text) {
631
637
  const actions = [];
632
638
  const textParts = [];
639
+
640
+ // Normalize: some LLMs output "json ... " (double-quote fences) instead of ```json ... ```
641
+ // Replace "json\n{...}\n" patterns with proper ```json fences before parsing
642
+ const normalized = text
643
+ .replace(/"json\s*\n([\s\S]*?)\n\s*"/g, (_, body) => '```json\n' + body.trim() + '\n```')
644
+ .replace(/'json\s*\n([\s\S]*?)\n\s*'/g, (_, body) => '```json\n' + body.trim() + '\n```');
645
+
633
646
  const fenceRegex = /```json\s*\n?([\s\S]*?)```/g;
634
647
  let lastIndex = 0;
635
648
  let match;
636
649
 
637
- while ((match = fenceRegex.exec(text)) !== null) {
638
- const before = text.slice(lastIndex, match.index).trim();
650
+ while ((match = fenceRegex.exec(normalized)) !== null) {
651
+ const before = normalized.slice(lastIndex, match.index).trim();
639
652
  if (before) textParts.push(before);
640
653
 
641
654
  try {
@@ -650,9 +663,30 @@ export function parseActions(text) {
650
663
  lastIndex = match.index + match[0].length;
651
664
  }
652
665
 
653
- const trailing = text.slice(lastIndex).trim();
666
+ const trailing = normalized.slice(lastIndex).trim();
654
667
  if (trailing) textParts.push(trailing);
655
668
 
669
+ // Fallback: if no fenced blocks found, scan for bare {"action": ...} objects in the text
670
+ if (actions.length === 0) {
671
+ const bareRegex = /\{[\s\S]*?"action"\s*:\s*"[^"]+[\s\S]*?\}/g;
672
+ let bareMatch;
673
+ const consumed = new Set();
674
+ while ((bareMatch = bareRegex.exec(text)) !== null) {
675
+ try {
676
+ const parsed = JSON.parse(bareMatch[0]);
677
+ if (parsed.action && typeof parsed.action === 'string' && !consumed.has(bareMatch[0])) {
678
+ actions.push({ action: parsed.action, params: parsed.params || {} });
679
+ consumed.add(bareMatch[0]);
680
+ }
681
+ } catch { /* not valid JSON, skip */ }
682
+ }
683
+ // If we found bare actions, rebuild textParts stripping out the JSON blobs
684
+ if (actions.length > 0) {
685
+ const cleaned = text.replace(/\{[\s\S]*?"action"\s*:\s*"[^"]+[\s\S]*?\}/g, '').trim();
686
+ return { textParts: cleaned ? [cleaned] : [], actions };
687
+ }
688
+ }
689
+
656
690
  return { textParts, actions };
657
691
  }
658
692
 
@@ -1318,6 +1352,25 @@ export async function executeTool(action, params, config) {
1318
1352
  return `Event updated successfully (${changes}). ${params.location ? `New location: ${params.location}` : ''}`;
1319
1353
  }
1320
1354
 
1355
+ case 'calendar_delete': {
1356
+ if (!params.eventId) return 'eventId required. Call calendar_find first to get the eventId.';
1357
+ // Smart eventId resolution: if it looks like a name instead of a Google Calendar ID, search for it
1358
+ let delEventId = params.eventId;
1359
+ if (delEventId && (delEventId.includes(' ') || delEventId.length < 10 || /[A-Z]/.test(delEventId))) {
1360
+ const fromD = new Date();
1361
+ const toD = new Date(fromD.getTime() + 60 * 86400000);
1362
+ const evts = await listEvents(config, 'primary', fromD, toD);
1363
+ const m = evts.find(e => (e.summary || '').toLowerCase().includes(delEventId.toLowerCase()));
1364
+ if (m) {
1365
+ delEventId = m.id;
1366
+ } else {
1367
+ return `Could not find event matching "${params.eventId}" in the next 60 days. Use calendar_find to search first.`;
1368
+ }
1369
+ }
1370
+ await deleteEvent(config, 'primary', delEventId);
1371
+ return `Event deleted successfully.`;
1372
+ }
1373
+
1321
1374
  // ── Smart Scheduling ──────────────────────────────────────────────────
1322
1375
  case 'schedule_meeting': {
1323
1376
  const slots = await findAvailableSlots(config, {