nothumanallowed 15.1.62 → 15.1.64
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.
|
|
3
|
+
"version": "15.1.64",
|
|
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.
|
|
8
|
+
export const VERSION = '15.1.64';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { callLLMStream, callLLM, callLLMVision, parseAgentFile } from '../../services/llm.mjs';
|
|
18
18
|
import { buildMemoryContext } from '../../services/memory.mjs';
|
|
19
19
|
import { parseActions, executeTool, buildSystemPrompt, stripOrphanFences } from '../../services/tool-executor.mjs';
|
|
20
|
-
import { detectLanguage } from '../../services/message-responder.mjs';
|
|
20
|
+
import { detectLanguage, tryDirectActionAll } from '../../services/message-responder.mjs';
|
|
21
21
|
|
|
22
22
|
// Migrate on import (once)
|
|
23
23
|
migrateOldHistory();
|
|
@@ -267,6 +267,32 @@ export function register(router) {
|
|
|
267
267
|
}, 3000);
|
|
268
268
|
|
|
269
269
|
try {
|
|
270
|
+
// ── Deterministic direct-action dispatcher (LLM-NLU + server execute) ──
|
|
271
|
+
// Same architecture used by Telegram/Discord. Before invoking the chat
|
|
272
|
+
// LLM, classify the message: if it maps to a state-changing tool
|
|
273
|
+
// (calendar/email/task/file/drive/slack/notion/github/...), execute it
|
|
274
|
+
// deterministically server-side and stream the result. No more "the
|
|
275
|
+
// model said done but didn't call the tool".
|
|
276
|
+
const direct = await tryDirectActionAll(msg, config, {
|
|
277
|
+
auditKey: `chat:${body.conversationId || 'anon'}`,
|
|
278
|
+
});
|
|
279
|
+
if (direct) {
|
|
280
|
+
if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; }
|
|
281
|
+
sse('tool', { action: direct.action, status: 'done', result: (direct.message || '').slice(0, 240) });
|
|
282
|
+
sse('token', { content: direct.message });
|
|
283
|
+
// Persist to conversation
|
|
284
|
+
if (body.conversationId) {
|
|
285
|
+
try {
|
|
286
|
+
const conv = loadConversation(body.conversationId);
|
|
287
|
+
if (conv) addMessages(conv, msg, direct.message);
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
sse('done', { content: direct.message });
|
|
291
|
+
res.write('data: [DONE]\n\n');
|
|
292
|
+
res.end();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
270
296
|
let fullResponse = '';
|
|
271
297
|
fullResponse = await callLLMStream(config, enrichedPrompt, userMessage, (chunk) => {
|
|
272
298
|
clearInterval(heartbeatInterval);
|
|
@@ -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';
|
|
@@ -829,9 +829,63 @@ class TelegramResponder {
|
|
|
829
829
|
// Track this user for broadcast notifications (update alerts, etc.)
|
|
830
830
|
touchTelegramUser(chatId, message.from?.username, message.from?.first_name);
|
|
831
831
|
|
|
832
|
-
let rawText = message.text || '';
|
|
832
|
+
let rawText = message.text || message.caption || '';
|
|
833
833
|
let isVoice = false;
|
|
834
834
|
|
|
835
|
+
// ── Image / photo handler (vision via Liara or fallback provider) ──────
|
|
836
|
+
// Telegram sends `message.photo` as an array of size variants — we pick
|
|
837
|
+
// the largest. For documents (e.g. screenshots sent as files), we accept
|
|
838
|
+
// any mime starting with image/.
|
|
839
|
+
const photo = Array.isArray(message.photo) && message.photo.length
|
|
840
|
+
? message.photo[message.photo.length - 1]
|
|
841
|
+
: null;
|
|
842
|
+
const isImageDoc = message.document && /^image\//.test(message.document.mime_type || '');
|
|
843
|
+
if (photo || isImageDoc) {
|
|
844
|
+
try {
|
|
845
|
+
await this._telegramCall('sendChatAction', { chat_id: chatId, action: 'typing' });
|
|
846
|
+
const fileId = photo ? photo.file_id : message.document.file_id;
|
|
847
|
+
const fileInfo = await this._telegramCall('getFile', { file_id: fileId });
|
|
848
|
+
const filePath = fileInfo?.result?.file_path;
|
|
849
|
+
if (!filePath) throw new Error('Telegram file_path missing');
|
|
850
|
+
const fileUrl = `https://api.telegram.org/file/bot${this.token}/${filePath}`;
|
|
851
|
+
const fileRes = await fetch(fileUrl);
|
|
852
|
+
if (!fileRes.ok) throw new Error(`Telegram file fetch ${fileRes.status}`);
|
|
853
|
+
const buf = Buffer.from(await fileRes.arrayBuffer());
|
|
854
|
+
const base64 = buf.toString('base64');
|
|
855
|
+
// Infer mediaType from file_path extension.
|
|
856
|
+
const ext = (filePath.split('.').pop() || 'jpg').toLowerCase();
|
|
857
|
+
const mediaType = ext === 'png' ? 'image/png'
|
|
858
|
+
: ext === 'gif' ? 'image/gif'
|
|
859
|
+
: ext === 'webp' ? 'image/webp'
|
|
860
|
+
: 'image/jpeg';
|
|
861
|
+
const userPrompt = rawText.trim()
|
|
862
|
+
|| 'Describe this image in detail. If it contains text, transcribe it exactly. Reply in Italian.';
|
|
863
|
+
const langInstruction = detectLanguage(userPrompt) || (rawText ? null : null);
|
|
864
|
+
const sysPrompt = `You are a helpful visual assistant. ${langInstruction === 'English' ? 'Reply in English.' : 'Rispondi in italiano.'} Be specific and accurate. If asked to extract text, transcribe it verbatim. If asked to identify objects, list them clearly.`;
|
|
865
|
+
const { callLLMVision } = await import('./llm.mjs');
|
|
866
|
+
const description = await callLLMVision(this.config, sysPrompt, userPrompt, { base64, mediaType });
|
|
867
|
+
const truncated = description.length > 4000 ? description.slice(0, 3950) + '\n\n... [truncated]' : description;
|
|
868
|
+
// Audit
|
|
869
|
+
this._recordAudit(chatId, {
|
|
870
|
+
tool: 'vision_describe',
|
|
871
|
+
success: true,
|
|
872
|
+
summary: `Image (${Math.round(buf.length / 1024)} KB) — "${(userPrompt).slice(0, 60)}"`,
|
|
873
|
+
});
|
|
874
|
+
const personaName = this.config.responder?.telegram?.botName || this.config.responder?.botName || '';
|
|
875
|
+
const personaMode = this.config.responder?.telegram?.personaMode || (personaName ? 'persona' : 'agent');
|
|
876
|
+
const prefix = personaMode === 'persona-only' && personaName ? ''
|
|
877
|
+
: personaName ? `[${personaName}]\n\n`
|
|
878
|
+
: `[HERALD]\n\n`;
|
|
879
|
+
await this._telegramCall('sendMessage', { chat_id: chatId, text: prefix + truncated });
|
|
880
|
+
this.log(`[Telegram] Image vision response to ${fromUser} (${buf.length} bytes, ${description.length} chars)`);
|
|
881
|
+
} catch (err) {
|
|
882
|
+
this.log(`[Telegram] Vision failed: ${err.message}`);
|
|
883
|
+
await this._telegramCall('sendMessage', { chat_id: chatId,
|
|
884
|
+
text: `Non riesco ad analizzare l'immagine: ${err.message}` }).catch(() => {});
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
835
889
|
// Handle voice notes — transcribe with Whisper (Groq or OpenAI)
|
|
836
890
|
if (message.voice || message.audio) {
|
|
837
891
|
const fileId = (message.voice || message.audio).file_id;
|
|
@@ -1047,13 +1101,17 @@ class TelegramResponder {
|
|
|
1047
1101
|
this._lastDirectAuditChatId = chatId;
|
|
1048
1102
|
// Run the per-domain direct-action dispatcher. First match wins; falls
|
|
1049
1103
|
// through to LLM if no handler claims the message.
|
|
1104
|
+
// Fast-path specialised handlers (regex-driven, lower latency for the
|
|
1105
|
+
// common cases), then the universal dispatcher that covers ALL 50+
|
|
1106
|
+
// mutation tools via a single LLM-NLU+deterministic-execute pass.
|
|
1050
1107
|
const directFresh =
|
|
1051
1108
|
await this._tryDirectFreshCalendarAction(cleanText, this.config) ||
|
|
1052
1109
|
await this._tryDirectFreshEmailAction(cleanText, this.config) ||
|
|
1053
1110
|
await this._tryDirectFreshTaskAction(cleanText, this.config) ||
|
|
1054
1111
|
await this._tryDirectFreshNoteAction(cleanText, this.config) ||
|
|
1055
1112
|
await this._tryDirectFreshReminderAction(cleanText, this.config) ||
|
|
1056
|
-
await this._tryDirectFreshSlackAction(cleanText, this.config)
|
|
1113
|
+
await this._tryDirectFreshSlackAction(cleanText, this.config) ||
|
|
1114
|
+
await this._tryDirectFreshUniversalAction(cleanText, this.config);
|
|
1057
1115
|
if (directFresh) {
|
|
1058
1116
|
this.log(`[Telegram] ${fromUser}: direct-fresh ${directFresh.action} → ${directFresh.success ? 'OK' : 'FAIL'}`);
|
|
1059
1117
|
const personaName = this.config.responder?.telegram?.botName || this.config.responder?.botName || '';
|
|
@@ -1663,6 +1721,181 @@ class TelegramResponder {
|
|
|
1663
1721
|
try { return this._extractJsonObject(await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 300 })); } catch { return null; }
|
|
1664
1722
|
}
|
|
1665
1723
|
|
|
1724
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1725
|
+
// UNIVERSAL DIRECT-ACTION DISPATCHER
|
|
1726
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1727
|
+
// Covers ALL 22 mutation tools in `DESTRUCTIVE_ACTIONS` (gmail, imap,
|
|
1728
|
+
// calendar, contacts, tasks, slack, github, file, drive, notify).
|
|
1729
|
+
// The LLM is used ONLY to (a) decide if the message maps to a tool and
|
|
1730
|
+
// (b) extract the params as JSON. Tool execution is then deterministic
|
|
1731
|
+
// server-side. No tool block is parsed from natural language by the
|
|
1732
|
+
// model — the model can never "say done" without us actually doing it.
|
|
1733
|
+
async _tryDirectFreshUniversalAction(userMessage, config) {
|
|
1734
|
+
if (!userMessage || typeof userMessage !== 'string' || userMessage.length < 3) return null;
|
|
1735
|
+
|
|
1736
|
+
const todayIso = new Date().toISOString().slice(0, 10);
|
|
1737
|
+
const sys =
|
|
1738
|
+
`You are a tool-routing classifier. Given a user message in any language, decide whether it ` +
|
|
1739
|
+
`requests a state-changing action that maps to ONE of these tools, and extract the params.\n\n` +
|
|
1740
|
+
`ALLOWED TOOLS (you MUST pick one of these OR return null):\n` +
|
|
1741
|
+
// Calendar
|
|
1742
|
+
`- calendar_create(summary, start, end, description?) — start/end ISO "YYYY-MM-DDTHH:MM:00"\n` +
|
|
1743
|
+
`- calendar_move(eventId? OR title, newStart, newEnd?) — if no eventId, title is used to find it\n` +
|
|
1744
|
+
`- calendar_update(eventId? OR title, summary?, start?, end?, description?)\n` +
|
|
1745
|
+
`- calendar_delete(eventId? OR title, date?)\n` +
|
|
1746
|
+
// Email Gmail
|
|
1747
|
+
`- gmail_send(to, subject, body) — primary email account\n` +
|
|
1748
|
+
`- gmail_reply(messageId? OR threadHint, body)\n` +
|
|
1749
|
+
`- gmail_delete(messageId? OR query)\n` +
|
|
1750
|
+
`- gmail_mark_read(messageId, isRead?)\n` +
|
|
1751
|
+
`- gmail_mark_starred(messageId, starred?)\n` +
|
|
1752
|
+
`- gmail_archive(messageId)\n` +
|
|
1753
|
+
// Email IMAP
|
|
1754
|
+
`- imap_send(accountId?, to, subject, body) — custom IMAP account\n` +
|
|
1755
|
+
`- imap_reply(accountId?, messageId, body)\n` +
|
|
1756
|
+
`- imap_trash(messageId)\n` +
|
|
1757
|
+
`- imap_mark_read(messageId, isRead?)\n` +
|
|
1758
|
+
`- imap_draft(accountId?, to, subject, body)\n` +
|
|
1759
|
+
// Contacts
|
|
1760
|
+
`- contact_add(name, email?, phone?, company?, address?)\n` +
|
|
1761
|
+
`- contact_update(query, email?, phone?, company?, address?)\n` +
|
|
1762
|
+
`- contact_delete(query)\n` +
|
|
1763
|
+
// Tasks
|
|
1764
|
+
`- task_add(title, priority?, due?)\n` +
|
|
1765
|
+
`- task_done(title)\n` +
|
|
1766
|
+
`- task_delete(title)\n` +
|
|
1767
|
+
// Google Tasks
|
|
1768
|
+
`- gtask_add(title, notes?, due?)\n` +
|
|
1769
|
+
`- gtask_complete(title)\n` +
|
|
1770
|
+
// Notes
|
|
1771
|
+
`- note_add(title, content?)\n` +
|
|
1772
|
+
// Reminders
|
|
1773
|
+
`- notify_remind(message, when) — when = ISO datetime\n` +
|
|
1774
|
+
`- reminder_create(message, when)\n` +
|
|
1775
|
+
// Slack
|
|
1776
|
+
`- slack_send(channel, text, threadTs?) — channel "#name"\n` +
|
|
1777
|
+
`- slack_dm(user, text) — user = name, id, or email\n` +
|
|
1778
|
+
`- slack_react(channel, ts, emoji)\n` +
|
|
1779
|
+
`- slack_mark_read(channel, ts)\n` +
|
|
1780
|
+
// Notion
|
|
1781
|
+
`- notion_page(title, content) — create a new Notion page\n` +
|
|
1782
|
+
// GitHub
|
|
1783
|
+
`- github_create_issue(repo, title, body?, labels?) — repo "owner/name"\n` +
|
|
1784
|
+
// File system (local to user)
|
|
1785
|
+
`- file_write(path, content)\n` +
|
|
1786
|
+
`- file_move(from, to)\n` +
|
|
1787
|
+
`- file_delete(path)\n` +
|
|
1788
|
+
`- file_mkdir(path)\n` +
|
|
1789
|
+
// Google Drive
|
|
1790
|
+
`- drive_upload(name, content, mimeType?) — Google Drive\n` +
|
|
1791
|
+
`- drive_update(fileId, content)\n` +
|
|
1792
|
+
`- drive_delete(fileId? OR name)\n` +
|
|
1793
|
+
`- drive_move(fileId, newParentFolderId? OR newName?)\n` +
|
|
1794
|
+
`- drive_share(fileId, email, role?) — role = "reader"|"writer"|"commenter"\n` +
|
|
1795
|
+
// Birthdays
|
|
1796
|
+
`- birthday_add(name, date) — date "YYYY-MM-DD" or "MM-DD"\n` +
|
|
1797
|
+
`- birthday_delete(name)\n` +
|
|
1798
|
+
// Alexandria E2E
|
|
1799
|
+
`- alexandria_send(channel, message)\n` +
|
|
1800
|
+
// Cron
|
|
1801
|
+
`- cron_create(name, schedule, command) — schedule = cron expression\n` +
|
|
1802
|
+
`- cron_delete(name)\n\n` +
|
|
1803
|
+
`Today is ${todayIso}. Relative dates: "domani" = ${this._addDaysIso(todayIso, 1).slice(0, 10)}, ` +
|
|
1804
|
+
`"dopodomani" = ${this._addDaysIso(todayIso, 2).slice(0, 10)}, "lunedì/martedì/..." resolve to next occurrence.\n\n` +
|
|
1805
|
+
`OUTPUT FORMAT (strict JSON, no markdown, no prose, no fences):\n` +
|
|
1806
|
+
`{"tool": "tool_name" | null, "params": { ... }}\n\n` +
|
|
1807
|
+
`If the message is a READ/LIST/QUERY operation (e.g. "mostra…", "che ho oggi", "leggi email", "trova"), ` +
|
|
1808
|
+
`OR is conversational chat (greetings, questions, opinions) OR is ambiguous → return {"tool": null}.\n` +
|
|
1809
|
+
`If a required param is genuinely missing AND not inferable → return {"tool": null}.\n` +
|
|
1810
|
+
`Never invent emails, eventIds, or recipient addresses.`;
|
|
1811
|
+
|
|
1812
|
+
let raw;
|
|
1813
|
+
try {
|
|
1814
|
+
raw = await callLLM(config, sys, userMessage, { temperature: 0, maxTokens: 400 });
|
|
1815
|
+
} catch (e) {
|
|
1816
|
+
this.log(`[direct-universal] LLM call failed: ${e.message}`);
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
const parsed = this._extractJsonObject(raw);
|
|
1820
|
+
if (!parsed || !parsed.tool || !DESTRUCTIVE_ACTIONS.has(parsed.tool)) return null;
|
|
1821
|
+
if (!parsed.params || typeof parsed.params !== 'object') return null;
|
|
1822
|
+
|
|
1823
|
+
// Per-tool param normalization (matches the executor's accepted shapes).
|
|
1824
|
+
const params = { ...parsed.params };
|
|
1825
|
+
if (parsed.tool === 'calendar_create' && !params.summary) params.summary = params.title || params.name || params.subject;
|
|
1826
|
+
if (parsed.tool === 'calendar_create' && params.start && !params.end) params.end = this._addMinutesIso(params.start, 60);
|
|
1827
|
+
|
|
1828
|
+
try {
|
|
1829
|
+
const result = await executeTool(parsed.tool, params, config);
|
|
1830
|
+
const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
|
|
1831
|
+
const ok = !/error|failed|not\s+found|invalid|does\s+not\s+exist|placeholder/i.test(resultStr);
|
|
1832
|
+
|
|
1833
|
+
if (this._lastDirectAuditChatId) {
|
|
1834
|
+
this._recordAudit(this._lastDirectAuditChatId, {
|
|
1835
|
+
tool: parsed.tool,
|
|
1836
|
+
success: ok,
|
|
1837
|
+
summary: this._summarizeParamsForAudit(parsed.tool, params),
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
const message = ok
|
|
1841
|
+
? this._formatActionResultIT(parsed.tool, params, resultStr)
|
|
1842
|
+
: `Non sono riuscito a eseguire ${parsed.tool}: ${resultStr.slice(0, 240)}`;
|
|
1843
|
+
return { action: parsed.tool, success: ok, message };
|
|
1844
|
+
} catch (e) {
|
|
1845
|
+
return { action: parsed.tool, success: false, message: `Errore durante ${parsed.tool}: ${e.message}` };
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/** Natural-language Italian summary for the audit log entry. */
|
|
1850
|
+
_summarizeParamsForAudit(tool, params) {
|
|
1851
|
+
if (tool.startsWith('calendar_')) {
|
|
1852
|
+
const t = params.summary || params.title || '';
|
|
1853
|
+
const s = params.start ? this._formatDateIT(String(params.start).slice(0, 10)) + ` ${String(params.start).slice(11, 16)}` : '';
|
|
1854
|
+
return `"${t}"${s ? ` · ${s}` : ''}`;
|
|
1855
|
+
}
|
|
1856
|
+
if (tool === 'gmail_send' || tool === 'imap_send') return `→ ${params.to || '?'} · "${(params.subject || '').slice(0, 60)}"`;
|
|
1857
|
+
if (tool === 'gmail_reply' || tool === 'imap_reply') return `reply → ${params.to || params.messageId || '?'}`;
|
|
1858
|
+
if (tool === 'slack_send') return `→ ${params.channel || '?'} · "${(params.text || '').slice(0, 60)}"`;
|
|
1859
|
+
if (tool === 'notify_remind') return `"${params.message || ''}" @ ${params.when || '?'}`;
|
|
1860
|
+
if (tool === 'github_create_issue') return `${params.repo || '?'}: "${params.title || ''}"`;
|
|
1861
|
+
if (tool === 'file_write') return `${params.path || '?'} (${(params.content || '').length} chars)`;
|
|
1862
|
+
if (tool === 'drive_upload') return `${params.name || '?'} (${(params.content || '').length} chars)`;
|
|
1863
|
+
if (tool === 'task_done' || tool === 'task_delete' || tool === 'contact_delete') return `"${params.title || params.name || '?'}"`;
|
|
1864
|
+
return JSON.stringify(params).slice(0, 80);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/** Italian-language natural response for a successful action. */
|
|
1868
|
+
_formatActionResultIT(tool, params, result) {
|
|
1869
|
+
switch (tool) {
|
|
1870
|
+
case 'calendar_create': {
|
|
1871
|
+
const t = params.summary || params.title || 'evento';
|
|
1872
|
+
const when = params.start ? `${this._formatDateIT(String(params.start).slice(0, 10))} alle ${String(params.start).slice(11, 16)}` : '';
|
|
1873
|
+
return `Fatto. Ho creato l'appuntamento "${t}"${when ? ` il ${when}` : ''}.`;
|
|
1874
|
+
}
|
|
1875
|
+
case 'calendar_move': return `Fatto. Appuntamento spostato.`;
|
|
1876
|
+
case 'calendar_update': return `Fatto. Appuntamento aggiornato.`;
|
|
1877
|
+
case 'calendar_delete': return `Fatto. Appuntamento cancellato.`;
|
|
1878
|
+
case 'gmail_send':
|
|
1879
|
+
case 'imap_send': return `Fatto. Email inviata a ${params.to}.`;
|
|
1880
|
+
case 'gmail_reply':
|
|
1881
|
+
case 'imap_reply': return `Fatto. Risposta inviata.`;
|
|
1882
|
+
case 'gmail_delete': return `Fatto. Email eliminata.`;
|
|
1883
|
+
case 'imap_trash': return `Fatto. Email spostata nel cestino.`;
|
|
1884
|
+
case 'contact_delete': return `Fatto. Contatto "${params.name || ''}" cancellato.`;
|
|
1885
|
+
case 'task_done': return `Fatto. Task "${params.title || ''}" completato.`;
|
|
1886
|
+
case 'task_delete': return `Fatto. Task "${params.title || ''}" cancellato.`;
|
|
1887
|
+
case 'task_clear': return `Fatto. Task list pulita.`;
|
|
1888
|
+
case 'notify_remind': return `Promemoria impostato: "${params.message || ''}" per ${params.when || ''}.`;
|
|
1889
|
+
case 'slack_send': return `Fatto. Messaggio inviato a ${params.channel || ''}.`;
|
|
1890
|
+
case 'github_create_issue': return `Issue creata su ${params.repo}: "${params.title}".`;
|
|
1891
|
+
case 'file_write': return `Fatto. File ${params.path} scritto (${(params.content || '').length} caratteri).`;
|
|
1892
|
+
case 'drive_upload': return `Fatto. "${params.name}" caricato su Google Drive.`;
|
|
1893
|
+
case 'drive_update': return `Fatto. File Drive aggiornato.`;
|
|
1894
|
+
case 'drive_delete': return `Fatto. File Drive eliminato.`;
|
|
1895
|
+
default: return `Fatto. ${result.slice(0, 200)}`;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1666
1899
|
// ── Direct fresh calendar action (no LLM) ─────────────────────────────────
|
|
1667
1900
|
// Detects DELETE / LIST_MONTH / LIST_WEEK / LIST_DAY / LIST_TODAY /
|
|
1668
1901
|
// LIST_TOMORROW intents from a fresh user message and runs the proper tool
|
|
@@ -2010,6 +2243,50 @@ class TelegramResponder {
|
|
|
2010
2243
|
}
|
|
2011
2244
|
}
|
|
2012
2245
|
|
|
2246
|
+
// ── Shared direct-action dispatcher (Telegram / Discord / Chat WebUI / Voice) ─
|
|
2247
|
+
// A reusable, instance-less handler. Internally piggybacks on a singleton
|
|
2248
|
+
// TelegramResponder built with a dummy config — we only use it as a host for
|
|
2249
|
+
// the `_tryDirectFresh*` methods. The audit log is keyed by the caller's
|
|
2250
|
+
// own `auditKey` (chatId for Telegram, channelId for Discord, conversationId
|
|
2251
|
+
// for Chat WebUI), so each platform keeps its own action history without
|
|
2252
|
+
// crossing wires.
|
|
2253
|
+
let _sharedDirectHandler = null;
|
|
2254
|
+
function _getDirectHandler() {
|
|
2255
|
+
if (!_sharedDirectHandler) {
|
|
2256
|
+
_sharedDirectHandler = new TelegramResponder(
|
|
2257
|
+
{ responder: { telegram: { token: '__noop__' } } },
|
|
2258
|
+
() => {},
|
|
2259
|
+
() => {},
|
|
2260
|
+
);
|
|
2261
|
+
// Ensure the in-memory store exists.
|
|
2262
|
+
_sharedDirectHandler._lastContextByChatId = _sharedDirectHandler._lastContextByChatId || {};
|
|
2263
|
+
}
|
|
2264
|
+
return _sharedDirectHandler;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/**
|
|
2268
|
+
* Try every direct-action handler in order (fast-path → universal). Returns
|
|
2269
|
+
* `{action, success, message}` on hit, `null` if nothing claimed the message.
|
|
2270
|
+
*
|
|
2271
|
+
* @param {string} text — the raw user message in any language
|
|
2272
|
+
* @param {object} config — loaded nha config (used by tools + LLM NLU)
|
|
2273
|
+
* @param {object} [opts]
|
|
2274
|
+
* @param {string} [opts.auditKey] — stable key for action audit (chatId, channelId, conversationId…)
|
|
2275
|
+
* @param {(line:string)=>void} [opts.log] — optional logger
|
|
2276
|
+
*/
|
|
2277
|
+
export async function tryDirectActionAll(text, config, opts = {}) {
|
|
2278
|
+
const h = _getDirectHandler();
|
|
2279
|
+
if (opts.auditKey) h._lastDirectAuditChatId = opts.auditKey;
|
|
2280
|
+
if (opts.log) h.log = opts.log;
|
|
2281
|
+
return await h._tryDirectFreshCalendarAction(text, config)
|
|
2282
|
+
|| await h._tryDirectFreshEmailAction(text, config)
|
|
2283
|
+
|| await h._tryDirectFreshTaskAction(text, config)
|
|
2284
|
+
|| await h._tryDirectFreshNoteAction(text, config)
|
|
2285
|
+
|| await h._tryDirectFreshReminderAction(text, config)
|
|
2286
|
+
|| await h._tryDirectFreshSlackAction(text, config)
|
|
2287
|
+
|| await h._tryDirectFreshUniversalAction(text, config);
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2013
2290
|
// ── Discord Bot (Gateway WebSocket via raw TLS, zero dependencies) ───────────
|
|
2014
2291
|
|
|
2015
2292
|
class DiscordResponder {
|
|
@@ -2333,6 +2610,21 @@ class DiscordResponder {
|
|
|
2333
2610
|
this.pendingRequests++;
|
|
2334
2611
|
|
|
2335
2612
|
try {
|
|
2613
|
+
// Try the deterministic direct-action dispatcher BEFORE routing to an
|
|
2614
|
+
// LLM agent. Same architecture used by Telegram: LLM only for NLU,
|
|
2615
|
+
// tool execution always server-side, audit log per channel.
|
|
2616
|
+
const directFresh = await tryDirectActionAll(cleanText, this.config, {
|
|
2617
|
+
auditKey: `discord:${channelId}`,
|
|
2618
|
+
log: this.log,
|
|
2619
|
+
});
|
|
2620
|
+
if (directFresh) {
|
|
2621
|
+
await this._discordApiCall('POST', `/channels/${channelId}/messages`, {
|
|
2622
|
+
content: directFresh.message,
|
|
2623
|
+
});
|
|
2624
|
+
this.log(`[Discord] direct-action ${directFresh.action} → ${directFresh.success ? 'OK' : 'FAIL'}`);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2336
2628
|
const agent = routeMessage(cleanText, this.autoRoute);
|
|
2337
2629
|
this.log(`[Discord] ${fromUser} (#${channelId}): routed to ${agent.toUpperCase()}`);
|
|
2338
2630
|
|
|
@@ -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
|
-
|
|
67
|
-
'gmail_send_attach',
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
'imap_send',
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
'
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
|
|
80
|
-
'
|
|
81
|
-
|
|
82
|
-
'
|
|
83
|
-
|
|
84
|
-
'slack_send',
|
|
85
|
-
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
|
|
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) ─────────────────────────────────────
|