nothumanallowed 15.1.61 → 15.1.63

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": "15.1.61",
3
+ "version": "15.1.63",
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 = '15.1.61';
8
+ export const VERSION = '15.1.63';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { callAgent, callLLM } from './llm.mjs';
12
- import { buildSystemPrompt, parseActions, executeTool, TOOL_DEFINITIONS, LIARA_TOOL_DEFINITIONS } from './tool-executor.mjs';
12
+ import { buildSystemPrompt, parseActions, executeTool, TOOL_DEFINITIONS, LIARA_TOOL_DEFINITIONS, DESTRUCTIVE_ACTIONS } from './tool-executor.mjs';
13
13
  import https from 'https';
14
14
  import http from 'http';
15
15
  import { URL } from 'url';
@@ -1045,7 +1045,19 @@ class TelegramResponder {
1045
1045
  // appointments of May". By running the calendar tool server-side, the
1046
1046
  // user always sees REAL data — never fabricated.
1047
1047
  this._lastDirectAuditChatId = chatId;
1048
- const directFresh = await this._tryDirectFreshCalendarAction(cleanText, this.config);
1048
+ // Run the per-domain direct-action dispatcher. First match wins; falls
1049
+ // through to LLM if no handler claims the message.
1050
+ // Fast-path specialised handlers (regex-driven, lower latency for the
1051
+ // common cases), then the universal dispatcher that covers ALL 50+
1052
+ // mutation tools via a single LLM-NLU+deterministic-execute pass.
1053
+ const directFresh =
1054
+ await this._tryDirectFreshCalendarAction(cleanText, this.config) ||
1055
+ await this._tryDirectFreshEmailAction(cleanText, this.config) ||
1056
+ await this._tryDirectFreshTaskAction(cleanText, this.config) ||
1057
+ await this._tryDirectFreshNoteAction(cleanText, this.config) ||
1058
+ await this._tryDirectFreshReminderAction(cleanText, this.config) ||
1059
+ await this._tryDirectFreshSlackAction(cleanText, this.config) ||
1060
+ await this._tryDirectFreshUniversalAction(cleanText, this.config);
1049
1061
  if (directFresh) {
1050
1062
  this.log(`[Telegram] ${fromUser}: direct-fresh ${directFresh.action} → ${directFresh.success ? 'OK' : 'FAIL'}`);
1051
1063
  const personaName = this.config.responder?.telegram?.botName || this.config.responder?.botName || '';
@@ -1407,6 +1419,429 @@ class TelegramResponder {
1407
1419
  return `${parseInt(m[3], 10)} ${months[parseInt(m[2], 10) - 1]} ${m[1]}`;
1408
1420
  }
1409
1421
 
1422
+ /** Add N minutes to an ISO datetime string. Returns ISO. */
1423
+ _addMinutesIso(isoStart, minutes) {
1424
+ try {
1425
+ const d = new Date(isoStart);
1426
+ d.setMinutes(d.getMinutes() + minutes);
1427
+ return d.toISOString();
1428
+ } catch { return isoStart; }
1429
+ }
1430
+
1431
+ /**
1432
+ * NLU only — extract structured calendar-create params from a natural
1433
+ * language message using a tiny LLM call. We pin output to strict JSON and
1434
+ * accept the result only if the title + start datetime are present.
1435
+ * The LLM NEVER executes anything — it only parses.
1436
+ */
1437
+ async _nluExtractCalendarCreate(userMessage, config) {
1438
+ const todayIso = new Date().toISOString().slice(0, 10);
1439
+ const sysPrompt =
1440
+ `You extract calendar event parameters from natural language.\n` +
1441
+ `Today is ${todayIso}. Output ONLY a JSON object with these keys:\n` +
1442
+ ` - title (string, the event subject, NOT including verbs like "fissa"/"crea")\n` +
1443
+ ` - start (string, ISO datetime "YYYY-MM-DDTHH:MM:00" in local time)\n` +
1444
+ ` - end (string, ISO datetime same format; if duration unknown, leave null and default 60 min will be applied)\n` +
1445
+ ` - description (string, optional notes)\n` +
1446
+ `Rules: if a field is missing, use null. If the user says "domani" → ${this._addDaysIso(todayIso, 1).slice(0, 10)}. ` +
1447
+ `If "lunedì/martedì/..." resolve to the next occurrence. If no time is given default to 09:00. ` +
1448
+ `Output ONLY the JSON, no prose.`;
1449
+ try {
1450
+ const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 200 });
1451
+ const json = this._extractJsonObject(raw);
1452
+ if (!json) return null;
1453
+ if (!json.title || !json.start) return null;
1454
+ // Sanity: start must look like an ISO datetime.
1455
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(json.start)) return null;
1456
+ return json;
1457
+ } catch { return null; }
1458
+ }
1459
+
1460
+ async _nluExtractCalendarMove(userMessage, config) {
1461
+ const todayIso = new Date().toISOString().slice(0, 10);
1462
+ const sysPrompt =
1463
+ `You extract reschedule-event parameters from natural language.\n` +
1464
+ `Today is ${todayIso}. Output ONLY a JSON object:\n` +
1465
+ ` - title (string, the event to find and move)\n` +
1466
+ ` - oldDate (string, original date if mentioned, format "YYYY-MM-DD"; else null)\n` +
1467
+ ` - newStart (string, NEW datetime "YYYY-MM-DDTHH:MM:00")\n` +
1468
+ ` - newEnd (string, optional, same format)\n` +
1469
+ `If "domani" → ${this._addDaysIso(todayIso, 1).slice(0, 10)}. Output JSON only.`;
1470
+ try {
1471
+ const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 200 });
1472
+ const json = this._extractJsonObject(raw);
1473
+ if (!json) return null;
1474
+ if (!json.newStart) return null;
1475
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(json.newStart)) return null;
1476
+ return json;
1477
+ } catch { return null; }
1478
+ }
1479
+
1480
+ /** Robust JSON object extractor — strips fences, finds the first balanced {...}. */
1481
+ _extractJsonObject(text) {
1482
+ if (!text) return null;
1483
+ const cleaned = String(text).replace(/```(?:json)?\s*/gi, '').replace(/```/g, '').trim();
1484
+ const start = cleaned.indexOf('{');
1485
+ if (start < 0) return null;
1486
+ let depth = 0, end = -1, inStr = false, esc = false;
1487
+ for (let i = start; i < cleaned.length; i++) {
1488
+ const c = cleaned[i];
1489
+ if (esc) { esc = false; continue; }
1490
+ if (c === '\\' && inStr) { esc = true; continue; }
1491
+ if (c === '"' && !esc) inStr = !inStr;
1492
+ if (inStr) continue;
1493
+ if (c === '{') depth++;
1494
+ else if (c === '}') { depth--; if (depth === 0) { end = i; break; } }
1495
+ }
1496
+ if (end < 0) return null;
1497
+ try { return JSON.parse(cleaned.slice(start, end + 1)); } catch { return null; }
1498
+ }
1499
+
1500
+ _addDaysIso(isoDate, days) {
1501
+ const d = new Date(isoDate + 'T00:00:00');
1502
+ d.setDate(d.getDate() + days);
1503
+ return d.toISOString();
1504
+ }
1505
+
1506
+ // ── Direct fresh EMAIL action (LLM only for NLU, executor server-side) ──
1507
+ // Detects "manda email a X riguardo Y" style requests, extracts recipient
1508
+ // + subject + body via a tiny LLM call, then sends via gmail_send tool.
1509
+ async _tryDirectFreshEmailAction(userMessage, config) {
1510
+ if (!userMessage || typeof userMessage !== 'string') return null;
1511
+ const lower = userMessage.toLowerCase();
1512
+ const isEmailSend = /\b(manda|mand[oa]|invi[oa]|spedis(ci|co)|scriv[io]|send|email\s+to|mail\s+to)\s+(?:un'?\s+)?(email|mail|messaggio|messag|e-?mail)\s+/i.test(lower)
1513
+ || /\b(invi[oa]r[ea]|mand[ao]|spedire)\s+un'?\s*e[- ]?mail\b/i.test(lower);
1514
+ if (!isEmailSend) return null;
1515
+
1516
+ const parsed = await this._nluExtractEmailSend(userMessage, config);
1517
+ if (!parsed || !parsed.to || !parsed.body) return null;
1518
+
1519
+ const { executeTool } = await import('./tool-executor.mjs');
1520
+ try {
1521
+ const result = await executeTool('gmail_send', {
1522
+ to: parsed.to,
1523
+ subject: parsed.subject || '(no subject)',
1524
+ body: parsed.body,
1525
+ }, config);
1526
+ const ok = typeof result === 'string' && /sent|inviat|✅|message-?id/i.test(result);
1527
+ const message = ok
1528
+ ? `Fatto. Email inviata a ${parsed.to}${parsed.subject ? ` con oggetto "${parsed.subject}"` : ''}.`
1529
+ : `Non sono riuscito a inviare l'email: ${result}`;
1530
+ if (this._lastDirectAuditChatId) {
1531
+ this._recordAudit(this._lastDirectAuditChatId, {
1532
+ tool: 'gmail_send',
1533
+ success: ok,
1534
+ summary: `→ ${parsed.to}${parsed.subject ? ` · "${parsed.subject}"` : ''}`,
1535
+ });
1536
+ }
1537
+ return { action: 'gmail_send', success: ok, message };
1538
+ } catch (e) {
1539
+ return { action: 'gmail_send', success: false, message: `Errore nell'invio: ${e.message}` };
1540
+ }
1541
+ }
1542
+
1543
+ async _nluExtractEmailSend(userMessage, config) {
1544
+ const sysPrompt =
1545
+ `You extract email-send parameters from natural language. Output ONLY a JSON object:\n` +
1546
+ ` - to (string, recipient email or name if email unknown)\n` +
1547
+ ` - subject (string, the email subject; if missing, infer a 2-6 word summary)\n` +
1548
+ ` - body (string, the email body in the same language as the user)\n` +
1549
+ `Rules: if any field is genuinely missing AND impossible to infer, use null. ` +
1550
+ `Keep body short and natural. Output JSON only, no markdown.`;
1551
+ try {
1552
+ const raw = await callLLM(config, sysPrompt, userMessage, { temperature: 0, maxTokens: 400 });
1553
+ const json = this._extractJsonObject(raw);
1554
+ if (!json || !json.to || !json.body) return null;
1555
+ return json;
1556
+ } catch { return null; }
1557
+ }
1558
+
1559
+ // ── Direct fresh TASK action (add / complete) ──────────────────────────
1560
+ async _tryDirectFreshTaskAction(userMessage, config) {
1561
+ if (!userMessage || typeof userMessage !== 'string') return null;
1562
+ const lower = userMessage.toLowerCase();
1563
+ const isTaskAdd = /\b(aggiung|cre[oai]|fiss|segn|metti|add|create)\w*\s+.*\b(task|attivit[àa]|cosa\s+da\s+fare|todo|to[- ]?do|promemoria\s+da\s+fare)/i.test(lower);
1564
+ const isTaskDone = /\b(complet|fatt[oa]|done|fini[stct]|chiud|spunt|tick|mark\s+as\s+done)\w*\s+.*\b(task|attivit[àa]|todo|to[- ]?do)/i.test(lower);
1565
+ const { executeTool } = await import('./tool-executor.mjs');
1566
+ if (isTaskAdd) {
1567
+ const parsed = await this._nluExtractTaskAdd(userMessage, config);
1568
+ if (!parsed?.title) return null;
1569
+ try {
1570
+ const r = await executeTool('task_add', { title: parsed.title, due: parsed.due || null, priority: parsed.priority || 'medium' }, config);
1571
+ const ok = typeof r === 'string' && !/error/i.test(r);
1572
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'task_add', success: ok, summary: `"${parsed.title}"${parsed.due ? ` (entro ${parsed.due})` : ''}` });
1573
+ return { action: 'task_add', success: ok, message: ok ? `Fatto. Ho aggiunto il task "${parsed.title}"${parsed.due ? ` con scadenza ${parsed.due}` : ''}.` : `Errore: ${r}` };
1574
+ } catch (e) { return { action: 'task_add', success: false, message: `Errore: ${e.message}` }; }
1575
+ }
1576
+ if (isTaskDone) {
1577
+ const parsed = await this._nluExtractTaskRef(userMessage, config);
1578
+ if (!parsed?.title) return null;
1579
+ try {
1580
+ const r = await executeTool('task_complete', { title: parsed.title }, config);
1581
+ const ok = typeof r === 'string' && !/error|not found/i.test(r);
1582
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'task_complete', success: ok, summary: `"${parsed.title}"` });
1583
+ return { action: 'task_complete', success: ok, message: ok ? `Fatto. Task "${parsed.title}" segnato come completato.` : `Errore: ${r}` };
1584
+ } catch (e) { return { action: 'task_complete', success: false, message: `Errore: ${e.message}` }; }
1585
+ }
1586
+ return null;
1587
+ }
1588
+
1589
+ // ── Direct fresh NOTE add ───────────────────────────────────────────────
1590
+ async _tryDirectFreshNoteAction(userMessage, config) {
1591
+ if (!userMessage || typeof userMessage !== 'string') return null;
1592
+ const lower = userMessage.toLowerCase();
1593
+ const isNoteAdd = /\b(crea|nuov[oa]|aggiung|salv|prend|scriv|appunt|new|add|save)\w*\s+(?:una\s+)?\b(nota|note|appunt)/i.test(lower);
1594
+ if (!isNoteAdd) return null;
1595
+ const parsed = await this._nluExtractNoteAdd(userMessage, config);
1596
+ if (!parsed?.title) return null;
1597
+ const { executeTool } = await import('./tool-executor.mjs');
1598
+ try {
1599
+ const r = await executeTool('note_add', { title: parsed.title, content: parsed.content || '' }, config);
1600
+ const ok = typeof r === 'string' && !/error/i.test(r);
1601
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'note_add', success: ok, summary: `"${parsed.title}"` });
1602
+ return { action: 'note_add', success: ok, message: ok ? `Fatto. Nota "${parsed.title}" salvata.` : `Errore: ${r}` };
1603
+ } catch (e) { return { action: 'note_add', success: false, message: `Errore: ${e.message}` }; }
1604
+ }
1605
+
1606
+ // ── Direct fresh REMINDER create ────────────────────────────────────────
1607
+ async _tryDirectFreshReminderAction(userMessage, config) {
1608
+ if (!userMessage || typeof userMessage !== 'string') return null;
1609
+ const lower = userMessage.toLowerCase();
1610
+ const isReminder = /\b(ricordami|reminder|promemoria|ricord[aoo]|notificami|avvisami|remind)\b/i.test(lower);
1611
+ if (!isReminder) return null;
1612
+ const parsed = await this._nluExtractReminder(userMessage, config);
1613
+ if (!parsed?.message || !parsed?.when) return null;
1614
+ const { executeTool } = await import('./tool-executor.mjs');
1615
+ try {
1616
+ const r = await executeTool('reminder_create', { message: parsed.message, when: parsed.when }, config);
1617
+ const ok = typeof r === 'string' && !/error/i.test(r);
1618
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'reminder_create', success: ok, summary: `"${parsed.message}" @ ${parsed.when}` });
1619
+ return { action: 'reminder_create', success: ok, message: ok ? `Fatto. Promemoria impostato: "${parsed.message}" per ${parsed.when}.` : `Errore: ${r}` };
1620
+ } catch (e) { return { action: 'reminder_create', success: false, message: `Errore: ${e.message}` }; }
1621
+ }
1622
+
1623
+ // ── Direct fresh SLACK send ─────────────────────────────────────────────
1624
+ async _tryDirectFreshSlackAction(userMessage, config) {
1625
+ if (!userMessage || typeof userMessage !== 'string') return null;
1626
+ const lower = userMessage.toLowerCase();
1627
+ const isSlackSend = /\b(manda|invi[ao]|posta|scriv[io]|send|post)\s+.*\b(slack|canale|channel|#)/i.test(lower)
1628
+ || /\bsu\s+slack\b/i.test(lower);
1629
+ if (!isSlackSend) return null;
1630
+ const parsed = await this._nluExtractSlackSend(userMessage, config);
1631
+ if (!parsed?.channel || !parsed?.text) return null;
1632
+ const { executeTool } = await import('./tool-executor.mjs');
1633
+ try {
1634
+ const r = await executeTool('slack_send', { channel: parsed.channel, text: parsed.text }, config);
1635
+ const ok = typeof r === 'string' && !/error|not found/i.test(r);
1636
+ if (this._lastDirectAuditChatId) this._recordAudit(this._lastDirectAuditChatId, { tool: 'slack_send', success: ok, summary: `→ ${parsed.channel}: "${parsed.text.slice(0, 60)}"` });
1637
+ return { action: 'slack_send', success: ok, message: ok ? `Fatto. Messaggio inviato a ${parsed.channel}.` : `Errore: ${r}` };
1638
+ } catch (e) { return { action: 'slack_send', success: false, message: `Errore: ${e.message}` }; }
1639
+ }
1640
+
1641
+ // ── NLU extractors (LLM-driven JSON parsing only, never tool execution) ──
1642
+ async _nluExtractTaskAdd(userMessage, config) {
1643
+ const todayIso = new Date().toISOString().slice(0, 10);
1644
+ const sys = `Today is ${todayIso}. Extract task params from the message. Output ONLY JSON:\n` +
1645
+ ` - title (string, the task description)\n` +
1646
+ ` - due (string, "YYYY-MM-DD" if mentioned, else null)\n` +
1647
+ ` - priority ("low" | "medium" | "high" | null)`;
1648
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 200 })); } catch { return null; }
1649
+ }
1650
+ async _nluExtractTaskRef(userMessage, config) {
1651
+ const sys = `Extract which task to mark completed. Output ONLY JSON:\n - title (string)`;
1652
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 150 })); } catch { return null; }
1653
+ }
1654
+ async _nluExtractNoteAdd(userMessage, config) {
1655
+ const sys = `Extract a new-note request. Output ONLY JSON:\n - title (string)\n - content (string, can be empty)`;
1656
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 400 })); } catch { return null; }
1657
+ }
1658
+ async _nluExtractReminder(userMessage, config) {
1659
+ const todayIso = new Date().toISOString().slice(0, 10);
1660
+ const sys = `Today is ${todayIso}. Extract reminder params. Output ONLY JSON:\n` +
1661
+ ` - message (string, what to remind)\n - when (string, ISO datetime "YYYY-MM-DDTHH:MM:00")`;
1662
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 200 })); } catch { return null; }
1663
+ }
1664
+ async _nluExtractSlackSend(userMessage, config) {
1665
+ const sys = `Extract Slack-send params. Output ONLY JSON:\n` +
1666
+ ` - channel (string, with "#" prefix for channels or "@user" for DMs)\n - text (string, message body)`;
1667
+ try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 300 })); } catch { return null; }
1668
+ }
1669
+
1670
+ // ════════════════════════════════════════════════════════════════════════
1671
+ // UNIVERSAL DIRECT-ACTION DISPATCHER
1672
+ // ════════════════════════════════════════════════════════════════════════
1673
+ // Covers ALL 22 mutation tools in `DESTRUCTIVE_ACTIONS` (gmail, imap,
1674
+ // calendar, contacts, tasks, slack, github, file, drive, notify).
1675
+ // The LLM is used ONLY to (a) decide if the message maps to a tool and
1676
+ // (b) extract the params as JSON. Tool execution is then deterministic
1677
+ // server-side. No tool block is parsed from natural language by the
1678
+ // model — the model can never "say done" without us actually doing it.
1679
+ async _tryDirectFreshUniversalAction(userMessage, config) {
1680
+ if (!userMessage || typeof userMessage !== 'string' || userMessage.length < 3) return null;
1681
+
1682
+ const todayIso = new Date().toISOString().slice(0, 10);
1683
+ const sys =
1684
+ `You are a tool-routing classifier. Given a user message in any language, decide whether it ` +
1685
+ `requests a state-changing action that maps to ONE of these tools, and extract the params.\n\n` +
1686
+ `ALLOWED TOOLS (you MUST pick one of these OR return null):\n` +
1687
+ // Calendar
1688
+ `- calendar_create(summary, start, end, description?) — start/end ISO "YYYY-MM-DDTHH:MM:00"\n` +
1689
+ `- calendar_move(eventId? OR title, newStart, newEnd?) — if no eventId, title is used to find it\n` +
1690
+ `- calendar_update(eventId? OR title, summary?, start?, end?, description?)\n` +
1691
+ `- calendar_delete(eventId? OR title, date?)\n` +
1692
+ // Email Gmail
1693
+ `- gmail_send(to, subject, body) — primary email account\n` +
1694
+ `- gmail_reply(messageId? OR threadHint, body)\n` +
1695
+ `- gmail_delete(messageId? OR query)\n` +
1696
+ `- gmail_mark_read(messageId, isRead?)\n` +
1697
+ `- gmail_mark_starred(messageId, starred?)\n` +
1698
+ `- gmail_archive(messageId)\n` +
1699
+ // Email IMAP
1700
+ `- imap_send(accountId?, to, subject, body) — custom IMAP account\n` +
1701
+ `- imap_reply(accountId?, messageId, body)\n` +
1702
+ `- imap_trash(messageId)\n` +
1703
+ `- imap_mark_read(messageId, isRead?)\n` +
1704
+ `- imap_draft(accountId?, to, subject, body)\n` +
1705
+ // Contacts
1706
+ `- contact_add(name, email?, phone?, company?, address?)\n` +
1707
+ `- contact_update(query, email?, phone?, company?, address?)\n` +
1708
+ `- contact_delete(query)\n` +
1709
+ // Tasks
1710
+ `- task_add(title, priority?, due?)\n` +
1711
+ `- task_done(title)\n` +
1712
+ `- task_delete(title)\n` +
1713
+ // Google Tasks
1714
+ `- gtask_add(title, notes?, due?)\n` +
1715
+ `- gtask_complete(title)\n` +
1716
+ // Notes
1717
+ `- note_add(title, content?)\n` +
1718
+ // Reminders
1719
+ `- notify_remind(message, when) — when = ISO datetime\n` +
1720
+ `- reminder_create(message, when)\n` +
1721
+ // Slack
1722
+ `- slack_send(channel, text, threadTs?) — channel "#name"\n` +
1723
+ `- slack_dm(user, text) — user = name, id, or email\n` +
1724
+ `- slack_react(channel, ts, emoji)\n` +
1725
+ `- slack_mark_read(channel, ts)\n` +
1726
+ // Notion
1727
+ `- notion_page(title, content) — create a new Notion page\n` +
1728
+ // GitHub
1729
+ `- github_create_issue(repo, title, body?, labels?) — repo "owner/name"\n` +
1730
+ // File system (local to user)
1731
+ `- file_write(path, content)\n` +
1732
+ `- file_move(from, to)\n` +
1733
+ `- file_delete(path)\n` +
1734
+ `- file_mkdir(path)\n` +
1735
+ // Google Drive
1736
+ `- drive_upload(name, content, mimeType?) — Google Drive\n` +
1737
+ `- drive_update(fileId, content)\n` +
1738
+ `- drive_delete(fileId? OR name)\n` +
1739
+ `- drive_move(fileId, newParentFolderId? OR newName?)\n` +
1740
+ `- drive_share(fileId, email, role?) — role = "reader"|"writer"|"commenter"\n` +
1741
+ // Birthdays
1742
+ `- birthday_add(name, date) — date "YYYY-MM-DD" or "MM-DD"\n` +
1743
+ `- birthday_delete(name)\n` +
1744
+ // Alexandria E2E
1745
+ `- alexandria_send(channel, message)\n` +
1746
+ // Cron
1747
+ `- cron_create(name, schedule, command) — schedule = cron expression\n` +
1748
+ `- cron_delete(name)\n\n` +
1749
+ `Today is ${todayIso}. Relative dates: "domani" = ${this._addDaysIso(todayIso, 1).slice(0, 10)}, ` +
1750
+ `"dopodomani" = ${this._addDaysIso(todayIso, 2).slice(0, 10)}, "lunedì/martedì/..." resolve to next occurrence.\n\n` +
1751
+ `OUTPUT FORMAT (strict JSON, no markdown, no prose, no fences):\n` +
1752
+ `{"tool": "tool_name" | null, "params": { ... }}\n\n` +
1753
+ `If the message is a READ/LIST/QUERY operation (e.g. "mostra…", "che ho oggi", "leggi email", "trova"), ` +
1754
+ `OR is conversational chat (greetings, questions, opinions) OR is ambiguous → return {"tool": null}.\n` +
1755
+ `If a required param is genuinely missing AND not inferable → return {"tool": null}.\n` +
1756
+ `Never invent emails, eventIds, or recipient addresses.`;
1757
+
1758
+ let raw;
1759
+ try {
1760
+ raw = await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 400 });
1761
+ } catch (e) {
1762
+ this.log(`[direct-universal] LLM call failed: ${e.message}`);
1763
+ return null;
1764
+ }
1765
+ const parsed = this._extractJsonObject(raw);
1766
+ if (!parsed || !parsed.tool || !DESTRUCTIVE_ACTIONS.has(parsed.tool)) return null;
1767
+ if (!parsed.params || typeof parsed.params !== 'object') return null;
1768
+
1769
+ // Per-tool param normalization (matches the executor's accepted shapes).
1770
+ const params = { ...parsed.params };
1771
+ if (parsed.tool === 'calendar_create' && !params.summary) params.summary = params.title || params.name || params.subject;
1772
+ if (parsed.tool === 'calendar_create' && params.start && !params.end) params.end = this._addMinutesIso(params.start, 60);
1773
+
1774
+ try {
1775
+ const result = await executeTool(parsed.tool, params, config);
1776
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
1777
+ const ok = !/error|failed|not\s+found|invalid|does\s+not\s+exist|placeholder/i.test(resultStr);
1778
+
1779
+ if (this._lastDirectAuditChatId) {
1780
+ this._recordAudit(this._lastDirectAuditChatId, {
1781
+ tool: parsed.tool,
1782
+ success: ok,
1783
+ summary: this._summarizeParamsForAudit(parsed.tool, params),
1784
+ });
1785
+ }
1786
+ const message = ok
1787
+ ? this._formatActionResultIT(parsed.tool, params, resultStr)
1788
+ : `Non sono riuscito a eseguire ${parsed.tool}: ${resultStr.slice(0, 240)}`;
1789
+ return { action: parsed.tool, success: ok, message };
1790
+ } catch (e) {
1791
+ return { action: parsed.tool, success: false, message: `Errore durante ${parsed.tool}: ${e.message}` };
1792
+ }
1793
+ }
1794
+
1795
+ /** Natural-language Italian summary for the audit log entry. */
1796
+ _summarizeParamsForAudit(tool, params) {
1797
+ if (tool.startsWith('calendar_')) {
1798
+ const t = params.summary || params.title || '';
1799
+ const s = params.start ? this._formatDateIT(String(params.start).slice(0, 10)) + ` ${String(params.start).slice(11, 16)}` : '';
1800
+ return `"${t}"${s ? ` · ${s}` : ''}`;
1801
+ }
1802
+ if (tool === 'gmail_send' || tool === 'imap_send') return `→ ${params.to || '?'} · "${(params.subject || '').slice(0, 60)}"`;
1803
+ if (tool === 'gmail_reply' || tool === 'imap_reply') return `reply → ${params.to || params.messageId || '?'}`;
1804
+ if (tool === 'slack_send') return `→ ${params.channel || '?'} · "${(params.text || '').slice(0, 60)}"`;
1805
+ if (tool === 'notify_remind') return `"${params.message || ''}" @ ${params.when || '?'}`;
1806
+ if (tool === 'github_create_issue') return `${params.repo || '?'}: "${params.title || ''}"`;
1807
+ if (tool === 'file_write') return `${params.path || '?'} (${(params.content || '').length} chars)`;
1808
+ if (tool === 'drive_upload') return `${params.name || '?'} (${(params.content || '').length} chars)`;
1809
+ if (tool === 'task_done' || tool === 'task_delete' || tool === 'contact_delete') return `"${params.title || params.name || '?'}"`;
1810
+ return JSON.stringify(params).slice(0, 80);
1811
+ }
1812
+
1813
+ /** Italian-language natural response for a successful action. */
1814
+ _formatActionResultIT(tool, params, result) {
1815
+ switch (tool) {
1816
+ case 'calendar_create': {
1817
+ const t = params.summary || params.title || 'evento';
1818
+ const when = params.start ? `${this._formatDateIT(String(params.start).slice(0, 10))} alle ${String(params.start).slice(11, 16)}` : '';
1819
+ return `Fatto. Ho creato l'appuntamento "${t}"${when ? ` il ${when}` : ''}.`;
1820
+ }
1821
+ case 'calendar_move': return `Fatto. Appuntamento spostato.`;
1822
+ case 'calendar_update': return `Fatto. Appuntamento aggiornato.`;
1823
+ case 'calendar_delete': return `Fatto. Appuntamento cancellato.`;
1824
+ case 'gmail_send':
1825
+ case 'imap_send': return `Fatto. Email inviata a ${params.to}.`;
1826
+ case 'gmail_reply':
1827
+ case 'imap_reply': return `Fatto. Risposta inviata.`;
1828
+ case 'gmail_delete': return `Fatto. Email eliminata.`;
1829
+ case 'imap_trash': return `Fatto. Email spostata nel cestino.`;
1830
+ case 'contact_delete': return `Fatto. Contatto "${params.name || ''}" cancellato.`;
1831
+ case 'task_done': return `Fatto. Task "${params.title || ''}" completato.`;
1832
+ case 'task_delete': return `Fatto. Task "${params.title || ''}" cancellato.`;
1833
+ case 'task_clear': return `Fatto. Task list pulita.`;
1834
+ case 'notify_remind': return `Promemoria impostato: "${params.message || ''}" per ${params.when || ''}.`;
1835
+ case 'slack_send': return `Fatto. Messaggio inviato a ${params.channel || ''}.`;
1836
+ case 'github_create_issue': return `Issue creata su ${params.repo}: "${params.title}".`;
1837
+ case 'file_write': return `Fatto. File ${params.path} scritto (${(params.content || '').length} caratteri).`;
1838
+ case 'drive_upload': return `Fatto. "${params.name}" caricato su Google Drive.`;
1839
+ case 'drive_update': return `Fatto. File Drive aggiornato.`;
1840
+ case 'drive_delete': return `Fatto. File Drive eliminato.`;
1841
+ default: return `Fatto. ${result.slice(0, 200)}`;
1842
+ }
1843
+ }
1844
+
1410
1845
  // ── Direct fresh calendar action (no LLM) ─────────────────────────────────
1411
1846
  // Detects DELETE / LIST_MONTH / LIST_WEEK / LIST_DAY / LIST_TODAY /
1412
1847
  // LIST_TOMORROW intents from a fresh user message and runs the proper tool
@@ -1432,6 +1867,14 @@ class TelegramResponder {
1432
1867
  // a date or title reference. We answer FACTUALLY by re-querying the
1433
1868
  // calendar — never by apologizing in the abstract.
1434
1869
  const isVerify = /\b(non\s+(vedo|c'è|esiste)|c'è\s+(ancora|sempre)|è\s+ancora|stai\s+sbagliand|hai\s+sbagliat|in\s+realt[àa]|sei\s+sicur|davvero|inaffidabil|controlla|verifica|conferma\b|guarda)\b/.test(lower);
1870
+ // CREATE event intent — action verb + event noun. Robust enough to catch
1871
+ // "fissami un appuntamento dal dentista venerdì alle 10", "crea evento
1872
+ // ortocheratologia 18 maggio 10-11", "segna riunione con Marco lunedì 14:30".
1873
+ const isCreate = /\b(fiss|cre[oai]|aggiung|inserisc|programm|segn|prenoti?|impost|metti|registr|set\s+up|create|add|schedule|book)\w*\s+/.test(lower)
1874
+ && /\b(appuntament|evento|event\b|meeting|riunion|incontro|chiamat|call\b|webinar|memo|reminder|promemoria|task|impegn)/i.test(lower);
1875
+ // MOVE intent — reschedule/postpone an existing event.
1876
+ const isMove = /\b(spost[ao]|rimand|rinvi|riprogramm|move|reschedule|postpone|cambia\s+(?:data|ora|orari))\w*/.test(lower)
1877
+ && /\b(appuntament|evento|event\b|meeting|riunion|incontro|chiamat|call\b)/i.test(lower);
1435
1878
 
1436
1879
  const { executeTool } = await import('./tool-executor.mjs');
1437
1880
 
@@ -1491,8 +1934,108 @@ class TelegramResponder {
1491
1934
  // No date/title to verify — fall through to LIST/DELETE detection.
1492
1935
  }
1493
1936
 
1937
+ // ─── CREATE intent ─────────────────────────────────────────────────────
1938
+ // We use a tiny LLM call ONLY to extract structured params from natural
1939
+ // language (NLU). Tool execution stays deterministic server-side. This
1940
+ // is the enterprise pattern: LLM for understanding + rendering, NEVER
1941
+ // for state-changing actions.
1942
+ if (isCreate && !isDelete && !isVerify) {
1943
+ const parsed = await this._nluExtractCalendarCreate(userMessage, config);
1944
+ if (parsed && parsed.title && parsed.start) {
1945
+ try {
1946
+ const result = await executeTool('calendar_create', {
1947
+ summary: parsed.title,
1948
+ start: parsed.start,
1949
+ end: parsed.end || this._addMinutesIso(parsed.start, 60),
1950
+ description: parsed.description || '',
1951
+ }, config);
1952
+ const ok = typeof result === 'string' && /created|event\s+"|eventId/i.test(result);
1953
+ const startLabel = `${this._formatDateIT(parsed.start.slice(0, 10))} alle ${parsed.start.slice(11, 16)}`;
1954
+ const message = ok
1955
+ ? `Fatto. Ho creato l'appuntamento "${parsed.title}" il ${startLabel}.`
1956
+ : `Non sono riuscito a creare l'appuntamento: ${result}`;
1957
+ if (this._lastDirectAuditChatId) {
1958
+ this._recordAudit(this._lastDirectAuditChatId, {
1959
+ tool: 'calendar_create',
1960
+ success: ok,
1961
+ summary: `"${parsed.title}" il ${startLabel}`,
1962
+ });
1963
+ }
1964
+ return { action: 'calendar_create', success: ok, message };
1965
+ } catch (e) {
1966
+ return { action: 'calendar_create', success: false, message: `Errore durante la creazione: ${e.message}` };
1967
+ }
1968
+ }
1969
+ // Couldn't extract title/date with confidence → let the LLM ask the user.
1970
+ // Fall through.
1971
+ }
1972
+
1973
+ // ─── MOVE intent ───────────────────────────────────────────────────────
1974
+ if (isMove && !isDelete && !isVerify && !isCreate) {
1975
+ const parsed = await this._nluExtractCalendarMove(userMessage, config);
1976
+ if (parsed && (parsed.title || parsed.oldDate) && parsed.newStart) {
1977
+ // Find the source event.
1978
+ let candidates = [];
1979
+ try {
1980
+ if (parsed.oldDate) {
1981
+ const r = await executeTool('calendar_date', { date: parsed.oldDate }, config);
1982
+ candidates = this._parseEventsFromToolOutput(r);
1983
+ }
1984
+ if (candidates.length === 0 && parsed.title) {
1985
+ const r = await executeTool('calendar_find', { query: parsed.title, daysAhead: 60 }, config);
1986
+ candidates = this._parseEventsFromToolOutput(r);
1987
+ }
1988
+ } catch (e) {
1989
+ return { action: 'calendar_move', success: false, message: `Errore nella ricerca dell'evento: ${e.message}` };
1990
+ }
1991
+ // Match by title token overlap, same heuristic as DELETE.
1992
+ const norm = (s) => String(s || '').toLowerCase()
1993
+ .normalize('NFD').replace(/[̀-ͯ]/g, '')
1994
+ .replace(/[^a-z0-9\s]/g, ' ')
1995
+ .split(/\s+/).filter(t => t.length > 2);
1996
+ let match = null;
1997
+ if (parsed.title) {
1998
+ const titleTokens = norm(parsed.title);
1999
+ const scored = candidates.map(c => {
2000
+ const summaryTokens = new Set(norm(c.summary));
2001
+ const score = titleTokens.filter(t => summaryTokens.has(t)).length;
2002
+ return { c, score };
2003
+ }).sort((a, b) => b.score - a.score);
2004
+ const top = scored[0];
2005
+ if (top && top.score >= Math.max(1, Math.ceil(titleTokens.length * 0.5))) match = top.c;
2006
+ }
2007
+ if (!match && candidates.length === 1) match = candidates[0];
2008
+ if (!match || !match.eventId) {
2009
+ return { action: 'calendar_move', success: false,
2010
+ message: `Non ho trovato l'appuntamento "${parsed.title || ''}" da spostare. Verifica titolo o data originale.` };
2011
+ }
2012
+ try {
2013
+ const result = await executeTool('calendar_move', {
2014
+ eventId: match.eventId,
2015
+ newStart: parsed.newStart,
2016
+ newEnd: parsed.newEnd || this._addMinutesIso(parsed.newStart, 60),
2017
+ }, config);
2018
+ const ok = typeof result === 'string' && /rescheduled|moved|spostat/i.test(result);
2019
+ const newLabel = `${this._formatDateIT(parsed.newStart.slice(0, 10))} alle ${parsed.newStart.slice(11, 16)}`;
2020
+ const message = ok
2021
+ ? `Fatto. Ho spostato "${match.summary}" al ${newLabel}.`
2022
+ : `Non sono riuscito a spostare l'appuntamento: ${result}`;
2023
+ if (this._lastDirectAuditChatId) {
2024
+ this._recordAudit(this._lastDirectAuditChatId, {
2025
+ tool: 'calendar_move',
2026
+ success: ok,
2027
+ summary: `"${match.summary}" → ${newLabel} (eventId ${match.eventId.slice(0, 16)}…)`,
2028
+ });
2029
+ }
2030
+ return { action: 'calendar_move', success: ok, message };
2031
+ } catch (e) {
2032
+ return { action: 'calendar_move', success: false, message: `Errore nello spostamento: ${e.message}` };
2033
+ }
2034
+ }
2035
+ }
2036
+
1494
2037
  // ─── LIST intents ──────────────────────────────────────────────────────
1495
- if (isList && !isDelete && !isVerify) {
2038
+ if (isList && !isDelete && !isVerify && !isCreate && !isMove) {
1496
2039
  // "appuntamenti di oggi"
1497
2040
  if (/\b(oggi|today)\b/.test(lower)) {
1498
2041
  try {
@@ -63,30 +63,39 @@ function getTsxPath() {
63
63
 
64
64
  /** Actions that mutate external state and require user confirmation. */
65
65
  export const DESTRUCTIVE_ACTIONS = new Set([
66
- 'gmail_send',
67
- 'gmail_send_attach',
68
- 'gmail_reply',
69
- 'gmail_delete',
70
- 'imap_send',
71
- 'imap_reply',
72
- 'imap_bulk_send',
73
- 'imap_send_template',
74
- 'imap_trash',
75
- 'calendar_create',
76
- 'calendar_move',
77
- 'calendar_update',
78
- 'calendar_delete',
79
- 'contact_delete',
80
- 'task_done',
81
- 'task_delete',
82
- 'task_clear',
83
- 'notify_remind',
84
- 'slack_send',
85
- 'github_create_issue',
86
- 'file_write',
87
- 'drive_upload',
88
- 'drive_update',
89
- 'drive_delete',
66
+ // Gmail
67
+ 'gmail_send', 'gmail_send_attach', 'gmail_reply', 'gmail_delete',
68
+ 'gmail_mark_read', 'gmail_mark_starred', 'gmail_archive', 'gmail_trash',
69
+ // IMAP (custom email accounts)
70
+ 'imap_send', 'imap_reply', 'imap_bulk_send', 'imap_send_template',
71
+ 'imap_trash', 'imap_mark_read', 'imap_mark_starred', 'imap_draft',
72
+ // Calendar
73
+ 'calendar_create', 'calendar_move', 'calendar_update', 'calendar_delete',
74
+ // Contacts
75
+ 'contact_add', 'contact_update', 'contact_delete',
76
+ // Tasks (local) + Google Tasks
77
+ 'task_add', 'task_done', 'task_delete', 'task_clear',
78
+ 'gtask_add', 'gtask_complete', 'gtask_delete',
79
+ // Notes
80
+ 'note_add',
81
+ // Reminders / notifications
82
+ 'notify_remind', 'reminder_create',
83
+ // Slack
84
+ 'slack_send', 'slack_dm', 'slack_react', 'slack_mark_read',
85
+ // Notion
86
+ 'notion_page', 'notion_update',
87
+ // GitHub
88
+ 'github_create_issue', 'github_comment',
89
+ // File system (local)
90
+ 'file_write', 'file_move', 'file_delete', 'file_mkdir',
91
+ // Google Drive
92
+ 'drive_upload', 'drive_update', 'drive_delete', 'drive_move', 'drive_share',
93
+ // Birthdays
94
+ 'birthday_add', 'birthday_update', 'birthday_delete',
95
+ // Alexandria messaging
96
+ 'alexandria_send',
97
+ // Cron / scheduling
98
+ 'cron_create', 'cron_delete',
90
99
  ]);
91
100
 
92
101
  // ── Tool Definitions (for system prompt) ─────────────────────────────────────