nothumanallowed 13.5.192 → 13.5.194

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.192",
3
+ "version": "13.5.194",
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.192';
8
+ export const VERSION = '13.5.194';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -179,7 +179,7 @@ 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
- async function callAgentWithTools(config, agentName, userMessage, languageOverride) {
182
+ async function callAgentWithTools(config, agentName, userMessage, languageOverride, preHistory) {
183
183
  const today = new Date().toISOString().split('T')[0];
184
184
  const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
185
185
  const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
@@ -193,23 +193,26 @@ async function callAgentWithTools(config, agentName, userMessage, languageOverri
193
193
  .replace(/\{\{LANGUAGE\}\}/g, language);
194
194
 
195
195
  // Multi-turn: serialize history as [User]/[Assistant] string (same pattern as chat.mjs)
196
- const history = []; // [{role, content}]
196
+ // preHistory: optional [{role, content}] from previous conversation turn (for sticky confirmations)
197
+ const history = preHistory ? [...preHistory] : [];
197
198
  let finalText = '';
199
+ let lastToolResults = null; // saved for context return
198
200
 
199
- for (let round = 0; round < 3; round++) {
201
+ for (let round = 0; round < 4; round++) {
200
202
  // Build serialized message
201
203
  const parts = history.map(h => (h.role === 'user' ? '[User]' : '[Assistant]') + ' ' + h.content);
202
204
  parts.push('[User] ' + userMessage);
203
- if (round > 0) {
204
- // Replace last user with tool results continuation
205
- }
206
205
  const serialized = parts.join('\n\n');
207
206
 
208
207
  const response = await callLLM(config, systemPrompt, serialized);
209
208
  const { textParts, actions } = parseActions(response);
210
- finalText = textParts.join('\n').trim();
211
209
 
212
- if (actions.length === 0) break; // No tools — pure text response
210
+ if (actions.length === 0) {
211
+ // Final round — no tools, just text for the user
212
+ finalText = textParts.join('\n').trim();
213
+ break;
214
+ }
215
+ // Intermediate round with tools — don't expose tool-call JSON to the user
213
216
 
214
217
  // Execute all tools and collect results
215
218
  const toolResults = [];
@@ -222,13 +225,14 @@ async function callAgentWithTools(config, agentName, userMessage, languageOverri
222
225
  toolResults.push(`[${action}] Error: ${err.message}`);
223
226
  }
224
227
  }
228
+ lastToolResults = toolResults;
225
229
 
226
230
  // Feed results back: append assistant response + tool results as next user turn
227
231
  history.push({ role: 'assistant', content: response });
228
232
  userMessage = 'Tool results:\n' + toolResults.join('\n') + '\n\nNow give the user a concise confirmation in ' + language + '. Do NOT use HERALD format — respond conversationally.';
229
233
  }
230
234
 
231
- return finalText || 'Done.';
235
+ return { text: finalText || 'Done.', history, lastToolResults };
232
236
  }
233
237
 
234
238
  // ── Telegram Bot (Long Polling via native fetch) ─────────────────────────────
@@ -583,17 +587,24 @@ class TelegramResponder {
583
587
 
584
588
  let agent;
585
589
  let enrichedMessage = cleanText;
590
+ let preHistory = null;
586
591
  if (useStickyAgent) {
587
592
  agent = lastCtx.agent;
588
- // Inject previous turn context so agent understands what "Sì" refers to
589
- const nl = '\n';
590
- enrichedMessage =
591
- '[Conversazione precedente]' + nl +
592
- 'Utente: ' + lastCtx.userMsg + nl +
593
- 'Tu (' + agent.toUpperCase() + '): ' + lastCtx.agentReply.slice(0, 400) + nl + nl +
594
- '[Nuovo messaggio utente]' + nl +
595
- cleanText;
596
- this.log(`[Telegram] ${fromUser}: sticky agent ${agent.toUpperCase()} (ambiguous message, last ctx ${Math.round(stickyAge/1000)}s ago)`);
593
+ // Re-inject the full conversation history from last turn so the agent
594
+ // has the eventId/context already and can proceed directly (e.g. calendar_delete)
595
+ if (lastCtx.history && lastCtx.history.length > 0) {
596
+ preHistory = lastCtx.history;
597
+ } else {
598
+ // Fallback: text-only context injection
599
+ const nl = '\n';
600
+ enrichedMessage =
601
+ '[Conversazione precedente]' + nl +
602
+ 'Utente: ' + lastCtx.userMsg + nl +
603
+ 'Tu (' + agent.toUpperCase() + '): ' + lastCtx.agentReply.slice(0, 600) + nl + nl +
604
+ '[Nuovo messaggio utente — è una conferma/risposta al turno precedente]' + nl +
605
+ cleanText;
606
+ }
607
+ this.log(`[Telegram] ${fromUser}: sticky agent ${agent.toUpperCase()} (ambiguous, last ctx ${Math.round(stickyAge/1000)}s ago, preHistory=${preHistory ? preHistory.length : 0} turns)`);
597
608
  } else {
598
609
  agent = routeMessage(cleanText, this.autoRoute);
599
610
  this.log(`[Telegram] ${fromUser} (chat ${chatId}): routed to ${agent.toUpperCase()}${isVoice ? ' [voice]' : ''}`);
@@ -616,25 +627,30 @@ class TelegramResponder {
616
627
  // Tool-capable agents use the full tool execution loop
617
628
  // Pure reasoning/analysis agents use the simple callAgent (no tools)
618
629
  const TOOL_AGENTS = new Set(['herald', 'hermes', 'edi', 'jarvis', 'flux', 'echo', 'mercury', 'pipe', 'navi', 'link', 'prometheus', 'tempest']);
619
- let response;
630
+ let responseText;
631
+ let responseHistory = null;
620
632
  if (TOOL_AGENTS.has(agent)) {
621
- response = await callAgentWithTools(this.config, agent, enrichedMessage, detectedLang);
633
+ const result = await callAgentWithTools(this.config, agent, enrichedMessage, detectedLang, preHistory);
634
+ responseText = result.text;
635
+ responseHistory = result.history;
622
636
  } else {
623
637
  // For non-tool agents: inject language instruction into the message
624
638
  const langInstruction = detectedLang ? `[Respond in ${detectedLang}] ` : '';
625
- response = await callAgent(this.config, agent, langInstruction + enrichedMessage);
639
+ responseText = await callAgent(this.config, agent, langInstruction + enrichedMessage);
626
640
  }
627
641
 
628
642
  // Truncate if too long for Telegram (4096 char limit)
629
- const truncated = response.length > 4000
630
- ? response.slice(0, 3950) + '\n\n... [truncated]'
631
- : response;
643
+ const truncated = responseText.length > 4000
644
+ ? responseText.slice(0, 3950) + '\n\n... [truncated]'
645
+ : responseText;
632
646
 
633
647
  // Save context for sticky agent continuity (next short message reuses this agent)
648
+ // Save full history so next confirmation turn has the eventId already resolved
634
649
  this._lastContextByChatId[chatId] = {
635
650
  agent,
636
651
  userMsg: cleanText,
637
- agentReply: response,
652
+ agentReply: responseText,
653
+ history: responseHistory,
638
654
  ts: Date.now(),
639
655
  };
640
656
  this._lastAgentByChatId[chatId] = agent;
@@ -645,7 +661,7 @@ class TelegramResponder {
645
661
  text: `[${agent.toUpperCase()}]\n\n${truncated}`,
646
662
  });
647
663
 
648
- this.log(`[Telegram] Responded to ${fromUser} via ${agent.toUpperCase()} (${response.length} chars)`);
664
+ this.log(`[Telegram] Responded to ${fromUser} via ${agent.toUpperCase()} (${responseText.length} chars)`);
649
665
  } catch (err) {
650
666
  this.log(`[Telegram] Agent call failed: ${err.message}`);
651
667
  // Send error message to user
@@ -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, {