nothumanallowed 15.1.42 → 15.1.43
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 +221 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.43",
|
|
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.43';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -919,6 +919,50 @@ class TelegramResponder {
|
|
|
919
919
|
const shortConfirm = /^(s[ìi]\.?|ok\.?|okay\.?|yes\.?|yep\.?|sure\.?|certo\.?|procedi\.?|fallo\.?|vai\.?|go\.?|do it\.?|conferma\.?|certo che s[ìi]\.?|fallo pure\.?)$/i.test(cleanText.trim());
|
|
920
920
|
|
|
921
921
|
if (proposedAction && shortConfirm) {
|
|
922
|
+
// ── Server-side deterministic execution (15.1.43) ───────────────
|
|
923
|
+
// The previous "prompt-engineering" force retry kept failing when
|
|
924
|
+
// the LLM declared success without emitting the tool block. For
|
|
925
|
+
// the most common confirmed actions (DELETE, MOVE), we now bypass
|
|
926
|
+
// the LLM entirely: parse the proposal, resolve the eventId via
|
|
927
|
+
// calendar_date / calendar_find, then call calendar_delete /
|
|
928
|
+
// calendar_move directly. The user gets a guaranteed real result.
|
|
929
|
+
const directResult = await this._tryDirectAction(lastCtx.agentReply || '', this.config);
|
|
930
|
+
if (directResult) {
|
|
931
|
+
this.log(`[Telegram] ${fromUser}: direct-action ${directResult.action} → ${directResult.success ? 'OK' : 'FAIL'}`);
|
|
932
|
+
const personaName = this.config.responder?.telegram?.botName || this.config.responder?.botName || '';
|
|
933
|
+
const personaMode = this.config.responder?.telegram?.personaMode || (personaName ? 'persona' : 'agent');
|
|
934
|
+
let reply;
|
|
935
|
+
if (personaMode === 'persona-only' && personaName) {
|
|
936
|
+
reply = directResult.message;
|
|
937
|
+
} else if (personaMode === 'persona+role' && personaName) {
|
|
938
|
+
reply = `[${personaName} · herald]\n\n${directResult.message}`;
|
|
939
|
+
} else if (personaMode === 'persona' && personaName) {
|
|
940
|
+
reply = `[${personaName}]\n\n${directResult.message}`;
|
|
941
|
+
} else {
|
|
942
|
+
reply = `[HERALD]\n\n${directResult.message}`;
|
|
943
|
+
}
|
|
944
|
+
await this._telegramCall('sendMessage', { chat_id: chatId, text: reply });
|
|
945
|
+
|
|
946
|
+
// Update rolling memory + reset pending action (so a follow-up
|
|
947
|
+
// "Si" doesn't try to delete a second time).
|
|
948
|
+
const MAX = 20;
|
|
949
|
+
const prevLog = (lastCtx && Array.isArray(lastCtx.conversationLog)) ? lastCtx.conversationLog : [];
|
|
950
|
+
this._lastContextByChatId[chatId] = {
|
|
951
|
+
agent: 'herald',
|
|
952
|
+
userMsg: cleanText,
|
|
953
|
+
agentReply: directResult.message,
|
|
954
|
+
history: null,
|
|
955
|
+
conversationLog: [...prevLog,
|
|
956
|
+
{ role: 'user', content: cleanText, ts: Date.now() },
|
|
957
|
+
{ role: 'assistant', content: directResult.message, ts: Date.now() },
|
|
958
|
+
].slice(-MAX * 2),
|
|
959
|
+
ts: Date.now(),
|
|
960
|
+
};
|
|
961
|
+
this._lastAgentByChatId[chatId] = 'herald';
|
|
962
|
+
this._persistContext();
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
922
966
|
// Extract the SPECIFIC action the assistant proposed (create/delete/
|
|
923
967
|
// update/move/send/...), so we can inject an explicit tool name in
|
|
924
968
|
// the instruction. Without this, Liara may pick a random tool —
|
|
@@ -1074,6 +1118,183 @@ class TelegramResponder {
|
|
|
1074
1118
|
}
|
|
1075
1119
|
}
|
|
1076
1120
|
|
|
1121
|
+
// ── Direct server-side execution of a confirmed pending action ────────
|
|
1122
|
+
// Returns { action, success, message } when it could resolve and execute
|
|
1123
|
+
// the action without involving the LLM. Returns null when the proposal
|
|
1124
|
+
// can't be reduced to deterministic parameters (e.g. ambiguous title,
|
|
1125
|
+
// multiple candidate events, unsupported action shape) — caller then
|
|
1126
|
+
// falls back to the prompt-engineering path.
|
|
1127
|
+
//
|
|
1128
|
+
// Currently supports: calendar_delete. (calendar_move/update can extend
|
|
1129
|
+
// the same pattern once the proposal language stabilizes.)
|
|
1130
|
+
async _tryDirectAction(proposalText, config) {
|
|
1131
|
+
if (!proposalText || typeof proposalText !== 'string') return null;
|
|
1132
|
+
const lower = proposalText.toLowerCase();
|
|
1133
|
+
|
|
1134
|
+
// Only handle DELETE for now — the riskiest "fake success" case.
|
|
1135
|
+
const isDelete = /\b(cancell|eliminar[eo]|rimuover|delete|cancel)\b/.test(lower);
|
|
1136
|
+
if (!isDelete) return null;
|
|
1137
|
+
|
|
1138
|
+
// Refuse if multiple distinct actions are proposed (chained plan).
|
|
1139
|
+
if (/(\bpoi\b|\binoltre\b|\bdopo\b|\bquindi\b).*(invier[oò]|crear[oò]|spostar[oò]|modificare[oò]|aggiunger[oò])/i.test(proposalText)) {
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const extracted = this._extractCalendarProposal(proposalText);
|
|
1144
|
+
if (!extracted.date && !extracted.title) return null;
|
|
1145
|
+
|
|
1146
|
+
const { executeTool } = await import('./tool-executor.mjs');
|
|
1147
|
+
|
|
1148
|
+
// Candidates: events on the proposed date, or matching the title.
|
|
1149
|
+
let candidates = [];
|
|
1150
|
+
try {
|
|
1151
|
+
if (extracted.date) {
|
|
1152
|
+
const result = await executeTool('calendar_date', { date: extracted.date }, config);
|
|
1153
|
+
candidates = this._parseEventsFromToolOutput(result);
|
|
1154
|
+
}
|
|
1155
|
+
if (candidates.length === 0 && extracted.title) {
|
|
1156
|
+
const result = await executeTool('calendar_find', { query: extracted.title, daysAhead: 60 }, config);
|
|
1157
|
+
candidates = this._parseEventsFromToolOutput(result);
|
|
1158
|
+
}
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
this.log(`[Telegram] direct-action lookup failed: ${err.message}`);
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (candidates.length === 0) return null;
|
|
1165
|
+
|
|
1166
|
+
// Match by title tokens (case-insensitive, accent-insensitive).
|
|
1167
|
+
const norm = (s) => String(s || '')
|
|
1168
|
+
.toLowerCase()
|
|
1169
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
1170
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
1171
|
+
.split(/\s+/).filter(t => t.length > 2);
|
|
1172
|
+
|
|
1173
|
+
let match = null;
|
|
1174
|
+
if (extracted.title) {
|
|
1175
|
+
const titleTokens = norm(extracted.title);
|
|
1176
|
+
const scored = candidates.map(c => {
|
|
1177
|
+
const summaryTokens = new Set(norm(c.summary));
|
|
1178
|
+
const score = titleTokens.filter(t => summaryTokens.has(t)).length;
|
|
1179
|
+
return { c, score };
|
|
1180
|
+
}).sort((a, b) => b.score - a.score);
|
|
1181
|
+
const top = scored[0];
|
|
1182
|
+
if (top && top.score >= Math.max(1, Math.ceil(titleTokens.length * 0.5))) {
|
|
1183
|
+
// Reject if a tied second-best also matches as well — ambiguous.
|
|
1184
|
+
if (scored.length === 1 || scored[1].score < top.score) match = top.c;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Fallback: if the date narrowed the day to a single event, that's
|
|
1189
|
+
// unambiguously the event the user meant.
|
|
1190
|
+
if (!match && candidates.length === 1 && extracted.date) match = candidates[0];
|
|
1191
|
+
|
|
1192
|
+
// Time hint as final tiebreaker.
|
|
1193
|
+
if (!match && extracted.time) {
|
|
1194
|
+
const exactTime = candidates.filter(c => (c.time || '').startsWith(extracted.time));
|
|
1195
|
+
if (exactTime.length === 1) match = exactTime[0];
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (!match || !match.eventId) return null;
|
|
1199
|
+
|
|
1200
|
+
try {
|
|
1201
|
+
const delResult = await executeTool('calendar_delete', { eventId: match.eventId }, config);
|
|
1202
|
+
const ok = typeof delResult === 'string' && !/error|failed|could not|invalid|placeholder/i.test(delResult);
|
|
1203
|
+
const summary = match.summary || extracted.title || 'l\'appuntamento';
|
|
1204
|
+
const dateStr = extracted.date || (match.date || '');
|
|
1205
|
+
const message = ok
|
|
1206
|
+
? `Fatto. Ho cancellato "${summary}"${dateStr ? ` del ${this._formatDateIT(dateStr)}` : ''}${match.time ? ` alle ${match.time}` : ''}.`
|
|
1207
|
+
: `Non sono riuscito a cancellare l'evento: ${delResult}`;
|
|
1208
|
+
return { action: 'calendar_delete', success: ok, message };
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
return { action: 'calendar_delete', success: false, message: `Errore nella cancellazione: ${err.message}` };
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Extract { title, date (YYYY-MM-DD), time (HH:MM) } from a free-form
|
|
1215
|
+
// Italian/English proposal sentence. Best-effort; returns empty fields
|
|
1216
|
+
// when nothing parseable was found.
|
|
1217
|
+
_extractCalendarProposal(text) {
|
|
1218
|
+
const out = { title: '', date: '', time: '' };
|
|
1219
|
+
|
|
1220
|
+
// Title: prefer quoted text ("..." or «...»).
|
|
1221
|
+
const q = text.match(/["«""]([^"«»""\n]{2,80})["»""]/);
|
|
1222
|
+
if (q) out.title = q[1].trim();
|
|
1223
|
+
|
|
1224
|
+
// Time: HH:MM (24h) or "alle 18" / "ore 18"
|
|
1225
|
+
const tm = text.match(/\b([01]?\d|2[0-3]):([0-5]\d)\b/);
|
|
1226
|
+
if (tm) out.time = `${tm[1].padStart(2, '0')}:${tm[2]}`;
|
|
1227
|
+
else {
|
|
1228
|
+
const hourOnly = text.match(/\b(?:alle|ore|at)\s+(\d{1,2})(?!\d)\b/i);
|
|
1229
|
+
if (hourOnly) out.time = `${hourOnly[1].padStart(2, '0')}:00`;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Date: italian "15 maggio [2026]", numeric "15/05[/2026]", ISO 2026-05-15
|
|
1233
|
+
const iso = text.match(/\b(20\d{2})-(\d{2})-(\d{2})\b/);
|
|
1234
|
+
if (iso) {
|
|
1235
|
+
out.date = `${iso[1]}-${iso[2]}-${iso[3]}`;
|
|
1236
|
+
} else {
|
|
1237
|
+
const numeric = text.match(/\b(\d{1,2})[\/\-](\d{1,2})(?:[\/\-](20\d{2}))?\b/);
|
|
1238
|
+
if (numeric) {
|
|
1239
|
+
const yr = numeric[3] || String(new Date().getFullYear());
|
|
1240
|
+
out.date = `${yr}-${numeric[2].padStart(2, '0')}-${numeric[1].padStart(2, '0')}`;
|
|
1241
|
+
} else {
|
|
1242
|
+
const MONTHS_IT = {
|
|
1243
|
+
gennaio:'01', febbraio:'02', marzo:'03', aprile:'04', maggio:'05', giugno:'06',
|
|
1244
|
+
luglio:'07', agosto:'08', settembre:'09', ottobre:'10', novembre:'11', dicembre:'12',
|
|
1245
|
+
};
|
|
1246
|
+
const MONTHS_EN = {
|
|
1247
|
+
january:'01', february:'02', march:'03', april:'04', may:'05', june:'06',
|
|
1248
|
+
july:'07', august:'08', september:'09', october:'10', november:'11', december:'12',
|
|
1249
|
+
jan:'01', feb:'02', mar:'03', apr:'04', jun:'06', jul:'07', aug:'08', sep:'09',
|
|
1250
|
+
sept:'09', oct:'10', nov:'11', dec:'12',
|
|
1251
|
+
};
|
|
1252
|
+
const all = { ...MONTHS_IT, ...MONTHS_EN };
|
|
1253
|
+
const monthRe = new RegExp(`\\b(\\d{1,2})\\s+(${Object.keys(all).join('|')})(?:\\s+(20\\d{2}))?\\b`, 'i');
|
|
1254
|
+
const monthM = text.match(monthRe);
|
|
1255
|
+
if (monthM) {
|
|
1256
|
+
const yr = monthM[3] || String(new Date().getFullYear());
|
|
1257
|
+
out.date = `${yr}-${all[monthM[2].toLowerCase()]}-${monthM[1].padStart(2, '0')}`;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return out;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Parse calendar_date / calendar_find tool output. The executor returns
|
|
1266
|
+
// a human-readable string with each event on its own line plus the
|
|
1267
|
+
// eventId in parentheses. We extract structured records.
|
|
1268
|
+
_parseEventsFromToolOutput(toolResult) {
|
|
1269
|
+
const text = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult || '');
|
|
1270
|
+
const lines = text.split(/\r?\n/);
|
|
1271
|
+
const events = [];
|
|
1272
|
+
for (const line of lines) {
|
|
1273
|
+
// Look for the eventId pattern: parenthesised long alphanumeric.
|
|
1274
|
+
const idMatch = line.match(/\(([a-z0-9_\-]{8,})\)/i);
|
|
1275
|
+
if (!idMatch) continue;
|
|
1276
|
+
const eventId = idMatch[1];
|
|
1277
|
+
// Strip the (id) and any leading bullets/dashes/times for the summary.
|
|
1278
|
+
const cleaned = line.replace(/\([a-z0-9_\-]{8,}\)/i, '').trim();
|
|
1279
|
+
const timeM = cleaned.match(/\b([01]?\d|2[0-3]):([0-5]\d)\b/);
|
|
1280
|
+
const time = timeM ? `${timeM[1].padStart(2, '0')}:${timeM[2]}` : '';
|
|
1281
|
+
// Heuristic summary: text after the time, before any " - " or " · ".
|
|
1282
|
+
let summary = cleaned;
|
|
1283
|
+
if (timeM) summary = cleaned.slice(cleaned.indexOf(timeM[0]) + timeM[0].length);
|
|
1284
|
+
summary = summary.replace(/^[\s\-–·•\.]+/, '').replace(/[\s\-–·•\.]+$/, '').trim();
|
|
1285
|
+
if (!summary) summary = cleaned.replace(/^[\s\-–·•\.\d:]+/, '').trim();
|
|
1286
|
+
events.push({ eventId, summary, time, date: '' });
|
|
1287
|
+
}
|
|
1288
|
+
return events;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
_formatDateIT(isoDate) {
|
|
1292
|
+
const m = String(isoDate || '').match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
1293
|
+
if (!m) return isoDate;
|
|
1294
|
+
const months = ['gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre'];
|
|
1295
|
+
return `${parseInt(m[3], 10)} ${months[parseInt(m[2], 10) - 1]} ${m[1]}`;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1077
1298
|
async _telegramCall(method, body) {
|
|
1078
1299
|
const res = await fetch(`https://api.telegram.org/bot${this.token}/${method}`, {
|
|
1079
1300
|
method: 'POST',
|