nothumanallowed 16.0.14 → 16.0.16

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.14",
3
+ "version": "16.0.16",
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.14';
8
+ export const VERSION = '16.0.16';
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
+ }
@@ -1486,6 +1486,244 @@ class TelegramResponder {
1486
1486
  // Parse calendar_date / calendar_find tool output. The executor returns
1487
1487
  // a human-readable string with each event on its own line plus the
1488
1488
  // eventId in parentheses. We extract structured records.
1489
+ // ── Generic LIST→REMEMBER + ANAPHORIC RESOLUTION (v16.0.16) ────────────
1490
+ // Same pattern as the calendar fix, applied uniformly to every list-tool:
1491
+ // email, task, contact, drive, note, reminder, gtask, notion.
1492
+ // Why: a single regex parser can never handle every tool's ID format.
1493
+ // The cleanest path is to call the low-level API directly and persist
1494
+ // structured items, then resolve anaphoric references generically.
1495
+ // Open allowlist of kinds — no hardcoded restriction. Any new tool can
1496
+ // call _rememberItems(<any-kind-string>, items) without changing this file.
1497
+ _propForKind(kind) { return `lastList_${kind}`; }
1498
+
1499
+ _rememberItems(kind, items, extra = {}) {
1500
+ if (!kind || !Array.isArray(items)) return;
1501
+ const chatId = this._lastDirectAuditChatId || '__last_list__';
1502
+ const prop = this._propForKind(kind);
1503
+ const prev = this._lastContextByChatId[chatId] || {};
1504
+ this._lastContextByChatId[chatId] = {
1505
+ ...prev,
1506
+ [prop]: items,
1507
+ [`${prop}At`]: Date.now(),
1508
+ lastListKind: kind,
1509
+ lastListAt: Date.now(),
1510
+ ...extra,
1511
+ };
1512
+ this.log(`[direct] ${kind} LIST stored: chatId=${chatId} count=${items.length}`);
1513
+ try { saveTelegramContext(this._lastContextByChatId); } catch {}
1514
+ }
1515
+
1516
+ /**
1517
+ * Resolve an anaphoric reference ("cancellalo", "il primo", "l'ultimo",
1518
+ * "il numero 2") against the most recently listed items of a given kind.
1519
+ * If kind is omitted, falls back to lastListKind (the most recently listed
1520
+ * type — calendar after calendar_today, email after gmail_list, etc).
1521
+ * Cross-key fallback included.
1522
+ */
1523
+ _resolveAnaphoric(kind, userMessage) {
1524
+ const chatId = this._lastDirectAuditChatId;
1525
+ // Use the shared list-cache module as source of truth — it's populated
1526
+ // by executeToolAndRemember from every channel.
1527
+ let items = [];
1528
+ try {
1529
+ const cacheFile = path.join(os.homedir(), '.nha', 'list-cache.json');
1530
+ let cache = {};
1531
+ if (fs.existsSync(cacheFile)) {
1532
+ try { cache = JSON.parse(fs.readFileSync(cacheFile, 'utf-8')); } catch { cache = {}; }
1533
+ }
1534
+ if (!kind) {
1535
+ // Auto-pick the freshest list across any chat.
1536
+ const direct = chatId && cache[chatId];
1537
+ if (direct?.lastListKind) kind = direct.lastListKind;
1538
+ else {
1539
+ let bestKind = null, bestAt = 0;
1540
+ for (const v of Object.values(cache)) {
1541
+ if (v?.lastListKind && (v.lastListAt || 0) > bestAt) { bestKind = v.lastListKind; bestAt = v.lastListAt; }
1542
+ }
1543
+ kind = bestKind;
1544
+ }
1545
+ }
1546
+ if (!kind) return null;
1547
+ const prop = `lastList_${kind}`;
1548
+ items = (chatId && cache[chatId]?.[prop]) || [];
1549
+ if (items.length === 0) {
1550
+ let bestArr = null, bestAt = 0;
1551
+ for (const v of Object.values(cache)) {
1552
+ if (Array.isArray(v?.[prop]) && v[prop].length > 0 && (v[`${prop}_at`] || 0) > bestAt) {
1553
+ bestArr = v[prop]; bestAt = v[`${prop}_at`] || 0;
1554
+ }
1555
+ }
1556
+ if (bestArr) items = bestArr;
1557
+ }
1558
+ } catch {}
1559
+ if (items.length === 0) return null;
1560
+ const low = (userMessage || '').toLowerCase();
1561
+ const ordinalMap = { primo: 0, prima: 0, secondo: 1, seconda: 1, terzo: 2, terza: 2, quarto: 3, quinto: 4, first: 0, second: 1, third: 2 };
1562
+ for (const [word, idx] of Object.entries(ordinalMap)) {
1563
+ if (new RegExp(`\\b${word}\\b`).test(low) && items[idx]) return { item: items[idx], kind };
1564
+ }
1565
+ if (/\b(ultim[oa]|last)\b/.test(low)) return { item: items[items.length - 1], kind };
1566
+ const numMatch = low.match(/\b(?:numero|number|n\.?|#)\s*(\d+)\b/);
1567
+ if (numMatch) {
1568
+ const idx = parseInt(numMatch[1], 10) - 1;
1569
+ if (items[idx]) return { item: items[idx], kind };
1570
+ }
1571
+ if (items.length === 1) return { item: items[0], kind };
1572
+ return null;
1573
+ }
1574
+
1575
+ /**
1576
+ * Detect a generic anaphoric DELETE/COMPLETE/REPLY/OPEN command.
1577
+ * Returns the matched action verb or null.
1578
+ */
1579
+ _detectAnaphoricAction(userMessage) {
1580
+ const t = (userMessage || '').trim();
1581
+ 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';
1584
+ 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';
1586
+ 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';
1588
+ return null;
1589
+ }
1590
+
1591
+ /**
1592
+ * Execute an anaphoric verb (delete/complete/reply/open/confirm) against
1593
+ * an item resolved by _resolveAnaphoric. The (verb, kind) pair maps to a
1594
+ * concrete tool call. Unknown combinations return null (fall through to
1595
+ * the LLM). All executions persist to the global audit log.
1596
+ */
1597
+ async _executeAnaphoricVerb(verb, kind, item, userText, config) {
1598
+ const { executeTool } = await import('./tool-executor.mjs');
1599
+ const chatId = this._lastDirectAuditChatId;
1600
+ const auditAndReturn = (toolName, success, message, summary) => {
1601
+ if (chatId) this._recordAudit(chatId, { tool: toolName, success, summary });
1602
+ return { action: toolName, success, message };
1603
+ };
1604
+
1605
+ // confirm = treat as the pending action (default: delete) when there's
1606
+ // a single recently-listed item.
1607
+ const effective = verb === 'confirm' ? 'delete' : verb;
1608
+
1609
+ // ── DELETE family ─────────────────────────────────────────────────────
1610
+ if (effective === 'delete') {
1611
+ if (kind === 'calendar' && item.eventId) {
1612
+ try { await executeTool('calendar_delete', { eventId: item.eventId }, config); return auditAndReturn('calendar_delete', true, `Ho cancellato "${item.summary}".`, item.summary); }
1613
+ catch (e) { return auditAndReturn('calendar_delete', false, `Errore: ${e.message}`, ''); }
1614
+ }
1615
+ if (kind === 'email' && (item.messageId || item.id)) {
1616
+ 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 || ''); }
1617
+ catch (e) { return auditAndReturn('gmail_delete', false, `Errore: ${e.message}`, ''); }
1618
+ }
1619
+ if (kind === 'task' && item.id) {
1620
+ try { await executeTool('task_delete', { id: item.id }, config); return auditAndReturn('task_delete', true, `Ho eliminato il task "${item.description || item.summary}".`, ''); }
1621
+ catch (e) { return auditAndReturn('task_delete', false, `Errore: ${e.message}`, ''); }
1622
+ }
1623
+ if (kind === 'contact' && item.id) {
1624
+ try { await executeTool('contact_delete', { id: item.id }, config); return auditAndReturn('contact_delete', true, `Ho eliminato il contatto "${item.name || item.summary}".`, ''); }
1625
+ catch (e) { return auditAndReturn('contact_delete', false, `Errore: ${e.message}`, ''); }
1626
+ }
1627
+ if (kind === 'drive' && (item.fileId || item.id)) {
1628
+ try { await executeTool('drive_delete', { fileId: item.fileId || item.id }, config); return auditAndReturn('drive_delete', true, `Ho eliminato il file "${item.name || item.summary}".`, ''); }
1629
+ catch (e) { return auditAndReturn('drive_delete', false, `Errore: ${e.message}`, ''); }
1630
+ }
1631
+ if (kind === 'note' && item.id) {
1632
+ try { await executeTool('note_delete', { id: item.id }, config); return auditAndReturn('note_delete', true, `Ho eliminato la nota "${item.title || item.summary}".`, ''); }
1633
+ catch (e) { return auditAndReturn('note_delete', false, `Errore: ${e.message}`, ''); }
1634
+ }
1635
+ if (kind === 'reminder' && item.id) {
1636
+ try { await executeTool('reminder_cancel', { id: item.id }, config); return auditAndReturn('reminder_cancel', true, `Ho cancellato il promemoria "${item.message || item.summary}".`, ''); }
1637
+ catch (e) { return auditAndReturn('reminder_cancel', false, `Errore: ${e.message}`, ''); }
1638
+ }
1639
+ if (kind === 'gtask' && (item.id || item.taskId)) {
1640
+ 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}".`, ''); }
1641
+ catch (e) { return auditAndReturn('gtask_delete', false, `Errore: ${e.message}`, ''); }
1642
+ }
1643
+ }
1644
+
1645
+ // ── COMPLETE family (tasks) ──────────────────────────────────────────
1646
+ if (effective === 'complete') {
1647
+ if (kind === 'task' && item.id) {
1648
+ try { await executeTool('task_done', { id: item.id }, config); return auditAndReturn('task_done', true, `Ho completato il task "${item.description || item.summary}".`, ''); }
1649
+ catch (e) { return auditAndReturn('task_done', false, `Errore: ${e.message}`, ''); }
1650
+ }
1651
+ if (kind === 'gtask' && (item.id || item.taskId)) {
1652
+ 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}".`, ''); }
1653
+ catch (e) { return auditAndReturn('gtask_complete', false, `Errore: ${e.message}`, ''); }
1654
+ }
1655
+ }
1656
+
1657
+ // ── OPEN/READ family ─────────────────────────────────────────────────
1658
+ if (effective === 'open') {
1659
+ if (kind === 'email' && (item.messageId || item.id)) {
1660
+ try { const out = await executeTool('gmail_read', { messageId: item.messageId || item.id }, config); return { action: 'gmail_read', success: true, message: String(out) }; }
1661
+ catch (e) { return { action: 'gmail_read', success: false, message: `Errore: ${e.message}` }; }
1662
+ }
1663
+ if (kind === 'drive' && (item.fileId || item.id)) {
1664
+ try { const out = await executeTool('drive_read', { fileId: item.fileId || item.id }, config); return { action: 'drive_read', success: true, message: String(out) }; }
1665
+ catch (e) { return { action: 'drive_read', success: false, message: `Errore: ${e.message}` }; }
1666
+ }
1667
+ if (kind === 'note' && item.id) {
1668
+ try { const out = await executeTool('note_read', { id: item.id }, config); return { action: 'note_read', success: true, message: String(out) }; }
1669
+ catch (e) { return { action: 'note_read', success: false, message: `Errore: ${e.message}` }; }
1670
+ }
1671
+ }
1672
+
1673
+ // ── REPLY family (email only) ────────────────────────────────────────
1674
+ if (effective === 'reply' && kind === 'email' && (item.messageId || item.id)) {
1675
+ // We don't know the reply body yet — return a prompt so the LLM can
1676
+ // ask the user. Tag the message so the next turn knows context.
1677
+ return {
1678
+ action: 'gmail_reply_pending', success: true,
1679
+ message: `Sto per rispondere a "${item.subject || item.from}". Scrivi il testo della risposta e procedo.`,
1680
+ };
1681
+ }
1682
+
1683
+ // Unknown verb+kind combination → fall back to the regular handlers.
1684
+ return null;
1685
+ }
1686
+
1687
+ /**
1688
+ * Map a list-tool invocation to the (timeMin, timeMax) range that listEvents
1689
+ * would query. Used as a fallback when the textual tool output doesn't
1690
+ * include event IDs (calendar_month, calendar_today, etc).
1691
+ */
1692
+ _computeRangeForListTool(toolName, args) {
1693
+ const now = new Date();
1694
+ if (toolName === 'calendar_today') {
1695
+ const from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1696
+ return { from, to: new Date(from.getTime() + 86400000) };
1697
+ }
1698
+ if (toolName === 'calendar_tomorrow') {
1699
+ const from = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
1700
+ return { from, to: new Date(from.getTime() + 86400000) };
1701
+ }
1702
+ if (toolName === 'calendar_week') {
1703
+ const from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1704
+ return { from, to: new Date(from.getTime() + 7 * 86400000) };
1705
+ }
1706
+ if (toolName === 'calendar_month') {
1707
+ let y = now.getFullYear(), m = now.getMonth();
1708
+ if (args?.month && /^\d{4}-\d{2}$/.test(args.month)) {
1709
+ const [yy, mm] = args.month.split('-');
1710
+ y = parseInt(yy, 10);
1711
+ m = parseInt(mm, 10) - 1;
1712
+ }
1713
+ return { from: new Date(y, m, 1), to: new Date(y, m + 1, 1) };
1714
+ }
1715
+ if (toolName === 'calendar_date' && args?.date && /^\d{4}-\d{2}-\d{2}$/.test(args.date)) {
1716
+ const [yy, mm, dd] = args.date.split('-').map(n => parseInt(n, 10));
1717
+ const from = new Date(yy, mm - 1, dd);
1718
+ return { from, to: new Date(from.getTime() + 86400000) };
1719
+ }
1720
+ if (toolName === 'calendar_upcoming') {
1721
+ const hours = parseInt(args?.hours || '48', 10);
1722
+ return { from: now, to: new Date(now.getTime() + hours * 3600000) };
1723
+ }
1724
+ return null;
1725
+ }
1726
+
1489
1727
  _parseEventsFromToolOutput(toolResult) {
1490
1728
  const text = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult || '');
1491
1729
  const lines = text.split(/\r?\n/);
@@ -2263,7 +2501,28 @@ class TelegramResponder {
2263
2501
  const runListAndRemember = async (toolName, args, actionKey) => {
2264
2502
  try {
2265
2503
  const out = await executeTool(toolName, args, config);
2266
- const events = this._parseEventsFromToolOutput(String(out));
2504
+ // Parse from text first (cheap, works when tools include event IDs).
2505
+ let events = this._parseEventsFromToolOutput(String(out));
2506
+ // Fallback: tools like calendar_month don't print event IDs in their
2507
+ // pretty-printed output, so we MUST call listEvents() directly to
2508
+ // get structured objects with real Google Calendar event IDs.
2509
+ // Without this, anaphoric "cancellalo" can never resolve.
2510
+ if (events.length === 0) {
2511
+ try {
2512
+ const { listEvents } = await import('./google-calendar.mjs');
2513
+ const range = this._computeRangeForListTool(toolName, args);
2514
+ if (range) {
2515
+ const evs = await listEvents(config, 'primary', range.from, range.to);
2516
+ events = (evs || []).map(e => ({
2517
+ eventId: e.id,
2518
+ summary: e.summary || '(senza titolo)',
2519
+ time: (e.start || '').slice(11, 16),
2520
+ date: (e.start || '').slice(0, 10),
2521
+ }));
2522
+ this.log(`[direct] LIST structured fallback: ${events.length} events via listEvents(${range.from.toISOString().slice(0,10)}..${range.to.toISOString().slice(0,10)})`);
2523
+ }
2524
+ } catch (e) { this.log(`[direct] LIST structured fallback failed: ${e.message}`); }
2525
+ }
2267
2526
  // Even without chatId, save to a global fallback bucket so the
2268
2527
  // anaphoric resolution can still find it.
2269
2528
  const persistKey = chatId || '__last_list__';
@@ -2504,6 +2763,26 @@ export async function tryDirectActionAll(text, config, opts = {}) {
2504
2763
  const h = _getDirectHandler();
2505
2764
  if (opts.auditKey) h._lastDirectAuditChatId = opts.auditKey;
2506
2765
  if (opts.log) h.log = opts.log;
2766
+
2767
+ // ── UNIVERSAL ANAPHORIC DISPATCHER (v16.0.16) ──
2768
+ // Intercept anaphoric / yes-confirm commands BEFORE any sub-handler. Resolves
2769
+ // the referent from the most recent list (any kind) and executes the right
2770
+ // tool deterministically. Stops the LLM from running fake-actions.
2771
+ const anaphor = h._detectAnaphoricAction ? h._detectAnaphoricAction(text) : null;
2772
+ if (anaphor) {
2773
+ const resolved = h._resolveAnaphoric ? h._resolveAnaphoric(null, text) : null;
2774
+ if (resolved && resolved.item) {
2775
+ try {
2776
+ const result = await h._executeAnaphoricVerb(anaphor, resolved.kind, resolved.item, text, config);
2777
+ if (result) return result;
2778
+ } catch (e) {
2779
+ h.log && h.log(`[direct] anaphoric universal dispatcher error: ${e.message}`);
2780
+ }
2781
+ } else {
2782
+ h.log && h.log(`[direct] anaphoric verb=${anaphor} but no item to resolve`);
2783
+ }
2784
+ }
2785
+
2507
2786
  return await h._tryDirectFreshCalendarAction(text, config)
2508
2787
  || await h._tryDirectFreshEmailAction(text, config)
2509
2788
  || 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 ──────────────────────────────────────────────────────────────