nothumanallowed 15.1.61 → 15.1.62

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.61",
3
+ "version": "15.1.62",
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.61';
8
+ export const VERSION = '15.1.62';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -1045,7 +1045,15 @@ class TelegramResponder {
1045
1045
  // appointments of May". By running the calendar tool server-side, the
1046
1046
  // user always sees REAL data — never fabricated.
1047
1047
  this._lastDirectAuditChatId = chatId;
1048
- const directFresh = await this._tryDirectFreshCalendarAction(cleanText, this.config);
1048
+ // Run the per-domain direct-action dispatcher. First match wins; falls
1049
+ // through to LLM if no handler claims the message.
1050
+ const directFresh =
1051
+ await this._tryDirectFreshCalendarAction(cleanText, this.config) ||
1052
+ await this._tryDirectFreshEmailAction(cleanText, this.config) ||
1053
+ await this._tryDirectFreshTaskAction(cleanText, this.config) ||
1054
+ await this._tryDirectFreshNoteAction(cleanText, this.config) ||
1055
+ await this._tryDirectFreshReminderAction(cleanText, this.config) ||
1056
+ await this._tryDirectFreshSlackAction(cleanText, this.config);
1049
1057
  if (directFresh) {
1050
1058
  this.log(`[Telegram] ${fromUser}: direct-fresh ${directFresh.action} → ${directFresh.success ? 'OK' : 'FAIL'}`);
1051
1059
  const personaName = this.config.responder?.telegram?.botName || this.config.responder?.botName || '';
@@ -1407,6 +1415,254 @@ class TelegramResponder {
1407
1415
  return `${parseInt(m[3], 10)} ${months[parseInt(m[2], 10) - 1]} ${m[1]}`;
1408
1416
  }
1409
1417
 
1418
+ /** Add N minutes to an ISO datetime string. Returns ISO. */
1419
+ _addMinutesIso(isoStart, minutes) {
1420
+ try {
1421
+ const d = new Date(isoStart);
1422
+ d.setMinutes(d.getMinutes() + minutes);
1423
+ return d.toISOString();
1424
+ } catch { return isoStart; }
1425
+ }
1426
+
1427
+ /**
1428
+ * NLU only — extract structured calendar-create params from a natural
1429
+ * language message using a tiny LLM call. We pin output to strict JSON and
1430
+ * accept the result only if the title + start datetime are present.
1431
+ * The LLM NEVER executes anything — it only parses.
1432
+ */
1433
+ async _nluExtractCalendarCreate(userMessage, config) {
1434
+ const todayIso = new Date().toISOString().slice(0, 10);
1435
+ const sysPrompt =
1436
+ `You extract calendar event parameters from natural language.\n` +
1437
+ `Today is ${todayIso}. Output ONLY a JSON object with these keys:\n` +
1438
+ ` - title (string, the event subject, NOT including verbs like "fissa"/"crea")\n` +
1439
+ ` - start (string, ISO datetime "YYYY-MM-DDTHH:MM:00" in local time)\n` +
1440
+ ` - end (string, ISO datetime same format; if duration unknown, leave null and default 60 min will be applied)\n` +
1441
+ ` - description (string, optional notes)\n` +
1442
+ `Rules: if a field is missing, use null. If the user says "domani" → ${this._addDaysIso(todayIso, 1).slice(0, 10)}. ` +
1443
+ `If "lunedì/martedì/..." resolve to the next occurrence. If no time is given default to 09:00. ` +
1444
+ `Output ONLY the JSON, no prose.`;
1445
+ try {
1446
+ const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 200 });
1447
+ const json = this._extractJsonObject(raw);
1448
+ if (!json) return null;
1449
+ if (!json.title || !json.start) return null;
1450
+ // Sanity: start must look like an ISO datetime.
1451
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(json.start)) return null;
1452
+ return json;
1453
+ } catch { return null; }
1454
+ }
1455
+
1456
+ async _nluExtractCalendarMove(userMessage, config) {
1457
+ const todayIso = new Date().toISOString().slice(0, 10);
1458
+ const sysPrompt =
1459
+ `You extract reschedule-event parameters from natural language.\n` +
1460
+ `Today is ${todayIso}. Output ONLY a JSON object:\n` +
1461
+ ` - title (string, the event to find and move)\n` +
1462
+ ` - oldDate (string, original date if mentioned, format "YYYY-MM-DD"; else null)\n` +
1463
+ ` - newStart (string, NEW datetime "YYYY-MM-DDTHH:MM:00")\n` +
1464
+ ` - newEnd (string, optional, same format)\n` +
1465
+ `If "domani" → ${this._addDaysIso(todayIso, 1).slice(0, 10)}. Output JSON only.`;
1466
+ try {
1467
+ const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 200 });
1468
+ const json = this._extractJsonObject(raw);
1469
+ if (!json) return null;
1470
+ if (!json.newStart) return null;
1471
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(json.newStart)) return null;
1472
+ return json;
1473
+ } catch { return null; }
1474
+ }
1475
+
1476
+ /** Robust JSON object extractor — strips fences, finds the first balanced {...}. */
1477
+ _extractJsonObject(text) {
1478
+ if (!text) return null;
1479
+ const cleaned = String(text).replace(/```(?:json)?\s*/gi, '').replace(/```/g, '').trim();
1480
+ const start = cleaned.indexOf('{');
1481
+ if (start < 0) return null;
1482
+ let depth = 0, end = -1, inStr = false, esc = false;
1483
+ for (let i = start; i < cleaned.length; i++) {
1484
+ const c = cleaned[i];
1485
+ if (esc) { esc = false; continue; }
1486
+ if (c === '\\' && inStr) { esc = true; continue; }
1487
+ if (c === '"' && !esc) inStr = !inStr;
1488
+ if (inStr) continue;
1489
+ if (c === '{') depth++;
1490
+ else if (c === '}') { depth--; if (depth === 0) { end = i; break; } }
1491
+ }
1492
+ if (end < 0) return null;
1493
+ try { return JSON.parse(cleaned.slice(start, end + 1)); } catch { return null; }
1494
+ }
1495
+
1496
+ _addDaysIso(isoDate, days) {
1497
+ const d = new Date(isoDate + 'T00:00:00');
1498
+ d.setDate(d.getDate() + days);
1499
+ return d.toISOString();
1500
+ }
1501
+
1502
+ // ── Direct fresh EMAIL action (LLM only for NLU, executor server-side) ──
1503
+ // Detects "manda email a X riguardo Y" style requests, extracts recipient
1504
+ // + subject + body via a tiny LLM call, then sends via gmail_send tool.
1505
+ async _tryDirectFreshEmailAction(userMessage, config) {
1506
+ if (!userMessage || typeof userMessage !== 'string') return null;
1507
+ const lower = userMessage.toLowerCase();
1508
+ const isEmailSend = /\b(manda|mand[oa]|invi[oa]|spedis(ci|co)|scriv[io]|send|email\s+to|mail\s+to)\s+(?:un'?\s+)?(email|mail|messaggio|messag|e-?mail)\s+/i.test(lower)
1509
+ || /\b(invi[oa]r[ea]|mand[ao]|spedire)\s+un'?\s*e[- ]?mail\b/i.test(lower);
1510
+ if (!isEmailSend) return null;
1511
+
1512
+ const parsed = await this._nluExtractEmailSend(userMessage, config);
1513
+ if (!parsed || !parsed.to || !parsed.body) return null;
1514
+
1515
+ const { executeTool } = await import('./tool-executor.mjs');
1516
+ try {
1517
+ const result = await executeTool('gmail_send', {
1518
+ to: parsed.to,
1519
+ subject: parsed.subject || '(no subject)',
1520
+ body: parsed.body,
1521
+ }, config);
1522
+ const ok = typeof result === 'string' && /sent|inviat|✅|message-?id/i.test(result);
1523
+ const message = ok
1524
+ ? `Fatto. Email inviata a ${parsed.to}${parsed.subject ? ` con oggetto "${parsed.subject}"` : ''}.`
1525
+ : `Non sono riuscito a inviare l'email: ${result}`;
1526
+ if (this._lastDirectAuditChatId) {
1527
+ this._recordAudit(this._lastDirectAuditChatId, {
1528
+ tool: 'gmail_send',
1529
+ success: ok,
1530
+ summary: `→ ${parsed.to}${parsed.subject ? ` · "${parsed.subject}"` : ''}`,
1531
+ });
1532
+ }
1533
+ return { action: 'gmail_send', success: ok, message };
1534
+ } catch (e) {
1535
+ return { action: 'gmail_send', success: false, message: `Errore nell'invio: ${e.message}` };
1536
+ }
1537
+ }
1538
+
1539
+ async _nluExtractEmailSend(userMessage, config) {
1540
+ const sysPrompt =
1541
+ `You extract email-send parameters from natural language. Output ONLY a JSON object:\n` +
1542
+ ` - to (string, recipient email or name if email unknown)\n` +
1543
+ ` - subject (string, the email subject; if missing, infer a 2-6 word summary)\n` +
1544
+ ` - body (string, the email body in the same language as the user)\n` +
1545
+ `Rules: if any field is genuinely missing AND impossible to infer, use null. ` +
1546
+ `Keep body short and natural. Output JSON only, no markdown.`;
1547
+ try {
1548
+ const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 400 });
1549
+ const json = this._extractJsonObject(raw);
1550
+ if (!json || !json.to || !json.body) return null;
1551
+ return json;
1552
+ } catch { return null; }
1553
+ }
1554
+
1555
+ // ── Direct fresh TASK action (add / complete) ──────────────────────────
1556
+ async _tryDirectFreshTaskAction(userMessage, config) {
1557
+ if (!userMessage || typeof userMessage !== 'string') return null;
1558
+ const lower = userMessage.toLowerCase();
1559
+ const isTaskAdd = /\b(aggiung|cre[oai]|fiss|segn|metti|add|create)\w*\s+.*\b(task|attivit[àa]|cosa\s+da\s+fare|todo|to[- ]?do|promemoria\s+da\s+fare)/i.test(lower);
1560
+ const isTaskDone = /\b(complet|fatt[oa]|done|fini[stct]|chiud|spunt|tick|mark\s+as\s+done)\w*\s+.*\b(task|attivit[àa]|todo|to[- ]?do)/i.test(lower);
1561
+ const { executeTool } = await import('./tool-executor.mjs');
1562
+ if (isTaskAdd) {
1563
+ const parsed = await this._nluExtractTaskAdd(userMessage, config);
1564
+ if (!parsed?.title) return null;
1565
+ try {
1566
+ const r = await executeTool('task_add', { title: parsed.title, due: parsed.due || null, priority: parsed.priority || 'medium' }, config);
1567
+ const ok = typeof r === 'string' && !/error/i.test(r);
1568
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'task_add', success: ok, summary: `"${parsed.title}"${parsed.due ? ` (entro ${parsed.due})` : ''}` });
1569
+ return { action: 'task_add', success: ok, message: ok ? `Fatto. Ho aggiunto il task "${parsed.title}"${parsed.due ? ` con scadenza ${parsed.due}` : ''}.` : `Errore: ${r}` };
1570
+ } catch (e) { return { action: 'task_add', success: false, message: `Errore: ${e.message}` }; }
1571
+ }
1572
+ if (isTaskDone) {
1573
+ const parsed = await this._nluExtractTaskRef(userMessage, config);
1574
+ if (!parsed?.title) return null;
1575
+ try {
1576
+ const r = await executeTool('task_complete', { title: parsed.title }, config);
1577
+ const ok = typeof r === 'string' && !/error|not found/i.test(r);
1578
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'task_complete', success: ok, summary: `"${parsed.title}"` });
1579
+ return { action: 'task_complete', success: ok, message: ok ? `Fatto. Task "${parsed.title}" segnato come completato.` : `Errore: ${r}` };
1580
+ } catch (e) { return { action: 'task_complete', success: false, message: `Errore: ${e.message}` }; }
1581
+ }
1582
+ return null;
1583
+ }
1584
+
1585
+ // ── Direct fresh NOTE add ───────────────────────────────────────────────
1586
+ async _tryDirectFreshNoteAction(userMessage, config) {
1587
+ if (!userMessage || typeof userMessage !== 'string') return null;
1588
+ const lower = userMessage.toLowerCase();
1589
+ const isNoteAdd = /\b(crea|nuov[oa]|aggiung|salv|prend|scriv|appunt|new|add|save)\w*\s+(?:una\s+)?\b(nota|note|appunt)/i.test(lower);
1590
+ if (!isNoteAdd) return null;
1591
+ const parsed = await this._nluExtractNoteAdd(userMessage, config);
1592
+ if (!parsed?.title) return null;
1593
+ const { executeTool } = await import('./tool-executor.mjs');
1594
+ try {
1595
+ const r = await executeTool('note_add', { title: parsed.title, content: parsed.content || '' }, config);
1596
+ const ok = typeof r === 'string' && !/error/i.test(r);
1597
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'note_add', success: ok, summary: `"${parsed.title}"` });
1598
+ return { action: 'note_add', success: ok, message: ok ? `Fatto. Nota "${parsed.title}" salvata.` : `Errore: ${r}` };
1599
+ } catch (e) { return { action: 'note_add', success: false, message: `Errore: ${e.message}` }; }
1600
+ }
1601
+
1602
+ // ── Direct fresh REMINDER create ────────────────────────────────────────
1603
+ async _tryDirectFreshReminderAction(userMessage, config) {
1604
+ if (!userMessage || typeof userMessage !== 'string') return null;
1605
+ const lower = userMessage.toLowerCase();
1606
+ const isReminder = /\b(ricordami|reminder|promemoria|ricord[aoo]|notificami|avvisami|remind)\b/i.test(lower);
1607
+ if (!isReminder) return null;
1608
+ const parsed = await this._nluExtractReminder(userMessage, config);
1609
+ if (!parsed?.message || !parsed?.when) return null;
1610
+ const { executeTool } = await import('./tool-executor.mjs');
1611
+ try {
1612
+ const r = await executeTool('reminder_create', { message: parsed.message, when: parsed.when }, config);
1613
+ const ok = typeof r === 'string' && !/error/i.test(r);
1614
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'reminder_create', success: ok, summary: `"${parsed.message}" @ ${parsed.when}` });
1615
+ return { action: 'reminder_create', success: ok, message: ok ? `Fatto. Promemoria impostato: "${parsed.message}" per ${parsed.when}.` : `Errore: ${r}` };
1616
+ } catch (e) { return { action: 'reminder_create', success: false, message: `Errore: ${e.message}` }; }
1617
+ }
1618
+
1619
+ // ── Direct fresh SLACK send ─────────────────────────────────────────────
1620
+ async _tryDirectFreshSlackAction(userMessage, config) {
1621
+ if (!userMessage || typeof userMessage !== 'string') return null;
1622
+ const lower = userMessage.toLowerCase();
1623
+ const isSlackSend = /\b(manda|invi[ao]|posta|scriv[io]|send|post)\s+.*\b(slack|canale|channel|#)/i.test(lower)
1624
+ || /\bsu\s+slack\b/i.test(lower);
1625
+ if (!isSlackSend) return null;
1626
+ const parsed = await this._nluExtractSlackSend(userMessage, config);
1627
+ if (!parsed?.channel || !parsed?.text) return null;
1628
+ const { executeTool } = await import('./tool-executor.mjs');
1629
+ try {
1630
+ const r = await executeTool('slack_send', { channel: parsed.channel, text: parsed.text }, config);
1631
+ const ok = typeof r === 'string' && !/error|not found/i.test(r);
1632
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'slack_send', success: ok, summary: `→ ${parsed.channel}: "${parsed.text.slice(0, 60)}"` });
1633
+ return { action: 'slack_send', success: ok, message: ok ? `Fatto. Messaggio inviato a ${parsed.channel}.` : `Errore: ${r}` };
1634
+ } catch (e) { return { action: 'slack_send', success: false, message: `Errore: ${e.message}` }; }
1635
+ }
1636
+
1637
+ // ── NLU extractors (LLM-driven JSON parsing only, never tool execution) ──
1638
+ async _nluExtractTaskAdd(userMessage, config) {
1639
+ const todayIso = new Date().toISOString().slice(0, 10);
1640
+ const sys = `Today is ${todayIso}. Extract task params from the message. Output ONLY JSON:\n` +
1641
+ ` - title (string, the task description)\n` +
1642
+ ` - due (string, "YYYY-MM-DD" if mentioned, else null)\n` +
1643
+ ` - priority ("low" | "medium" | "high" | null)`;
1644
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 200 })); } catch { return null; }
1645
+ }
1646
+ async _nluExtractTaskRef(userMessage, config) {
1647
+ const sys = `Extract which task to mark completed. Output ONLY JSON:\n - title (string)`;
1648
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 150 })); } catch { return null; }
1649
+ }
1650
+ async _nluExtractNoteAdd(userMessage, config) {
1651
+ const sys = `Extract a new-note request. Output ONLY JSON:\n - title (string)\n - content (string, can be empty)`;
1652
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 400 })); } catch { return null; }
1653
+ }
1654
+ async _nluExtractReminder(userMessage, config) {
1655
+ const todayIso = new Date().toISOString().slice(0, 10);
1656
+ const sys = `Today is ${todayIso}. Extract reminder params. Output ONLY JSON:\n` +
1657
+ ` - message (string, what to remind)\n - when (string, ISO datetime "YYYY-MM-DDTHH:MM:00")`;
1658
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 200 })); } catch { return null; }
1659
+ }
1660
+ async _nluExtractSlackSend(userMessage, config) {
1661
+ const sys = `Extract Slack-send params. Output ONLY JSON:\n` +
1662
+ ` - channel (string, with "#" prefix for channels or "@user" for DMs)\n - text (string, message body)`;
1663
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 300 })); } catch { return null; }
1664
+ }
1665
+
1410
1666
  // ── Direct fresh calendar action (no LLM) ─────────────────────────────────
1411
1667
  // Detects DELETE / LIST_MONTH / LIST_WEEK / LIST_DAY / LIST_TODAY /
1412
1668
  // LIST_TOMORROW intents from a fresh user message and runs the proper tool
@@ -1432,6 +1688,14 @@ class TelegramResponder {
1432
1688
  // a date or title reference. We answer FACTUALLY by re-querying the
1433
1689
  // calendar — never by apologizing in the abstract.
1434
1690
  const isVerify = /\b(non\s+(vedo|c'è|esiste)|c'è\s+(ancora|sempre)|è\s+ancora|stai\s+sbagliand|hai\s+sbagliat|in\s+realt[àa]|sei\s+sicur|davvero|inaffidabil|controlla|verifica|conferma\b|guarda)\b/.test(lower);
1691
+ // CREATE event intent — action verb + event noun. Robust enough to catch
1692
+ // "fissami un appuntamento dal dentista venerdì alle 10", "crea evento
1693
+ // ortocheratologia 18 maggio 10-11", "segna riunione con Marco lunedì 14:30".
1694
+ const isCreate = /\b(fiss|cre[oai]|aggiung|inserisc|programm|segn|prenoti?|impost|metti|registr|set\s+up|create|add|schedule|book)\w*\s+/.test(lower)
1695
+ && /\b(appuntament|evento|event\b|meeting|riunion|incontro|chiamat|call\b|webinar|memo|reminder|promemoria|task|impegn)/i.test(lower);
1696
+ // MOVE intent — reschedule/postpone an existing event.
1697
+ const isMove = /\b(spost[ao]|rimand|rinvi|riprogramm|move|reschedule|postpone|cambia\s+(?:data|ora|orari))\w*/.test(lower)
1698
+ && /\b(appuntament|evento|event\b|meeting|riunion|incontro|chiamat|call\b)/i.test(lower);
1435
1699
 
1436
1700
  const { executeTool } = await import('./tool-executor.mjs');
1437
1701
 
@@ -1491,8 +1755,108 @@ class TelegramResponder {
1491
1755
  // No date/title to verify — fall through to LIST/DELETE detection.
1492
1756
  }
1493
1757
 
1758
+ // ─── CREATE intent ─────────────────────────────────────────────────────
1759
+ // We use a tiny LLM call ONLY to extract structured params from natural
1760
+ // language (NLU). Tool execution stays deterministic server-side. This
1761
+ // is the enterprise pattern: LLM for understanding + rendering, NEVER
1762
+ // for state-changing actions.
1763
+ if (isCreate && !isDelete && !isVerify) {
1764
+ const parsed = await this._nluExtractCalendarCreate(userMessage, config);
1765
+ if (parsed && parsed.title && parsed.start) {
1766
+ try {
1767
+ const result = await executeTool('calendar_create', {
1768
+ summary: parsed.title,
1769
+ start: parsed.start,
1770
+ end: parsed.end || this._addMinutesIso(parsed.start, 60),
1771
+ description: parsed.description || '',
1772
+ }, config);
1773
+ const ok = typeof result === 'string' && /created|event\s+"|eventId/i.test(result);
1774
+ const startLabel = `${this._formatDateIT(parsed.start.slice(0, 10))} alle ${parsed.start.slice(11, 16)}`;
1775
+ const message = ok
1776
+ ? `Fatto. Ho creato l'appuntamento "${parsed.title}" il ${startLabel}.`
1777
+ : `Non sono riuscito a creare l'appuntamento: ${result}`;
1778
+ if (this._lastDirectAuditChatId) {
1779
+ this._recordAudit(this._lastDirectAuditChatId, {
1780
+ tool: 'calendar_create',
1781
+ success: ok,
1782
+ summary: `"${parsed.title}" il ${startLabel}`,
1783
+ });
1784
+ }
1785
+ return { action: 'calendar_create', success: ok, message };
1786
+ } catch (e) {
1787
+ return { action: 'calendar_create', success: false, message: `Errore durante la creazione: ${e.message}` };
1788
+ }
1789
+ }
1790
+ // Couldn't extract title/date with confidence → let the LLM ask the user.
1791
+ // Fall through.
1792
+ }
1793
+
1794
+ // ─── MOVE intent ───────────────────────────────────────────────────────
1795
+ if (isMove && !isDelete && !isVerify && !isCreate) {
1796
+ const parsed = await this._nluExtractCalendarMove(userMessage, config);
1797
+ if (parsed && (parsed.title || parsed.oldDate) && parsed.newStart) {
1798
+ // Find the source event.
1799
+ let candidates = [];
1800
+ try {
1801
+ if (parsed.oldDate) {
1802
+ const r = await executeTool('calendar_date', { date: parsed.oldDate }, config);
1803
+ candidates = this._parseEventsFromToolOutput(r);
1804
+ }
1805
+ if (candidates.length === 0 && parsed.title) {
1806
+ const r = await executeTool('calendar_find', { query: parsed.title, daysAhead: 60 }, config);
1807
+ candidates = this._parseEventsFromToolOutput(r);
1808
+ }
1809
+ } catch (e) {
1810
+ return { action: 'calendar_move', success: false, message: `Errore nella ricerca dell'evento: ${e.message}` };
1811
+ }
1812
+ // Match by title token overlap, same heuristic as DELETE.
1813
+ const norm = (s) => String(s || '').toLowerCase()
1814
+ .normalize('NFD').replace(/[̀-ͯ]/g, '')
1815
+ .replace(/[^a-z0-9\s]/g, ' ')
1816
+ .split(/\s+/).filter(t => t.length > 2);
1817
+ let match = null;
1818
+ if (parsed.title) {
1819
+ const titleTokens = norm(parsed.title);
1820
+ const scored = candidates.map(c => {
1821
+ const summaryTokens = new Set(norm(c.summary));
1822
+ const score = titleTokens.filter(t => summaryTokens.has(t)).length;
1823
+ return { c, score };
1824
+ }).sort((a, b) => b.score - a.score);
1825
+ const top = scored[0];
1826
+ if (top && top.score >= Math.max(1, Math.ceil(titleTokens.length * 0.5))) match = top.c;
1827
+ }
1828
+ if (!match && candidates.length === 1) match = candidates[0];
1829
+ if (!match || !match.eventId) {
1830
+ return { action: 'calendar_move', success: false,
1831
+ message: `Non ho trovato l'appuntamento "${parsed.title || ''}" da spostare. Verifica titolo o data originale.` };
1832
+ }
1833
+ try {
1834
+ const result = await executeTool('calendar_move', {
1835
+ eventId: match.eventId,
1836
+ newStart: parsed.newStart,
1837
+ newEnd: parsed.newEnd || this._addMinutesIso(parsed.newStart, 60),
1838
+ }, config);
1839
+ const ok = typeof result === 'string' && /rescheduled|moved|spostat/i.test(result);
1840
+ const newLabel = `${this._formatDateIT(parsed.newStart.slice(0, 10))} alle ${parsed.newStart.slice(11, 16)}`;
1841
+ const message = ok
1842
+ ? `Fatto. Ho spostato "${match.summary}" al ${newLabel}.`
1843
+ : `Non sono riuscito a spostare l'appuntamento: ${result}`;
1844
+ if (this._lastDirectAuditChatId) {
1845
+ this._recordAudit(this._lastDirectAuditChatId, {
1846
+ tool: 'calendar_move',
1847
+ success: ok,
1848
+ summary: `"${match.summary}" → ${newLabel} (eventId ${match.eventId.slice(0, 16)}…)`,
1849
+ });
1850
+ }
1851
+ return { action: 'calendar_move', success: ok, message };
1852
+ } catch (e) {
1853
+ return { action: 'calendar_move', success: false, message: `Errore nello spostamento: ${e.message}` };
1854
+ }
1855
+ }
1856
+ }
1857
+
1494
1858
  // ─── LIST intents ──────────────────────────────────────────────────────
1495
- if (isList && !isDelete && !isVerify) {
1859
+ if (isList && !isDelete && !isVerify && !isCreate && !isMove) {
1496
1860
  // "appuntamenti di oggi"
1497
1861
  if (/\b(oggi|today)\b/.test(lower)) {
1498
1862
  try {