nothumanallowed 16.0.15 → 16.0.17

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.15",
3
+ "version": "16.0.17",
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.15';
8
+ export const VERSION = '16.0.17';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -16,7 +16,7 @@ import {
16
16
  } from '../../services/conversations.mjs';
17
17
  import { callLLMStream, callLLM, callLLMVision, parseAgentFile } from '../../services/llm.mjs';
18
18
  import { buildMemoryContext } from '../../services/memory.mjs';
19
- import { parseActions, executeTool, buildSystemPrompt, stripOrphanFences } from '../../services/tool-executor.mjs';
19
+ import { parseActions, executeTool, executeToolAndRemember, buildSystemPrompt, stripOrphanFences } from '../../services/tool-executor.mjs';
20
20
  import { detectLanguage, tryDirectActionAll } from '../../services/message-responder.mjs';
21
21
 
22
22
  // Migrate on import (once)
@@ -487,7 +487,11 @@ export function register(router) {
487
487
  if (action === 'web_search' && wantsScreenshot) params.screenshot = true;
488
488
  sse('tool', { action, status: 'executing' });
489
489
  try {
490
- const result = await executeTool(action, params, config);
490
+ // Use the remembering variant so list-tools auto-populate the
491
+ // anaphoric cache (~/.nha/list-cache.json). Same chatId/auditKey
492
+ // pattern as direct-action handlers.
493
+ const auditKey = `chat:${body.conversationId || 'anon'}`;
494
+ const result = await executeToolAndRemember(action, params, config, auditKey);
491
495
 
492
496
  // ── Screenshot result handling ───────────────────────────────────
493
497
  if (result && typeof result === 'object' && result.__screenshot) {
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Last-list cache — shared between tool-executor and message-responder.
3
+ *
4
+ * Whenever a list-tool runs (gmail_list, task_list, drive_list, etc.) the
5
+ * structured items are stored here keyed by chatId. The anaphoric resolver
6
+ * in message-responder reads from here to map "cancellalo / aprilo / il
7
+ * primo / l'ultimo" to the correct item ID.
8
+ *
9
+ * Persisted to ~/.nha/list-cache.json so it survives daemon restarts.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { NHA_DIR } from '../constants.mjs';
15
+
16
+ const CACHE_FILE = path.join(NHA_DIR, 'list-cache.json');
17
+ const MAX_ITEMS_PER_LIST = 50; // cap per list to avoid prompt explosion
18
+
19
+ let _inMemory = null;
20
+
21
+ function _load() {
22
+ if (_inMemory) return _inMemory;
23
+ try {
24
+ if (fs.existsSync(CACHE_FILE)) {
25
+ _inMemory = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
26
+ } else {
27
+ _inMemory = {};
28
+ }
29
+ } catch { _inMemory = {}; }
30
+ return _inMemory;
31
+ }
32
+
33
+ function _save() {
34
+ try {
35
+ if (!fs.existsSync(NHA_DIR)) fs.mkdirSync(NHA_DIR, { recursive: true });
36
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(_inMemory, null, 2));
37
+ } catch {}
38
+ }
39
+
40
+ /**
41
+ * Store the most recently shown items of a given kind for a given chat.
42
+ * @param {string} chatId — caller identity ('chat:abc', telegramChatId, '__last_list__')
43
+ * @param {string} kind — 'calendar' | 'email' | 'task' | 'contact' | 'drive' | 'note' | 'reminder' | 'gtask' | 'notion' | 'slack' | 'github' | any
44
+ * @param {Array<object>} items — structured items, each must have an id field
45
+ */
46
+ export function rememberList(chatId, kind, items) {
47
+ if (!kind || !Array.isArray(items)) return;
48
+ const cache = _load();
49
+ const key = chatId || '__last_list__';
50
+ cache[key] = cache[key] || {};
51
+ const capped = items.slice(0, MAX_ITEMS_PER_LIST);
52
+ cache[key][`lastList_${kind}`] = capped;
53
+ cache[key][`lastList_${kind}_at`] = Date.now();
54
+ cache[key].lastListKind = kind;
55
+ cache[key].lastListAt = Date.now();
56
+ _save();
57
+ }
58
+
59
+ /**
60
+ * Retrieve items by kind for a chatId. Falls back to the freshest list of
61
+ * that kind across ALL chats if the specific one is empty.
62
+ */
63
+ export function getList(chatId, kind) {
64
+ const cache = _load();
65
+ const direct = cache[chatId || '__last_list__']?.[`lastList_${kind}`];
66
+ if (Array.isArray(direct) && direct.length > 0) return direct;
67
+ // Fallback: best-most-recent across chats.
68
+ let best = null, bestAt = 0;
69
+ for (const v of Object.values(cache)) {
70
+ const items = v?.[`lastList_${kind}`];
71
+ const at = v?.[`lastList_${kind}_at`] || 0;
72
+ if (Array.isArray(items) && items.length > 0 && at > bestAt) {
73
+ best = items; bestAt = at;
74
+ }
75
+ }
76
+ return best || [];
77
+ }
78
+
79
+ /**
80
+ * What kind was last listed (any chat). Used when the user issues an
81
+ * anaphoric command without specifying kind.
82
+ */
83
+ export function getLastListKind(chatId) {
84
+ const cache = _load();
85
+ if (chatId && cache[chatId]?.lastListKind) return cache[chatId].lastListKind;
86
+ let bestKind = null, bestAt = 0;
87
+ for (const v of Object.values(cache)) {
88
+ if (v?.lastListKind && (v.lastListAt || 0) > bestAt) {
89
+ bestKind = v.lastListKind; bestAt = v.lastListAt;
90
+ }
91
+ }
92
+ return bestKind;
93
+ }
@@ -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 || '';
@@ -1486,6 +1509,204 @@ class TelegramResponder {
1486
1509
  // Parse calendar_date / calendar_find tool output. The executor returns
1487
1510
  // a human-readable string with each event on its own line plus the
1488
1511
  // eventId in parentheses. We extract structured records.
1512
+ // ── Generic LIST→REMEMBER + ANAPHORIC RESOLUTION (v16.0.16) ────────────
1513
+ // Same pattern as the calendar fix, applied uniformly to every list-tool:
1514
+ // email, task, contact, drive, note, reminder, gtask, notion.
1515
+ // Why: a single regex parser can never handle every tool's ID format.
1516
+ // The cleanest path is to call the low-level API directly and persist
1517
+ // structured items, then resolve anaphoric references generically.
1518
+ // Open allowlist of kinds — no hardcoded restriction. Any new tool can
1519
+ // call _rememberItems(<any-kind-string>, items) without changing this file.
1520
+ _propForKind(kind) { return `lastList_${kind}`; }
1521
+
1522
+ _rememberItems(kind, items, extra = {}) {
1523
+ if (!kind || !Array.isArray(items)) return;
1524
+ const chatId = this._lastDirectAuditChatId || '__last_list__';
1525
+ const prop = this._propForKind(kind);
1526
+ const prev = this._lastContextByChatId[chatId] || {};
1527
+ this._lastContextByChatId[chatId] = {
1528
+ ...prev,
1529
+ [prop]: items,
1530
+ [`${prop}At`]: Date.now(),
1531
+ lastListKind: kind,
1532
+ lastListAt: Date.now(),
1533
+ ...extra,
1534
+ };
1535
+ this.log(`[direct] ${kind} LIST stored: chatId=${chatId} count=${items.length}`);
1536
+ try { saveTelegramContext(this._lastContextByChatId); } catch {}
1537
+ }
1538
+
1539
+ /**
1540
+ * Resolve an anaphoric reference ("cancellalo", "il primo", "l'ultimo",
1541
+ * "il numero 2") against the most recently listed items of a given kind.
1542
+ * If kind is omitted, falls back to lastListKind (the most recently listed
1543
+ * type — calendar after calendar_today, email after gmail_list, etc).
1544
+ * Cross-key fallback included.
1545
+ */
1546
+ _resolveAnaphoric(kind, userMessage) {
1547
+ const chatId = this._lastDirectAuditChatId;
1548
+ // Use the shared list-cache module as source of truth — it's populated
1549
+ // by executeToolAndRemember from every channel.
1550
+ let items = [];
1551
+ try {
1552
+ const cacheFile = path.join(os.homedir(), '.nha', 'list-cache.json');
1553
+ let cache = {};
1554
+ if (fs.existsSync(cacheFile)) {
1555
+ try { cache = JSON.parse(fs.readFileSync(cacheFile, 'utf-8')); } catch { cache = {}; }
1556
+ }
1557
+ if (!kind) {
1558
+ // Auto-pick the freshest list across any chat.
1559
+ const direct = chatId && cache[chatId];
1560
+ if (direct?.lastListKind) kind = direct.lastListKind;
1561
+ else {
1562
+ let bestKind = null, bestAt = 0;
1563
+ for (const v of Object.values(cache)) {
1564
+ if (v?.lastListKind && (v.lastListAt || 0) > bestAt) { bestKind = v.lastListKind; bestAt = v.lastListAt; }
1565
+ }
1566
+ kind = bestKind;
1567
+ }
1568
+ }
1569
+ if (!kind) return null;
1570
+ const prop = `lastList_${kind}`;
1571
+ items = (chatId && cache[chatId]?.[prop]) || [];
1572
+ if (items.length === 0) {
1573
+ let bestArr = null, bestAt = 0;
1574
+ for (const v of Object.values(cache)) {
1575
+ if (Array.isArray(v?.[prop]) && v[prop].length > 0 && (v[`${prop}_at`] || 0) > bestAt) {
1576
+ bestArr = v[prop]; bestAt = v[`${prop}_at`] || 0;
1577
+ }
1578
+ }
1579
+ if (bestArr) items = bestArr;
1580
+ }
1581
+ } catch {}
1582
+ if (items.length === 0) return null;
1583
+ const low = (userMessage || '').toLowerCase();
1584
+ const ordinalMap = { primo: 0, prima: 0, secondo: 1, seconda: 1, terzo: 2, terza: 2, quarto: 3, quinto: 4, first: 0, second: 1, third: 2 };
1585
+ for (const [word, idx] of Object.entries(ordinalMap)) {
1586
+ if (new RegExp(`\\b${word}\\b`).test(low) && items[idx]) return { item: items[idx], kind };
1587
+ }
1588
+ if (/\b(ultim[oa]|last)\b/.test(low)) return { item: items[items.length - 1], kind };
1589
+ const numMatch = low.match(/\b(?:numero|number|n\.?|#)\s*(\d+)\b/);
1590
+ if (numMatch) {
1591
+ const idx = parseInt(numMatch[1], 10) - 1;
1592
+ if (items[idx]) return { item: items[idx], kind };
1593
+ }
1594
+ if (items.length === 1) return { item: items[0], kind };
1595
+ return null;
1596
+ }
1597
+
1598
+ /**
1599
+ * Detect a generic anaphoric DELETE/COMPLETE/REPLY/OPEN command.
1600
+ * Returns the matched action verb or null.
1601
+ */
1602
+ _detectAnaphoricAction(userMessage) {
1603
+ const t = (userMessage || '').trim();
1604
+ if (!t) return null;
1605
+ 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);
1606
+ if (yes) return 'confirm';
1607
+ if (/(cancell|elimin|rimuov|delete|remove)\w*\s*[!.?]?$/i.test(t)) return 'delete';
1608
+ if (/(complet|don[ei]|spunt|finit|fatt)\w*\s*[!.?]?$/i.test(t)) return 'complete';
1609
+ if (/(rispond|reply|risp\b)\w*\s*[!.?]?$/i.test(t)) return 'reply';
1610
+ if (/(apri|open|leggi|read|mostra|view)\w*\s*[!.?]?$/i.test(t)) return 'open';
1611
+ return null;
1612
+ }
1613
+
1614
+ /**
1615
+ * Execute an anaphoric verb (delete/complete/reply/open/confirm) against
1616
+ * an item resolved by _resolveAnaphoric. The (verb, kind) pair maps to a
1617
+ * concrete tool call. Unknown combinations return null (fall through to
1618
+ * the LLM). All executions persist to the global audit log.
1619
+ */
1620
+ async _executeAnaphoricVerb(verb, kind, item, userText, config) {
1621
+ const { executeTool } = await import('./tool-executor.mjs');
1622
+ const chatId = this._lastDirectAuditChatId;
1623
+ const auditAndReturn = (toolName, success, message, summary) => {
1624
+ if (chatId) this._recordAudit(chatId, { tool: toolName, success, summary });
1625
+ return { action: toolName, success, message };
1626
+ };
1627
+
1628
+ // confirm = treat as the pending action (default: delete) when there's
1629
+ // a single recently-listed item.
1630
+ const effective = verb === 'confirm' ? 'delete' : verb;
1631
+
1632
+ // ── DELETE family ─────────────────────────────────────────────────────
1633
+ if (effective === 'delete') {
1634
+ if (kind === 'calendar' && item.eventId) {
1635
+ try { await executeTool('calendar_delete', { eventId: item.eventId }, config); return auditAndReturn('calendar_delete', true, `Ho cancellato "${item.summary}".`, item.summary); }
1636
+ catch (e) { return auditAndReturn('calendar_delete', false, `Errore: ${e.message}`, ''); }
1637
+ }
1638
+ if (kind === 'email' && (item.messageId || item.id)) {
1639
+ try { await executeTool('gmail_delete', { messageId: item.messageId || item.id }, config); return auditAndReturn('gmail_delete', true, `Ho eliminato l'email "${item.subject || item.summary}".`, item.subject || ''); }
1640
+ catch (e) { return auditAndReturn('gmail_delete', false, `Errore: ${e.message}`, ''); }
1641
+ }
1642
+ if (kind === 'task' && item.id) {
1643
+ try { await executeTool('task_delete', { id: item.id }, config); return auditAndReturn('task_delete', true, `Ho eliminato il task "${item.description || item.summary}".`, ''); }
1644
+ catch (e) { return auditAndReturn('task_delete', false, `Errore: ${e.message}`, ''); }
1645
+ }
1646
+ if (kind === 'contact' && item.id) {
1647
+ try { await executeTool('contact_delete', { id: item.id }, config); return auditAndReturn('contact_delete', true, `Ho eliminato il contatto "${item.name || item.summary}".`, ''); }
1648
+ catch (e) { return auditAndReturn('contact_delete', false, `Errore: ${e.message}`, ''); }
1649
+ }
1650
+ if (kind === 'drive' && (item.fileId || item.id)) {
1651
+ try { await executeTool('drive_delete', { fileId: item.fileId || item.id }, config); return auditAndReturn('drive_delete', true, `Ho eliminato il file "${item.name || item.summary}".`, ''); }
1652
+ catch (e) { return auditAndReturn('drive_delete', false, `Errore: ${e.message}`, ''); }
1653
+ }
1654
+ if (kind === 'note' && item.id) {
1655
+ try { await executeTool('note_delete', { id: item.id }, config); return auditAndReturn('note_delete', true, `Ho eliminato la nota "${item.title || item.summary}".`, ''); }
1656
+ catch (e) { return auditAndReturn('note_delete', false, `Errore: ${e.message}`, ''); }
1657
+ }
1658
+ if (kind === 'reminder' && item.id) {
1659
+ try { await executeTool('reminder_cancel', { id: item.id }, config); return auditAndReturn('reminder_cancel', true, `Ho cancellato il promemoria "${item.message || item.summary}".`, ''); }
1660
+ catch (e) { return auditAndReturn('reminder_cancel', false, `Errore: ${e.message}`, ''); }
1661
+ }
1662
+ if (kind === 'gtask' && (item.id || item.taskId)) {
1663
+ try { await executeTool('gtask_delete', { id: item.id || item.taskId }, config); return auditAndReturn('gtask_delete', true, `Ho eliminato il task Google "${item.title || item.summary}".`, ''); }
1664
+ catch (e) { return auditAndReturn('gtask_delete', false, `Errore: ${e.message}`, ''); }
1665
+ }
1666
+ }
1667
+
1668
+ // ── COMPLETE family (tasks) ──────────────────────────────────────────
1669
+ if (effective === 'complete') {
1670
+ if (kind === 'task' && item.id) {
1671
+ try { await executeTool('task_done', { id: item.id }, config); return auditAndReturn('task_done', true, `Ho completato il task "${item.description || item.summary}".`, ''); }
1672
+ catch (e) { return auditAndReturn('task_done', false, `Errore: ${e.message}`, ''); }
1673
+ }
1674
+ if (kind === 'gtask' && (item.id || item.taskId)) {
1675
+ try { await executeTool('gtask_complete', { id: item.id || item.taskId }, config); return auditAndReturn('gtask_complete', true, `Ho completato il task Google "${item.title || item.summary}".`, ''); }
1676
+ catch (e) { return auditAndReturn('gtask_complete', false, `Errore: ${e.message}`, ''); }
1677
+ }
1678
+ }
1679
+
1680
+ // ── OPEN/READ family ─────────────────────────────────────────────────
1681
+ if (effective === 'open') {
1682
+ if (kind === 'email' && (item.messageId || item.id)) {
1683
+ try { const out = await executeTool('gmail_read', { messageId: item.messageId || item.id }, config); return { action: 'gmail_read', success: true, message: String(out) }; }
1684
+ catch (e) { return { action: 'gmail_read', success: false, message: `Errore: ${e.message}` }; }
1685
+ }
1686
+ if (kind === 'drive' && (item.fileId || item.id)) {
1687
+ try { const out = await executeTool('drive_read', { fileId: item.fileId || item.id }, config); return { action: 'drive_read', success: true, message: String(out) }; }
1688
+ catch (e) { return { action: 'drive_read', success: false, message: `Errore: ${e.message}` }; }
1689
+ }
1690
+ if (kind === 'note' && item.id) {
1691
+ try { const out = await executeTool('note_read', { id: item.id }, config); return { action: 'note_read', success: true, message: String(out) }; }
1692
+ catch (e) { return { action: 'note_read', success: false, message: `Errore: ${e.message}` }; }
1693
+ }
1694
+ }
1695
+
1696
+ // ── REPLY family (email only) ────────────────────────────────────────
1697
+ if (effective === 'reply' && kind === 'email' && (item.messageId || item.id)) {
1698
+ // We don't know the reply body yet — return a prompt so the LLM can
1699
+ // ask the user. Tag the message so the next turn knows context.
1700
+ return {
1701
+ action: 'gmail_reply_pending', success: true,
1702
+ message: `Sto per rispondere a "${item.subject || item.from}". Scrivi il testo della risposta e procedo.`,
1703
+ };
1704
+ }
1705
+
1706
+ // Unknown verb+kind combination → fall back to the regular handlers.
1707
+ return null;
1708
+ }
1709
+
1489
1710
  /**
1490
1711
  * Map a list-tool invocation to the (timeMin, timeMax) range that listEvents
1491
1712
  * would query. Used as a fallback when the textual tool output doesn't
@@ -1502,7 +1723,14 @@ class TelegramResponder {
1502
1723
  return { from, to: new Date(from.getTime() + 86400000) };
1503
1724
  }
1504
1725
  if (toolName === 'calendar_week') {
1505
- const from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1726
+ // Respect optional startDate (e.g. for "settimana prossima").
1727
+ let from;
1728
+ if (args?.startDate && /^\d{4}-\d{2}-\d{2}$/.test(args.startDate)) {
1729
+ const [yy, mm, dd] = args.startDate.split('-').map(n => parseInt(n, 10));
1730
+ from = new Date(yy, mm - 1, dd);
1731
+ } else {
1732
+ from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1733
+ }
1506
1734
  return { from, to: new Date(from.getTime() + 7 * 86400000) };
1507
1735
  }
1508
1736
  if (toolName === 'calendar_month') {
@@ -2345,6 +2573,26 @@ class TelegramResponder {
2345
2573
  return await runListAndRemember('calendar_today', {}, 'calendar_today');
2346
2574
  if (/\b(domani|tomorrow)\b/.test(lower))
2347
2575
  return await runListAndRemember('calendar_tomorrow', {}, 'calendar_tomorrow');
2576
+ // "settimana prossima / next week / settimana che viene" → calendar_week
2577
+ // starting next Monday. Without this offset, calendar_week always shows
2578
+ // the CURRENT week, which is wrong for "settimana prossima" and lets
2579
+ // the LLM hallucinate a fake list of upcoming events.
2580
+ if (/\b(settimana\s+(prossima|che\s+viene|seguente)|next\s+week|prossima\s+settimana)\b/.test(lower)) {
2581
+ const today = new Date();
2582
+ const dayOfWeek = today.getDay(); // 0=sun..6=sat
2583
+ const daysUntilMonday = ((1 - dayOfWeek + 7) % 7) || 7;
2584
+ const nextMonday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + daysUntilMonday);
2585
+ const startDate = nextMonday.toISOString().slice(0, 10);
2586
+ return await runListAndRemember('calendar_week', { startDate }, 'calendar_week_next');
2587
+ }
2588
+ if (/\b(settimana\s+scorsa|last\s+week|scorsa\s+settimana)\b/.test(lower)) {
2589
+ const today = new Date();
2590
+ const dayOfWeek = today.getDay();
2591
+ const daysToLastMonday = (dayOfWeek === 0 ? 6 : dayOfWeek - 1) + 7;
2592
+ const lastMonday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - daysToLastMonday);
2593
+ const startDate = lastMonday.toISOString().slice(0, 10);
2594
+ return await runListAndRemember('calendar_week', { startDate }, 'calendar_week_last');
2595
+ }
2348
2596
  if (/\b(settimana|week|questa\s+settimana|this\s+week)\b/.test(lower))
2349
2597
  return await runListAndRemember('calendar_week', {}, 'calendar_week');
2350
2598
  const monthMatch = lower.match(new RegExp(`\\b(${Object.keys(MONTH_MAP).join('|')})(?:\\s+(20\\d{2}))?\\b`));
@@ -2565,6 +2813,26 @@ export async function tryDirectActionAll(text, config, opts = {}) {
2565
2813
  const h = _getDirectHandler();
2566
2814
  if (opts.auditKey) h._lastDirectAuditChatId = opts.auditKey;
2567
2815
  if (opts.log) h.log = opts.log;
2816
+
2817
+ // ── UNIVERSAL ANAPHORIC DISPATCHER (v16.0.16) ──
2818
+ // Intercept anaphoric / yes-confirm commands BEFORE any sub-handler. Resolves
2819
+ // the referent from the most recent list (any kind) and executes the right
2820
+ // tool deterministically. Stops the LLM from running fake-actions.
2821
+ const anaphor = h._detectAnaphoricAction ? h._detectAnaphoricAction(text) : null;
2822
+ if (anaphor) {
2823
+ const resolved = h._resolveAnaphoric ? h._resolveAnaphoric(null, text) : null;
2824
+ if (resolved && resolved.item) {
2825
+ try {
2826
+ const result = await h._executeAnaphoricVerb(anaphor, resolved.kind, resolved.item, text, config);
2827
+ if (result) return result;
2828
+ } catch (e) {
2829
+ h.log && h.log(`[direct] anaphoric universal dispatcher error: ${e.message}`);
2830
+ }
2831
+ } else {
2832
+ h.log && h.log(`[direct] anaphoric verb=${anaphor} but no item to resolve`);
2833
+ }
2834
+ }
2835
+
2568
2836
  return await h._tryDirectFreshCalendarAction(text, config)
2569
2837
  || await h._tryDirectFreshEmailAction(text, config)
2570
2838
  || await h._tryDirectFreshTaskAction(text, config)
@@ -1386,6 +1386,169 @@ function isPlaceholderEventId(id) {
1386
1386
  return false;
1387
1387
  }
1388
1388
 
1389
+ /**
1390
+ * Wrapper around executeTool that ALSO persists structured items for
1391
+ * any list-tool, so the anaphoric resolver in message-responder can later
1392
+ * map "cancellalo / il primo / aprilo" to the correct item ID.
1393
+ * Use this from chat.mjs / message-responder.mjs to get free "memory"
1394
+ * across turns and channels.
1395
+ */
1396
+ export async function executeToolAndRemember(action, params, config, chatId) {
1397
+ const result = await executeTool(action, params, config);
1398
+ try { await _maybeRememberList(action, params, result, config, chatId); } catch {}
1399
+ return result;
1400
+ }
1401
+
1402
+ async function _maybeRememberList(action, params, result, config, chatId) {
1403
+ if (!action) return;
1404
+ const { rememberList } = await import('./list-cache.mjs');
1405
+ // ── CALENDAR list tools — call listEvents directly for structured IDs ──
1406
+ if (['calendar_today', 'calendar_tomorrow', 'calendar_week', 'calendar_month', 'calendar_date', 'calendar_upcoming'].includes(action)) {
1407
+ try {
1408
+ const { listEvents } = await import('./google-calendar.mjs');
1409
+ const now = new Date();
1410
+ let from, to;
1411
+ if (action === 'calendar_today') { from = new Date(now.getFullYear(), now.getMonth(), now.getDate()); to = new Date(from.getTime() + 86400000); }
1412
+ else if (action === 'calendar_tomorrow') { from = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); to = new Date(from.getTime() + 86400000); }
1413
+ else if (action === 'calendar_week') { from = new Date(now.getFullYear(), now.getMonth(), now.getDate()); to = new Date(from.getTime() + 7 * 86400000); }
1414
+ else if (action === 'calendar_month') {
1415
+ let y = now.getFullYear(), m = now.getMonth();
1416
+ if (params?.month && /^\d{4}-\d{2}$/.test(params.month)) { const [yy, mm] = params.month.split('-'); y = parseInt(yy, 10); m = parseInt(mm, 10) - 1; }
1417
+ from = new Date(y, m, 1); to = new Date(y, m + 1, 1);
1418
+ }
1419
+ else if (action === 'calendar_date' && params?.date) { const [yy, mm, dd] = params.date.split('-').map(n => parseInt(n, 10)); from = new Date(yy, mm - 1, dd); to = new Date(from.getTime() + 86400000); }
1420
+ else if (action === 'calendar_upcoming') { const h = parseInt(params?.hours || '48', 10); from = now; to = new Date(now.getTime() + h * 3600000); }
1421
+ if (from && to) {
1422
+ const evs = await listEvents(config, 'primary', from, to);
1423
+ const items = (evs || []).map(e => ({ eventId: e.id, id: e.id, summary: e.summary || '(senza titolo)', time: (e.start || '').slice(11, 16), date: (e.start || '').slice(0, 10) }));
1424
+ rememberList(chatId, 'calendar', items);
1425
+ }
1426
+ } catch {}
1427
+ return;
1428
+ }
1429
+ // ── EMAIL list tools ──
1430
+ if (['gmail_list', 'gmail_search', 'gmail_unread', 'email_search', 'email_list'].includes(action)) {
1431
+ try {
1432
+ const { listMessages, getMessage } = await import('./google-gmail.mjs');
1433
+ const query = params?.query || (action === 'gmail_unread' ? 'is:unread' : 'in:inbox');
1434
+ const refs = await listMessages(config, query, params?.maxResults || 10);
1435
+ const items = [];
1436
+ for (const ref of refs.slice(0, 10)) {
1437
+ try { const m = await getMessage(config, ref.id); items.push({ messageId: m.id, id: m.id, subject: m.subject, from: m.from, date: m.date }); }
1438
+ catch {}
1439
+ }
1440
+ rememberList(chatId, 'email', items);
1441
+ } catch {}
1442
+ return;
1443
+ }
1444
+ // ── TASK list (internal NHA tasks) ──
1445
+ if (action === 'task_list') {
1446
+ try {
1447
+ const { getTasks } = await import('./tasks.mjs');
1448
+ const items = (getTasks() || []).map(t => ({ id: t.id, description: t.description, priority: t.priority, due: t.due }));
1449
+ rememberList(chatId, 'task', items);
1450
+ } catch {}
1451
+ return;
1452
+ }
1453
+ // ── GOOGLE TASKS list ──
1454
+ if (action === 'gtask_list') {
1455
+ try {
1456
+ const { listGTasks } = await import('./google-tasks.mjs').catch(() => ({}));
1457
+ if (listGTasks) {
1458
+ const tasks = await listGTasks(config, params?.listId);
1459
+ const items = (tasks || []).map(t => ({ id: t.id, taskId: t.id, title: t.title, due: t.due }));
1460
+ rememberList(chatId, 'gtask', items);
1461
+ }
1462
+ } catch {}
1463
+ return;
1464
+ }
1465
+ // ── CONTACT search ──
1466
+ if (action === 'contact_search' || action === 'contact_list') {
1467
+ try {
1468
+ const { searchContacts, listContacts } = await import('./google-contacts.mjs').catch(() => ({}));
1469
+ const fn = action === 'contact_search' ? searchContacts : listContacts;
1470
+ if (fn) {
1471
+ const arr = await fn(config, params?.query || '');
1472
+ const items = (arr || []).map(c => ({ id: c.resourceName || c.id, name: c.name || c.displayName, email: c.email }));
1473
+ rememberList(chatId, 'contact', items);
1474
+ }
1475
+ } catch {}
1476
+ return;
1477
+ }
1478
+ // ── DRIVE list ──
1479
+ if (action === 'drive_list' || action === 'drive_search') {
1480
+ try {
1481
+ const { listDriveFiles } = await import('./google-drive.mjs').catch(() => ({}));
1482
+ if (listDriveFiles) {
1483
+ const files = await listDriveFiles(config, params?.folderId || null, params?.query || '');
1484
+ const items = (files || []).map(f => ({ fileId: f.id, id: f.id, name: f.name, mimeType: f.mimeType }));
1485
+ rememberList(chatId, 'drive', items);
1486
+ }
1487
+ } catch {}
1488
+ return;
1489
+ }
1490
+ // ── NOTE list ──
1491
+ if (action === 'note_list') {
1492
+ try {
1493
+ const { getNotes } = await import('./notes.mjs').catch(() => ({}));
1494
+ if (getNotes) {
1495
+ const notes = getNotes();
1496
+ const items = (notes || []).map(n => ({ id: n.id, title: n.title, body: n.body?.slice(0, 200) }));
1497
+ rememberList(chatId, 'note', items);
1498
+ }
1499
+ } catch {}
1500
+ return;
1501
+ }
1502
+ // ── REMINDER list ──
1503
+ if (action === 'reminder_list') {
1504
+ try {
1505
+ const { listReminders } = await import('./reminders.mjs').catch(() => ({}));
1506
+ if (listReminders) {
1507
+ const arr = listReminders();
1508
+ const items = (arr || []).map(r => ({ id: r.id, message: r.message, when: r.when }));
1509
+ rememberList(chatId, 'reminder', items);
1510
+ }
1511
+ } catch {}
1512
+ return;
1513
+ }
1514
+ // ── NOTION search ──
1515
+ if (action === 'notion_search') {
1516
+ try {
1517
+ // Notion search returns pages — we parse from the textual result as
1518
+ // a best-effort, falling back to whatever ID hints the result contains.
1519
+ const m = String(result).match(/[a-f0-9]{32}|[a-f0-9-]{36}/g);
1520
+ if (m && m.length > 0) {
1521
+ const items = m.slice(0, 10).map(id => ({ id, pageId: id }));
1522
+ rememberList(chatId, 'notion', items);
1523
+ }
1524
+ } catch {}
1525
+ return;
1526
+ }
1527
+ // ── SLACK search ──
1528
+ if (action === 'slack_search') {
1529
+ try {
1530
+ const m = String(result).match(/\b(\d{10}\.\d{6})\b/g);
1531
+ if (m && m.length > 0) {
1532
+ const items = m.slice(0, 10).map(ts => ({ id: ts, ts }));
1533
+ rememberList(chatId, 'slack', items);
1534
+ }
1535
+ } catch {}
1536
+ return;
1537
+ }
1538
+ // ── GITHUB issue/PR list ──
1539
+ if (action === 'github_list_issues' || action === 'github_issues' || action === 'github_prs' || action === 'github_pulls') {
1540
+ try {
1541
+ // Best effort from result text: lines like "#123 Title"
1542
+ const matches = [...String(result).matchAll(/#(\d+)\s+(.+)/g)];
1543
+ if (matches.length > 0) {
1544
+ const items = matches.slice(0, 20).map(m => ({ id: m[1], number: parseInt(m[1], 10), title: m[2].trim() }));
1545
+ rememberList(chatId, 'github', items);
1546
+ }
1547
+ } catch {}
1548
+ return;
1549
+ }
1550
+ }
1551
+
1389
1552
  export async function executeTool(action, params, config) {
1390
1553
  switch (action) {
1391
1554
  // ── Gmail ──────────────────────────────────────────────────────────────