nothumanallowed 16.0.22 → 16.0.23
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 +145 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.23",
|
|
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.23';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -774,6 +774,43 @@ class TelegramResponder {
|
|
|
774
774
|
this.log('[Telegram] Responder stopped');
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Send a Telegram message that may exceed the 4096-char API limit.
|
|
779
|
+
* Splits on paragraph/line boundaries when possible, never on a multi-byte
|
|
780
|
+
* sequence. Sends in order with a small delay to avoid rate-limit 429.
|
|
781
|
+
* Returns the array of message IDs sent.
|
|
782
|
+
*/
|
|
783
|
+
async _sendMessageSafe(chatId, text, extraOpts = {}) {
|
|
784
|
+
const TG_MAX = 4000; // safety margin from 4096 to account for emoji weight
|
|
785
|
+
const str = String(text == null ? '' : text);
|
|
786
|
+
if (str.length <= TG_MAX) {
|
|
787
|
+
return [await this._telegramCall('sendMessage', { chat_id: chatId, text: str, ...extraOpts })];
|
|
788
|
+
}
|
|
789
|
+
// Split intelligently: try paragraph breaks, then lines, then hard slice.
|
|
790
|
+
const chunks = [];
|
|
791
|
+
let remaining = str;
|
|
792
|
+
while (remaining.length > TG_MAX) {
|
|
793
|
+
let cutAt = remaining.lastIndexOf('\n\n', TG_MAX);
|
|
794
|
+
if (cutAt < TG_MAX / 2) cutAt = remaining.lastIndexOf('\n', TG_MAX);
|
|
795
|
+
if (cutAt < TG_MAX / 2) cutAt = remaining.lastIndexOf(' ', TG_MAX);
|
|
796
|
+
if (cutAt < TG_MAX / 2) cutAt = TG_MAX;
|
|
797
|
+
chunks.push(remaining.slice(0, cutAt).trim());
|
|
798
|
+
remaining = remaining.slice(cutAt).trim();
|
|
799
|
+
}
|
|
800
|
+
if (remaining) chunks.push(remaining);
|
|
801
|
+
const ids = [];
|
|
802
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
803
|
+
const part = chunks.length > 1 ? `(${i + 1}/${chunks.length})\n${chunks[i]}` : chunks[i];
|
|
804
|
+
try {
|
|
805
|
+
ids.push(await this._telegramCall('sendMessage', { chat_id: chatId, text: part, ...extraOpts }));
|
|
806
|
+
} catch (e) {
|
|
807
|
+
this.log(`[Telegram] sendMessage chunk ${i + 1}/${chunks.length} failed: ${e.message}`);
|
|
808
|
+
}
|
|
809
|
+
await this._sleep(180); // avoid 429 rate limit
|
|
810
|
+
}
|
|
811
|
+
return ids;
|
|
812
|
+
}
|
|
813
|
+
|
|
777
814
|
async _scheduleUpdateCheck() {
|
|
778
815
|
await this._checkAndNotifyUpdate();
|
|
779
816
|
await this._checkLocalUpdateAndRestart();
|
|
@@ -834,14 +871,10 @@ class TelegramResponder {
|
|
|
834
871
|
|
|
835
872
|
for (const chatId of chatIds) {
|
|
836
873
|
try {
|
|
837
|
-
await this.
|
|
838
|
-
chat_id: parseInt(chatId, 10),
|
|
839
|
-
text: msg,
|
|
840
|
-
});
|
|
874
|
+
await this._sendMessageSafe(parseInt(chatId, 10), msg);
|
|
841
875
|
} catch {
|
|
842
876
|
// User blocked bot or chat no longer exists — ignore
|
|
843
877
|
}
|
|
844
|
-
// Small delay to avoid Telegram rate limits
|
|
845
878
|
await this._sleep(300);
|
|
846
879
|
}
|
|
847
880
|
} catch (err) {
|
|
@@ -1037,12 +1070,11 @@ class TelegramResponder {
|
|
|
1037
1070
|
const prefix = personaMode === 'persona-only' && personaName ? ''
|
|
1038
1071
|
: personaName ? `[${personaName}]\n\n`
|
|
1039
1072
|
: `[HERALD]\n\n`;
|
|
1040
|
-
await this.
|
|
1073
|
+
await this._sendMessageSafe(chatId, prefix + description);
|
|
1041
1074
|
this.log(`[Telegram] Image vision response to ${fromUser} (${buf.length} bytes, ${description.length} chars)`);
|
|
1042
1075
|
} catch (err) {
|
|
1043
1076
|
this.log(`[Telegram] Vision failed: ${err.message}`);
|
|
1044
|
-
await this.
|
|
1045
|
-
text: `Non riesco ad analizzare l'immagine: ${err.message}` }).catch(() => {});
|
|
1077
|
+
await this._sendMessageSafe(chatId, `Non riesco ad analizzare l'immagine: ${err.message}`).catch(() => {});
|
|
1046
1078
|
}
|
|
1047
1079
|
return;
|
|
1048
1080
|
}
|
|
@@ -1055,16 +1087,13 @@ class TelegramResponder {
|
|
|
1055
1087
|
await this._telegramCall('sendChatAction', { chat_id: chatId, action: 'typing' });
|
|
1056
1088
|
rawText = await this._transcribeVoice(fileId);
|
|
1057
1089
|
if (!rawText.trim()) {
|
|
1058
|
-
await this.
|
|
1090
|
+
await this._sendMessageSafe(chatId, 'Non ho capito il vocale. Riprova.');
|
|
1059
1091
|
return;
|
|
1060
1092
|
}
|
|
1061
1093
|
this.log(`[Telegram] Voice transcribed for ${fromUser}: "${rawText.slice(0, 80)}"`);
|
|
1062
1094
|
} catch (err) {
|
|
1063
1095
|
this.log(`[Telegram] Voice transcription failed: ${err.message}`);
|
|
1064
|
-
await this.
|
|
1065
|
-
chat_id: chatId,
|
|
1066
|
-
text: `Non riesco a trascrivere il vocale (${err.message}).\n\nPer abilitare la trascrizione vocale gratuita, dal computer esegui:\nnha config set groqKey TUA_CHIAVE_GROQ\n\nLa chiave si ottiene gratis su https://console.groq.com/keys`,
|
|
1067
|
-
});
|
|
1096
|
+
await this._sendMessageSafe(chatId, `Non riesco a trascrivere il vocale (${err.message}).\n\nPer abilitare la trascrizione vocale gratuita, dal computer esegui:\nnha config set groqKey TUA_CHIAVE_GROQ\n\nLa chiave si ottiene gratis su https://console.groq.com/keys`);
|
|
1068
1097
|
return;
|
|
1069
1098
|
}
|
|
1070
1099
|
}
|
|
@@ -1082,10 +1111,7 @@ class TelegramResponder {
|
|
|
1082
1111
|
|
|
1083
1112
|
// If voice: show transcription so user knows what was understood
|
|
1084
1113
|
if (isVoice) {
|
|
1085
|
-
await this.
|
|
1086
|
-
chat_id: chatId,
|
|
1087
|
-
text: `🎤 "${cleanText}"`,
|
|
1088
|
-
}).catch(() => {});
|
|
1114
|
+
await this._sendMessageSafe(chatId, `🎤 "${cleanText}"`).catch(() => {});
|
|
1089
1115
|
}
|
|
1090
1116
|
|
|
1091
1117
|
this.pendingRequests++;
|
|
@@ -1183,7 +1209,7 @@ class TelegramResponder {
|
|
|
1183
1209
|
} else {
|
|
1184
1210
|
reply = `[HERALD]\n\n${directResult.message}`;
|
|
1185
1211
|
}
|
|
1186
|
-
await this.
|
|
1212
|
+
await this._sendMessageSafe(chatId, reply);
|
|
1187
1213
|
|
|
1188
1214
|
// Update rolling memory + reset pending action (so a follow-up
|
|
1189
1215
|
// "Si" doesn't try to delete a second time).
|
|
@@ -1310,7 +1336,7 @@ class TelegramResponder {
|
|
|
1310
1336
|
} else {
|
|
1311
1337
|
reply = `[HERALD]\n\n${directFresh.message}`;
|
|
1312
1338
|
}
|
|
1313
|
-
await this.
|
|
1339
|
+
await this._sendMessageSafe(chatId, reply);
|
|
1314
1340
|
const MAX = 20;
|
|
1315
1341
|
const prevLog = (lastCtx && Array.isArray(lastCtx.conversationLog)) ? lastCtx.conversationLog : [];
|
|
1316
1342
|
this._lastContextByChatId[chatId] = {
|
|
@@ -1437,19 +1463,13 @@ class TelegramResponder {
|
|
|
1437
1463
|
prefixedText = `[${agentLabel}]\n\n${truncated}`;
|
|
1438
1464
|
}
|
|
1439
1465
|
|
|
1440
|
-
await this.
|
|
1441
|
-
chat_id: chatId,
|
|
1442
|
-
text: prefixedText,
|
|
1443
|
-
});
|
|
1466
|
+
await this._sendMessageSafe(chatId, prefixedText);
|
|
1444
1467
|
|
|
1445
1468
|
this.log(`[Telegram] Responded to ${fromUser} via ${agentLabel}${personaName ? ` (as "${personaName}")` : ''} (${responseText.length} chars)${isCompletedAction(responseText) ? ' [action completed — context reset]' : ''}`);
|
|
1446
1469
|
} catch (err) {
|
|
1447
1470
|
this.log(`[Telegram] Agent call failed: ${err.message}`);
|
|
1448
1471
|
// Send error message to user
|
|
1449
|
-
await this.
|
|
1450
|
-
chat_id: chatId,
|
|
1451
|
-
text: `Error: ${err.message}`,
|
|
1452
|
-
}).catch(() => {});
|
|
1472
|
+
await this._sendMessageSafe(chatId, `Error: ${err.message}`).catch(() => {});
|
|
1453
1473
|
} finally {
|
|
1454
1474
|
this.pendingRequests--;
|
|
1455
1475
|
}
|
|
@@ -2598,6 +2618,63 @@ class TelegramResponder {
|
|
|
2598
2618
|
const chatId = this._lastDirectAuditChatId;
|
|
2599
2619
|
const { executeTool: _executeToolPre } = await import('./tool-executor.mjs');
|
|
2600
2620
|
|
|
2621
|
+
// ─── PAGINATION: "mostra i prossimi / vai avanti / i restanti" ─────────
|
|
2622
|
+
// Deterministic next-page fetch from the cached lastCalendarEvents instead
|
|
2623
|
+
// of letting the LLM hallucinate "the remaining events" (Giovanni
|
|
2624
|
+
// Santaniello's bug: NHA inventing fake events when asked for page 2).
|
|
2625
|
+
const isPaginationRequest = /\b(mostra(?:mi)?\s+(?:i\s+)?(?:prossimi|altri|restanti|seguenti)|vai\s+avanti|continua\s+(?:l[ai']\s+)?(?:lista|elenco)|i\s+(?:prossimi|altri|restanti|seguenti)\s+\d*|gli\s+altri|dammi\s+(?:i\s+)?(?:prossimi|restanti|altri)|next\s+page|next\s+\d+|show\s+more|continue\b)/i.test(userMessage);
|
|
2626
|
+
if (isPaginationRequest) {
|
|
2627
|
+
// Pull from list-cache (works across all kinds) — calendar first.
|
|
2628
|
+
try {
|
|
2629
|
+
const cacheFile = path.join(os.homedir(), '.nha', 'list-cache.json');
|
|
2630
|
+
if (fs.existsSync(cacheFile)) {
|
|
2631
|
+
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
2632
|
+
// Find the most recent calendar list across all chats
|
|
2633
|
+
let allItems = []; let shown = 0; let foundChatKey = null;
|
|
2634
|
+
for (const [k, v] of Object.entries(cache)) {
|
|
2635
|
+
if (Array.isArray(v?.lastList_calendar) && v.lastList_calendar.length > 0) {
|
|
2636
|
+
if (!foundChatKey || (v.lastList_calendar_at || 0) > (cache[foundChatKey]?.lastList_calendar_at || 0)) {
|
|
2637
|
+
foundChatKey = k;
|
|
2638
|
+
allItems = v.lastList_calendar;
|
|
2639
|
+
shown = v.lastList_calendar_shownCount || 0;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
if (allItems.length > 0 && shown < allItems.length) {
|
|
2644
|
+
// Page size from regex capture or default 8
|
|
2645
|
+
const numMatch = userMessage.match(/\b(\d+)\b/);
|
|
2646
|
+
const pageSize = numMatch ? Math.min(parseInt(numMatch[1], 10), 20) : 8;
|
|
2647
|
+
const nextSlice = allItems.slice(shown, shown + pageSize);
|
|
2648
|
+
const lines = [`📅 Eventi ${shown + 1}-${shown + nextSlice.length} di ${allItems.length}:`];
|
|
2649
|
+
const byDay = new Map();
|
|
2650
|
+
for (const e of nextSlice) {
|
|
2651
|
+
const day = e.date || (e.start || '').slice(0, 10) || 'misc';
|
|
2652
|
+
if (!byDay.has(day)) byDay.set(day, []);
|
|
2653
|
+
byDay.get(day).push(e);
|
|
2654
|
+
}
|
|
2655
|
+
for (const [day, evs] of [...byDay.entries()].sort()) {
|
|
2656
|
+
const d = day !== 'misc' ? new Date(day + 'T12:00:00').toLocaleDateString('it-IT', { weekday: 'short', day: 'numeric', month: 'short' }) : '';
|
|
2657
|
+
if (d) lines.push(`\n${d}:`);
|
|
2658
|
+
for (const e of evs) lines.push(` ${e.time || '—'} — ${e.summary}`);
|
|
2659
|
+
}
|
|
2660
|
+
const newShown = shown + nextSlice.length;
|
|
2661
|
+
const remaining = allItems.length - newShown;
|
|
2662
|
+
if (remaining > 0) lines.push(`\n... ${remaining} eventi rimanenti. Scrivi "mostra i prossimi" per continuare.`);
|
|
2663
|
+
else lines.push(`\n✓ Fine elenco.`);
|
|
2664
|
+
// Update shownCount in cache
|
|
2665
|
+
cache[foundChatKey].lastList_calendar_shownCount = newShown;
|
|
2666
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
2667
|
+
this.log(`[direct] PAGINATION calendar: shown ${shown}→${newShown} of ${allItems.length}`);
|
|
2668
|
+
return { action: 'calendar_page', success: true, message: lines.join('\n') };
|
|
2669
|
+
}
|
|
2670
|
+
if (allItems.length > 0 && shown >= allItems.length) {
|
|
2671
|
+
return { action: 'calendar_page', success: true, message: '✓ Hai già visto tutti gli eventi. Scrivi "appuntamenti di oggi/settimana/maggio" per una nuova ricerca.' };
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
} catch (e) { this.log(`[direct] pagination failed: ${e.message}`); }
|
|
2675
|
+
// Fall through if no cached list
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2601
2678
|
// ─── ANAPHORIC delete + CONFIRMATION yes ────────────────────────────────
|
|
2602
2679
|
// If the previous turn ran a LIST/LAST-SHOWN and the user now says
|
|
2603
2680
|
// "cancellalo / eliminalo / quello / si / conferma / fallo", resolve the
|
|
@@ -2875,6 +2952,16 @@ class TelegramResponder {
|
|
|
2875
2952
|
const runListAndRemember = async (toolName, args, actionKey) => {
|
|
2876
2953
|
try {
|
|
2877
2954
|
const out = await executeTool(toolName, args, config);
|
|
2955
|
+
// Reset pagination state for this kind: a NEW list starts fresh.
|
|
2956
|
+
try {
|
|
2957
|
+
const cacheFile = path.join(os.homedir(), '.nha', 'list-cache.json');
|
|
2958
|
+
if (fs.existsSync(cacheFile)) {
|
|
2959
|
+
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
2960
|
+
const persistKey = chatId || '__last_list__';
|
|
2961
|
+
if (cache[persistKey]) delete cache[persistKey].lastList_calendar_shownCount;
|
|
2962
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
2963
|
+
}
|
|
2964
|
+
} catch {}
|
|
2878
2965
|
// Parse from text first (cheap, works when tools include event IDs).
|
|
2879
2966
|
let events = this._parseEventsFromToolOutput(String(out));
|
|
2880
2967
|
// Fallback: tools like calendar_month don't print event IDs in their
|
|
@@ -2909,6 +2996,37 @@ class TelegramResponder {
|
|
|
2909
2996
|
};
|
|
2910
2997
|
this.log(`[direct] LIST stored: chatId=${persistKey} eventsCount=${events.length} tool=${toolName}`);
|
|
2911
2998
|
try { saveTelegramContext(this._lastContextByChatId); } catch (e) { this.log(`[direct] persist FAILED: ${e.message}`); }
|
|
2999
|
+
|
|
3000
|
+
// ── PAGINATION CAP (Giovanni's bug, v16.0.23) ──
|
|
3001
|
+
// Telegram limits messages to 4096 chars. If the list has many events,
|
|
3002
|
+
// show first 8 with footer "scrivi 'mostra i prossimi' per continuare"
|
|
3003
|
+
// and persist shownCount in list-cache so pagination is deterministic.
|
|
3004
|
+
const PAGE_SIZE = 8;
|
|
3005
|
+
if (events.length > PAGE_SIZE) {
|
|
3006
|
+
const firstPage = events.slice(0, PAGE_SIZE);
|
|
3007
|
+
const lines = [`📅 ${firstPage.length} eventi (di ${events.length} totali):`];
|
|
3008
|
+
const byDay = new Map();
|
|
3009
|
+
for (const e of firstPage) {
|
|
3010
|
+
const day = e.date || (e.start || '').slice(0, 10) || 'misc';
|
|
3011
|
+
if (!byDay.has(day)) byDay.set(day, []);
|
|
3012
|
+
byDay.get(day).push(e);
|
|
3013
|
+
}
|
|
3014
|
+
for (const [day, evs] of [...byDay.entries()].sort()) {
|
|
3015
|
+
const d = day !== 'misc' ? new Date(day + 'T12:00:00').toLocaleDateString('it-IT', { weekday: 'short', day: 'numeric', month: 'short' }) : '';
|
|
3016
|
+
if (d) lines.push(`\n${d}:`);
|
|
3017
|
+
for (const e of evs) lines.push(` ${e.time || '—'} — ${e.summary}`);
|
|
3018
|
+
}
|
|
3019
|
+
lines.push(`\n... ${events.length - PAGE_SIZE} eventi rimanenti. Scrivi "mostra i prossimi" per continuare.`);
|
|
3020
|
+
// Mark shownCount in list-cache so the pagination handler picks up from here.
|
|
3021
|
+
try {
|
|
3022
|
+
const cacheFile = path.join(os.homedir(), '.nha', 'list-cache.json');
|
|
3023
|
+
const cache = fs.existsSync(cacheFile) ? JSON.parse(fs.readFileSync(cacheFile, 'utf-8')) : {};
|
|
3024
|
+
cache[persistKey] = cache[persistKey] || {};
|
|
3025
|
+
cache[persistKey].lastList_calendar_shownCount = PAGE_SIZE;
|
|
3026
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
3027
|
+
} catch {}
|
|
3028
|
+
return { action: actionKey, success: true, message: lines.join('\n') };
|
|
3029
|
+
}
|
|
2912
3030
|
return { action: actionKey, success: true, message: String(out) };
|
|
2913
3031
|
} catch (e) { return { action: actionKey, success: false, message: `Errore: ${e.message}` }; }
|
|
2914
3032
|
};
|