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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 ──────────────────────────────────────────────────────────────
|