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