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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
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 ──────────────────────────────────────────────────────────────
|