nothumanallowed 16.0.16 → 16.0.18

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": "16.0.16",
3
+ "version": "16.0.18",
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 = '16.0.16';
8
+ export const VERSION = '16.0.18';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -1160,19 +1160,42 @@ class TelegramResponder {
1160
1160
  // appointments of May". By running the calendar tool server-side, the
1161
1161
  // user always sees REAL data — never fabricated.
1162
1162
  this._lastDirectAuditChatId = chatId;
1163
+
1164
+ // ── UNIVERSAL ANAPHORIC PRE-STEP (v16.0.17) ──
1165
+ // Same logic as tryDirectActionAll on the chat web: intercept
1166
+ // anaphoric commands ("cancellalo", "il primo", "si") BEFORE the
1167
+ // per-domain handlers, since the calendar regex doesn't catch
1168
+ // pronouns and the LLM otherwise hallucinates.
1169
+ let directFresh = null;
1170
+ try {
1171
+ const anaphor = this._detectAnaphoricAction(cleanText);
1172
+ if (anaphor) {
1173
+ const resolved = this._resolveAnaphoric(null, cleanText);
1174
+ if (resolved?.item) {
1175
+ directFresh = await this._executeAnaphoricVerb(anaphor, resolved.kind, resolved.item, cleanText, this.config);
1176
+ } else {
1177
+ this.log(`[Telegram] anaphoric verb=${anaphor} but no item to resolve`);
1178
+ }
1179
+ }
1180
+ } catch (e) {
1181
+ this.log(`[Telegram] anaphoric dispatcher error: ${e.message}`);
1182
+ }
1183
+
1163
1184
  // Run the per-domain direct-action dispatcher. First match wins; falls
1164
1185
  // through to LLM if no handler claims the message.
1165
1186
  // Fast-path specialised handlers (regex-driven, lower latency for the
1166
1187
  // common cases), then the universal dispatcher that covers ALL 50+
1167
1188
  // mutation tools via a single LLM-NLU+deterministic-execute pass.
1168
- const directFresh =
1169
- await this._tryDirectFreshCalendarAction(cleanText, this.config) ||
1170
- await this._tryDirectFreshEmailAction(cleanText, this.config) ||
1171
- await this._tryDirectFreshTaskAction(cleanText, this.config) ||
1172
- await this._tryDirectFreshNoteAction(cleanText, this.config) ||
1173
- await this._tryDirectFreshReminderAction(cleanText, this.config) ||
1174
- await this._tryDirectFreshSlackAction(cleanText, this.config) ||
1175
- await this._tryDirectFreshUniversalAction(cleanText, this.config);
1189
+ if (!directFresh) {
1190
+ directFresh =
1191
+ await this._tryDirectFreshCalendarAction(cleanText, this.config) ||
1192
+ await this._tryDirectFreshEmailAction(cleanText, this.config) ||
1193
+ await this._tryDirectFreshTaskAction(cleanText, this.config) ||
1194
+ await this._tryDirectFreshNoteAction(cleanText, this.config) ||
1195
+ await this._tryDirectFreshReminderAction(cleanText, this.config) ||
1196
+ await this._tryDirectFreshSlackAction(cleanText, this.config) ||
1197
+ await this._tryDirectFreshUniversalAction(cleanText, this.config);
1198
+ }
1176
1199
  if (directFresh) {
1177
1200
  this.log(`[Telegram] ${fromUser}: direct-fresh ${directFresh.action} → ${directFresh.success ? 'OK' : 'FAIL'}`);
1178
1201
  const personaName = this.config.responder?.telegram?.botName || this.config.responder?.botName || '';
@@ -1573,21 +1596,152 @@ class TelegramResponder {
1573
1596
  }
1574
1597
 
1575
1598
  /**
1576
- * Detect a generic anaphoric DELETE/COMPLETE/REPLY/OPEN command.
1577
- * Returns the matched action verb or null.
1599
+ * Detect a generic anaphoric verb. Covers 15 verbs in IT+EN.
1600
+ * Order matters: more-specific patterns come BEFORE generic delete/edit
1601
+ * (e.g. "spostalo" must NOT be classified as delete because of '...alo').
1602
+ * Returns the verb string or null.
1578
1603
  */
1579
1604
  _detectAnaphoricAction(userMessage) {
1580
1605
  const t = (userMessage || '').trim();
1581
1606
  if (!t) return null;
1582
- const yes = /^\s*(s[ìi]\b|si\s|sì\s|ok\b|okay\b|certo\b|certamente\b|d'?accordo\b|fai|fallo|procedi|esegui|conferm[oa]|yes\b|yep\b|confirm\b|do\s*it|go\s*ahead)/i.test(t);
1583
- if (yes) return 'confirm';
1607
+
1608
+ // YES/CONFIRM short standalone tokens
1609
+ if (/^\s*(s[ìi]\b|si\s|sì\s|ok\b|okay\b|certo\b|certamente\b|d'?accordo\b|fai\b|fallo|procedi|esegui|conferm[oa]|yes\b|yep\b|confirm\b|do\s*it|go\s*ahead)/i.test(t)) return 'confirm';
1610
+
1611
+ // MOVE/RESCHEDULE — order: BEFORE delete, else "sposta" can be confused
1612
+ if (/\b(spost[ao]l?[oaie]?|sposta|rimand|rinvi[ao]|riprogramm|posticip|sposta?lo|sposta?la|move|reschedul|postpone)\w*/i.test(t)) return 'move';
1613
+
1614
+ // RENAME
1615
+ if (/\b(rinomin|rename|chiamalo|chiamala)\w*/i.test(t)) return 'rename';
1616
+
1617
+ // MARK READ / UNREAD (must come before generic "open/leggi")
1618
+ if (/\b(segna(?:lo|la|li|le)?\s+come\s+(non\s+letto|unread)|mark\s+unread)\b/i.test(t)) return 'mark_unread';
1619
+ if (/\b(segna(?:lo|la|li|le)?\s+come\s+letto|mark\s+(?:as\s+)?read|gi[àa]\s+letto|letto\s+gi[àa])\b/i.test(t)) return 'mark_read';
1620
+
1621
+ // ARCHIVE
1622
+ if (/\b(archivi)\w*/i.test(t)) return 'archive';
1623
+
1624
+ // LABEL / TAG
1625
+ if (/\b(etichett|categoriz|tag\s|aggiungi\s+(?:label|etichetta)|label\b)\w*/i.test(t)) return 'label';
1626
+
1627
+ // FORWARD
1628
+ if (/\b(inoltra|inoltralo|inoltrala|forward|gira(?:lo|la)?)\w*/i.test(t)) return 'forward';
1629
+
1630
+ // SHARE
1631
+ if (/\b(condivid|condividilo|condividila|share|invia\s+link)\w*/i.test(t)) return 'share';
1632
+
1633
+ // PRIORITY change
1634
+ if (/\b(prioriti?z|priority|priorit[àa])\w*/i.test(t)) return 'priority';
1635
+
1636
+ // SNOOZE
1637
+ if (/\b(snooze|rimanda\s+notifica|posticipa\s+(?:notifica|reminder|promemoria))\w*/i.test(t)) return 'snooze';
1638
+
1639
+ // UNDO
1640
+ if (/\b(annulla|undo|disfa|disfa\s+l'?ultima)\w*/i.test(t)) return 'undo';
1641
+
1642
+ // EDIT/MODIFY — generic. Must come AFTER specific edits (rename/label/priority).
1643
+ if (/\b(modifi|aggiorn|cambi(?!a\s+canale)|edit|update)\w*/i.test(t)) return 'edit';
1644
+
1645
+ // DELETE — generic
1584
1646
  if (/(cancell|elimin|rimuov|delete|remove)\w*\s*[!.?]?$/i.test(t)) return 'delete';
1585
- if (/(complet|don[ei]|spunt|finit|fatt)\w*\s*[!.?]?$/i.test(t)) return 'complete';
1647
+
1648
+ // COMPLETE / DONE
1649
+ if (/(complet|don[ei]\b|spunt|finit|fatt)\w*\s*[!.?]?$/i.test(t)) return 'complete';
1650
+
1651
+ // REPLY
1586
1652
  if (/(rispond|reply|risp\b)\w*\s*[!.?]?$/i.test(t)) return 'reply';
1587
- if (/(apri|open|leggi|read|mostra|view)\w*\s*[!.?]?$/i.test(t)) return 'open';
1653
+
1654
+ // OPEN / VIEW / READ
1655
+ if (/(apri|open|leggi|read|mostra|view|visualizz)\w*\s*[!.?]?$/i.test(t)) return 'open';
1656
+
1588
1657
  return null;
1589
1658
  }
1590
1659
 
1660
+ /**
1661
+ * Extract structured parameters for a given verb against a target item.
1662
+ * Uses cheap regex first, then a tiny LLM NLU call when needed.
1663
+ * Returns {} for verbs that don't need params (delete, complete, archive...).
1664
+ */
1665
+ async _extractParamsForVerb(verb, kind, userText, config) {
1666
+ const text = String(userText || '');
1667
+ // ── No-param verbs ────────────────────────────────────────────────────
1668
+ if (['delete', 'complete', 'archive', 'mark_read', 'mark_unread', 'share', 'open', 'snooze', 'undo', 'confirm'].includes(verb)) {
1669
+ return {};
1670
+ }
1671
+
1672
+ // ── PRIORITY: regex extract high/medium/low ──────────────────────────
1673
+ if (verb === 'priority') {
1674
+ if (/\b(alta|high|urgent[ei]?|importante)\b/i.test(text)) return { priority: 'high' };
1675
+ if (/\b(media|medium|normale)\b/i.test(text)) return { priority: 'medium' };
1676
+ if (/\b(bassa|low|secondaria|non\s+urgente)\b/i.test(text)) return { priority: 'low' };
1677
+ return {};
1678
+ }
1679
+
1680
+ // ── LABEL: extract quoted or bare label after the keyword ─────────────
1681
+ if (verb === 'label') {
1682
+ const m = text.match(/(?:etichett[ao]|tag(?:ga)?(?:lo|la)?|label)\s+(?:come\s+|with\s+|as\s+)?["']?([\w\-#/]+)["']?/i);
1683
+ if (m) return { label: m[1] };
1684
+ return {};
1685
+ }
1686
+
1687
+ // ── FORWARD: extract recipient email ─────────────────────────────────
1688
+ if (verb === 'forward') {
1689
+ const m = text.match(/[\w.+-]+@[\w-]+\.[\w.-]+/);
1690
+ if (m) return { to: m[0] };
1691
+ return {};
1692
+ }
1693
+
1694
+ // ── RENAME: extract new name (quoted or after "in/a") ─────────────────
1695
+ if (verb === 'rename') {
1696
+ const m1 = text.match(/(?:rinomina(?:lo|la)?|rename(?:\s+it)?|chiamal[oa])\s+(?:in\s+|a\s+|to\s+|as\s+)?["']([^"']+)["']/i);
1697
+ if (m1) return { newName: m1[1] };
1698
+ const m2 = text.match(/(?:rinomina(?:lo|la)?|rename(?:\s+it)?|chiamal[oa])\s+(?:in\s+|a\s+|to\s+|as\s+)?([\w\-./]+\.\w{2,5})/i);
1699
+ if (m2) return { newName: m2[1] };
1700
+ const m3 = text.match(/(?:rinomina(?:lo|la)?|rename(?:\s+it)?|chiamal[oa])\s+(?:in\s+|a\s+|to\s+|as\s+)([^\n!.?]{3,80})/i);
1701
+ if (m3) return { newName: m3[1].trim() };
1702
+ return {};
1703
+ }
1704
+
1705
+ // ── MOVE: needs newStart/newEnd — use existing _nluExtractCalendarMove ──
1706
+ if (verb === 'move' && kind === 'calendar') {
1707
+ try {
1708
+ const parsed = await this._nluExtractCalendarMove(text, config);
1709
+ if (parsed) return { newStart: parsed.newStart, newEnd: parsed.newEnd };
1710
+ } catch {}
1711
+ return {};
1712
+ }
1713
+
1714
+ // ── EDIT: free-form. Tiny LLM extractor returning a partial object ────
1715
+ if (verb === 'edit') {
1716
+ try {
1717
+ const sys =
1718
+ 'You are a parameter extractor for an EDIT command. Given the user instruction and ' +
1719
+ `the entity kind (${kind}), return STRICT JSON with the fields to update. ` +
1720
+ 'Fields by kind: ' +
1721
+ 'calendar={summary?,start?,end?,location?,description?}; ' +
1722
+ 'email={subject?,body?}; ' +
1723
+ 'task={description?,priority?,due?}; ' +
1724
+ 'contact={name?,email?,phone?}; ' +
1725
+ 'drive={name?}; note={title?,body?}; ' +
1726
+ 'reminder={message?,when?}; gtask={title?,due?}. ' +
1727
+ 'ONLY include fields the user explicitly mentioned. No extra prose.';
1728
+ const raw = await callLLM(config, sys, text, { max_tokens: 200, temperature: 0.1 });
1729
+ const m = raw.match(/\{[\s\S]*\}/);
1730
+ if (m) return JSON.parse(m[0]);
1731
+ } catch {}
1732
+ return {};
1733
+ }
1734
+
1735
+ // ── REPLY: body extraction — let downstream handler ask user if missing ──
1736
+ if (verb === 'reply') {
1737
+ const m = text.match(/(?:rispond[ie](?:gli)?|reply)\s+(?:con\s+|with\s+)?["']([^"']+)["']/i);
1738
+ if (m) return { body: m[1] };
1739
+ return {};
1740
+ }
1741
+
1742
+ return {};
1743
+ }
1744
+
1591
1745
  /**
1592
1746
  * Execute an anaphoric verb (delete/complete/reply/open/confirm) against
1593
1747
  * an item resolved by _resolveAnaphoric. The (verb, kind) pair maps to a
@@ -1601,11 +1755,104 @@ class TelegramResponder {
1601
1755
  if (chatId) this._recordAudit(chatId, { tool: toolName, success, summary });
1602
1756
  return { action: toolName, success, message };
1603
1757
  };
1758
+ const safe = async (toolName, args, okMsg, summary) => {
1759
+ try { await executeTool(toolName, args, config); return auditAndReturn(toolName, true, okMsg, summary); }
1760
+ catch (e) { return auditAndReturn(toolName, false, `Errore: ${e.message}`, ''); }
1761
+ };
1762
+ const id = item.eventId || item.messageId || item.fileId || item.taskId || item.id;
1763
+ const label = item.subject || item.summary || item.name || item.title || item.description || item.message || String(id);
1764
+ const params = await this._extractParamsForVerb(verb, kind, userText, config);
1604
1765
 
1605
1766
  // confirm = treat as the pending action (default: delete) when there's
1606
1767
  // a single recently-listed item.
1607
1768
  const effective = verb === 'confirm' ? 'delete' : verb;
1608
1769
 
1770
+ // ── MOVE/RESCHEDULE ──────────────────────────────────────────────────
1771
+ if (effective === 'move') {
1772
+ if (kind === 'calendar' && id && params.newStart) {
1773
+ return await safe('calendar_move',
1774
+ { eventId: id, newStart: params.newStart, newEnd: params.newEnd || this._addMinutesIso(params.newStart, 60) },
1775
+ `Spostato "${label}" a ${this._formatDateIT(params.newStart.slice(0, 10))} alle ${params.newStart.slice(11, 16)}.`, label);
1776
+ }
1777
+ if (kind === 'drive' && id && params.newName) {
1778
+ return await safe('drive_move', { fileId: id, folderId: params.newName }, `Spostato "${label}".`, label);
1779
+ }
1780
+ if (!params.newStart) {
1781
+ return { action: 'move_pending', success: false, message: `A quando vuoi spostare "${label}"? Es. "venerdì 23 maggio alle 15".` };
1782
+ }
1783
+ }
1784
+
1785
+ // ── RENAME ────────────────────────────────────────────────────────────
1786
+ if (effective === 'rename' && params.newName) {
1787
+ if (kind === 'drive' && id) return await safe('drive_rename', { fileId: id, newName: params.newName }, `Rinominato "${label}" → "${params.newName}".`, params.newName);
1788
+ if (kind === 'note' && id) return await safe('note_update', { id, title: params.newName }, `Rinominata la nota "${label}" → "${params.newName}".`, params.newName);
1789
+ if (kind === 'contact' && id) return await safe('contact_update', { id, name: params.newName }, `Rinominato il contatto "${label}" → "${params.newName}".`, params.newName);
1790
+ if (kind === 'task' && id) return await safe('task_edit', { id, description: params.newName }, `Rinominato il task → "${params.newName}".`, params.newName);
1791
+ }
1792
+ if (effective === 'rename' && !params.newName) {
1793
+ return { action: 'rename_pending', success: false, message: `Come vuoi rinominare "${label}"?` };
1794
+ }
1795
+
1796
+ // ── EDIT (free-form fields) ──────────────────────────────────────────
1797
+ if (effective === 'edit' && Object.keys(params).length > 0 && id) {
1798
+ if (kind === 'calendar') return await safe('calendar_update', { eventId: id, ...params }, `Aggiornato "${label}".`, label);
1799
+ if (kind === 'email') return await safe('gmail_draft_update', { messageId: id, ...params }, `Aggiornata la bozza email.`, label);
1800
+ if (kind === 'task') return await safe('task_edit', { id, ...params }, `Aggiornato il task "${label}".`, label);
1801
+ if (kind === 'contact') return await safe('contact_update', { id, ...params }, `Aggiornato il contatto "${label}".`, label);
1802
+ if (kind === 'note') return await safe('note_update', { id, ...params }, `Aggiornata la nota "${label}".`, label);
1803
+ if (kind === 'reminder') return await safe('reminder_update', { id, ...params }, `Aggiornato il promemoria.`, label);
1804
+ if (kind === 'gtask') return await safe('gtask_edit', { id, ...params }, `Aggiornato il task Google.`, label);
1805
+ if (kind === 'drive' && params.name) return await safe('drive_rename', { fileId: id, newName: params.name }, `Rinominato il file → "${params.name}".`, params.name);
1806
+ }
1807
+ if (effective === 'edit' && Object.keys(params).length === 0) {
1808
+ return { action: 'edit_pending', success: false, message: `Cosa vuoi modificare di "${label}"? (es. titolo, orario, descrizione, priorità)` };
1809
+ }
1810
+
1811
+ // ── MARK READ/UNREAD (email) ─────────────────────────────────────────
1812
+ if (effective === 'mark_read' && kind === 'email' && id) {
1813
+ return await safe('gmail_mark_read', { messageId: id }, `Segnata come letta: "${label}".`, label);
1814
+ }
1815
+ if (effective === 'mark_unread' && kind === 'email' && id) {
1816
+ return await safe('gmail_mark_unread', { messageId: id }, `Segnata come NON letta: "${label}".`, label);
1817
+ }
1818
+
1819
+ // ── ARCHIVE ──────────────────────────────────────────────────────────
1820
+ if (effective === 'archive') {
1821
+ if (kind === 'email' && id) return await safe('gmail_archive', { messageId: id }, `Archiviata email "${label}".`, label);
1822
+ if (kind === 'task' && id) return await safe('task_archive', { id }, `Archiviato task "${label}".`, label);
1823
+ }
1824
+
1825
+ // ── LABEL/TAG ────────────────────────────────────────────────────────
1826
+ if (effective === 'label' && params.label) {
1827
+ if (kind === 'email' && id) return await safe('gmail_label', { messageId: id, label: params.label }, `Aggiunta etichetta "${params.label}" a "${label}".`, params.label);
1828
+ if (kind === 'task' && id) return await safe('task_tag', { id, tag: params.label }, `Aggiunto tag "${params.label}" al task.`, params.label);
1829
+ }
1830
+ if (effective === 'label' && !params.label) {
1831
+ return { action: 'label_pending', success: false, message: `Quale etichetta vuoi applicare a "${label}"?` };
1832
+ }
1833
+
1834
+ // ── FORWARD (email) ──────────────────────────────────────────────────
1835
+ if (effective === 'forward' && kind === 'email' && id) {
1836
+ if (!params.to) return { action: 'forward_pending', success: false, message: `A chi vuoi inoltrare "${label}"?` };
1837
+ return await safe('gmail_forward', { messageId: id, to: params.to }, `Inoltrata "${label}" a ${params.to}.`, params.to);
1838
+ }
1839
+
1840
+ // ── SHARE (drive, calendar) ──────────────────────────────────────────
1841
+ if (effective === 'share') {
1842
+ if (kind === 'drive' && id) return await safe('drive_share', { fileId: id }, `Condiviso "${label}" (link copiato).`, label);
1843
+ }
1844
+
1845
+ // ── PRIORITY change (task) ───────────────────────────────────────────
1846
+ if (effective === 'priority' && params.priority) {
1847
+ if (kind === 'task' && id) return await safe('task_edit', { id, priority: params.priority }, `Priorità di "${label}" → ${params.priority}.`, params.priority);
1848
+ if (kind === 'gtask' && id) return await safe('gtask_edit', { id, priority: params.priority }, `Priorità task Google → ${params.priority}.`, params.priority);
1849
+ }
1850
+
1851
+ // ── SNOOZE (reminder) ───────────────────────────────────────────────
1852
+ if (effective === 'snooze' && kind === 'reminder' && id) {
1853
+ return await safe('reminder_snooze', { id, minutes: 30 }, `Posticipato di 30 minuti il promemoria "${label}".`, label);
1854
+ }
1855
+
1609
1856
  // ── DELETE family ─────────────────────────────────────────────────────
1610
1857
  if (effective === 'delete') {
1611
1858
  if (kind === 'calendar' && item.eventId) {
@@ -1700,7 +1947,14 @@ class TelegramResponder {
1700
1947
  return { from, to: new Date(from.getTime() + 86400000) };
1701
1948
  }
1702
1949
  if (toolName === 'calendar_week') {
1703
- const from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1950
+ // Respect optional startDate (e.g. for "settimana prossima").
1951
+ let from;
1952
+ if (args?.startDate && /^\d{4}-\d{2}-\d{2}$/.test(args.startDate)) {
1953
+ const [yy, mm, dd] = args.startDate.split('-').map(n => parseInt(n, 10));
1954
+ from = new Date(yy, mm - 1, dd);
1955
+ } else {
1956
+ from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1957
+ }
1704
1958
  return { from, to: new Date(from.getTime() + 7 * 86400000) };
1705
1959
  }
1706
1960
  if (toolName === 'calendar_month') {
@@ -2543,6 +2797,26 @@ class TelegramResponder {
2543
2797
  return await runListAndRemember('calendar_today', {}, 'calendar_today');
2544
2798
  if (/\b(domani|tomorrow)\b/.test(lower))
2545
2799
  return await runListAndRemember('calendar_tomorrow', {}, 'calendar_tomorrow');
2800
+ // "settimana prossima / next week / settimana che viene" → calendar_week
2801
+ // starting next Monday. Without this offset, calendar_week always shows
2802
+ // the CURRENT week, which is wrong for "settimana prossima" and lets
2803
+ // the LLM hallucinate a fake list of upcoming events.
2804
+ if (/\b(settimana\s+(prossima|che\s+viene|seguente)|next\s+week|prossima\s+settimana)\b/.test(lower)) {
2805
+ const today = new Date();
2806
+ const dayOfWeek = today.getDay(); // 0=sun..6=sat
2807
+ const daysUntilMonday = ((1 - dayOfWeek + 7) % 7) || 7;
2808
+ const nextMonday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + daysUntilMonday);
2809
+ const startDate = nextMonday.toISOString().slice(0, 10);
2810
+ return await runListAndRemember('calendar_week', { startDate }, 'calendar_week_next');
2811
+ }
2812
+ if (/\b(settimana\s+scorsa|last\s+week|scorsa\s+settimana)\b/.test(lower)) {
2813
+ const today = new Date();
2814
+ const dayOfWeek = today.getDay();
2815
+ const daysToLastMonday = (dayOfWeek === 0 ? 6 : dayOfWeek - 1) + 7;
2816
+ const lastMonday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - daysToLastMonday);
2817
+ const startDate = lastMonday.toISOString().slice(0, 10);
2818
+ return await runListAndRemember('calendar_week', { startDate }, 'calendar_week_last');
2819
+ }
2546
2820
  if (/\b(settimana|week|questa\s+settimana|this\s+week)\b/.test(lower))
2547
2821
  return await runListAndRemember('calendar_week', {}, 'calendar_week');
2548
2822
  const monthMatch = lower.match(new RegExp(`\\b(${Object.keys(MONTH_MAP).join('|')})(?:\\s+(20\\d{2}))?\\b`));
@@ -3191,6 +3191,119 @@ export async function executeTool(action, params, config) {
3191
3191
  return `File ${params.fileId} moved to trash.`;
3192
3192
  }
3193
3193
 
3194
+ case 'drive_rename': {
3195
+ if (!params.fileId || !params.newName) return 'Error: fileId and newName required.';
3196
+ const token = await (await import('./token-store.mjs')).getAccessToken(config);
3197
+ const res = await fetch(`https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=id,name,webViewLink`, {
3198
+ method: 'PATCH',
3199
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
3200
+ body: JSON.stringify({ name: params.newName }),
3201
+ });
3202
+ if (!res.ok) {
3203
+ const err = await res.text();
3204
+ throw new Error(`Drive rename ${res.status}: ${err.slice(0, 200)}`);
3205
+ }
3206
+ const data = await res.json();
3207
+ return `Renamed: ${data.name} — ${data.webViewLink || data.id}`;
3208
+ }
3209
+
3210
+ case 'drive_move': {
3211
+ if (!params.fileId || !params.folderId) return 'Error: fileId and folderId required.';
3212
+ const token = await (await import('./token-store.mjs')).getAccessToken(config);
3213
+ // Need to first get current parents to remove them
3214
+ const cur = await fetch(`https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=parents`, { headers: { 'Authorization': `Bearer ${token}` } });
3215
+ const curData = await cur.json();
3216
+ const oldParents = (curData.parents || []).join(',');
3217
+ const res = await fetch(`https://www.googleapis.com/drive/v3/files/${params.fileId}?addParents=${params.folderId}&removeParents=${oldParents}&fields=id,name,parents`, {
3218
+ method: 'PATCH',
3219
+ headers: { 'Authorization': `Bearer ${token}` },
3220
+ });
3221
+ if (!res.ok) throw new Error(`Drive move ${res.status}: ${(await res.text()).slice(0, 200)}`);
3222
+ return `Moved: ${(await res.json()).name}`;
3223
+ }
3224
+
3225
+ case 'drive_share': {
3226
+ if (!params.fileId) return 'Error: fileId required.';
3227
+ const token = await (await import('./token-store.mjs')).getAccessToken(config);
3228
+ const role = params.role || 'reader';
3229
+ const res = await fetch(`https://www.googleapis.com/drive/v3/files/${params.fileId}/permissions`, {
3230
+ method: 'POST',
3231
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
3232
+ body: JSON.stringify({ type: 'anyone', role }),
3233
+ });
3234
+ if (!res.ok) throw new Error(`Drive share ${res.status}: ${(await res.text()).slice(0, 200)}`);
3235
+ const meta = await fetch(`https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=webViewLink,name`, { headers: { 'Authorization': `Bearer ${token}` } });
3236
+ const m = await meta.json();
3237
+ return `Shared "${m.name}" (${role}): ${m.webViewLink}`;
3238
+ }
3239
+
3240
+ case 'gmail_label': {
3241
+ if (!params.messageId || !params.label) return 'Error: messageId and label required.';
3242
+ const token = await (await import('./token-store.mjs')).getAccessToken(config);
3243
+ // Find or create the label by name
3244
+ const labelsRes = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/labels', { headers: { 'Authorization': `Bearer ${token}` } });
3245
+ const labelsData = await labelsRes.json();
3246
+ let labelId = (labelsData.labels || []).find(l => l.name.toLowerCase() === params.label.toLowerCase())?.id;
3247
+ if (!labelId) {
3248
+ const createRes = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/labels', {
3249
+ method: 'POST',
3250
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
3251
+ body: JSON.stringify({ name: params.label, labelListVisibility: 'labelShow', messageListVisibility: 'show' }),
3252
+ });
3253
+ if (!createRes.ok) throw new Error(`Label create ${createRes.status}`);
3254
+ labelId = (await createRes.json()).id;
3255
+ }
3256
+ const res = await fetch(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${params.messageId}/modify`, {
3257
+ method: 'POST',
3258
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
3259
+ body: JSON.stringify({ addLabelIds: [labelId] }),
3260
+ });
3261
+ if (!res.ok) throw new Error(`Gmail label ${res.status}: ${(await res.text()).slice(0, 200)}`);
3262
+ return `Applied label "${params.label}" to message.`;
3263
+ }
3264
+
3265
+ case 'gmail_forward': {
3266
+ if (!params.messageId || !params.to) return 'Error: messageId and to required.';
3267
+ const { getMessage, sendEmail } = await import('./google-gmail.mjs');
3268
+ const orig = await getMessage(config, params.messageId);
3269
+ const body = `\n\n---------- Messaggio inoltrato ----------\nDa: ${orig.from}\nData: ${orig.date}\nOggetto: ${orig.subject}\n\n${orig.body}`;
3270
+ await sendEmail(config, {
3271
+ to: params.to,
3272
+ subject: `Fwd: ${orig.subject}`,
3273
+ body: (params.note || '') + body,
3274
+ });
3275
+ return `Forwarded "${orig.subject}" to ${params.to}.`;
3276
+ }
3277
+
3278
+ case 'gtask_update': {
3279
+ if (!params.id) return 'Error: id required.';
3280
+ const token = await (await import('./token-store.mjs')).getAccessToken(config);
3281
+ const listId = params.listId || '@default';
3282
+ const patch = {};
3283
+ if (params.title) patch.title = params.title;
3284
+ if (params.due) patch.due = new Date(params.due).toISOString();
3285
+ if (params.notes) patch.notes = params.notes;
3286
+ const res = await fetch(`https://tasks.googleapis.com/tasks/v1/lists/${listId}/tasks/${params.id}`, {
3287
+ method: 'PATCH',
3288
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
3289
+ body: JSON.stringify(patch),
3290
+ });
3291
+ if (!res.ok) throw new Error(`GTask update ${res.status}: ${(await res.text()).slice(0, 200)}`);
3292
+ return `Updated Google Task.`;
3293
+ }
3294
+
3295
+ case 'gtask_delete': {
3296
+ if (!params.id) return 'Error: id required.';
3297
+ const token = await (await import('./token-store.mjs')).getAccessToken(config);
3298
+ const listId = params.listId || '@default';
3299
+ const res = await fetch(`https://tasks.googleapis.com/tasks/v1/lists/${listId}/tasks/${params.id}`, {
3300
+ method: 'DELETE',
3301
+ headers: { 'Authorization': `Bearer ${token}` },
3302
+ });
3303
+ if (!res.ok && res.status !== 204) throw new Error(`GTask delete ${res.status}`);
3304
+ return `Deleted Google Task.`;
3305
+ }
3306
+
3194
3307
  case 'drive_info': {
3195
3308
  if (!params.fileId) return 'Error: fileId required.';
3196
3309
  const drv = await import('./google-drive.mjs');