nothumanallowed 16.0.21 → 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
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
|
|
|
@@ -752,7 +752,9 @@ class TelegramResponder {
|
|
|
752
752
|
async start() {
|
|
753
753
|
if (!this.enabled) return;
|
|
754
754
|
this.running = true;
|
|
755
|
-
|
|
755
|
+
// Explicit version log at boot so the user can verify what's running.
|
|
756
|
+
// Critical when chasing "bot still mentes" — answers "are you on v16.0.21?".
|
|
757
|
+
this.log(`[Telegram] Responder started — VERSION ${VERSION} — polling for messages`);
|
|
756
758
|
this._pollLoop();
|
|
757
759
|
// Check for npm updates after 60s, then every 24h
|
|
758
760
|
this._updateCheckTimer = setTimeout(() => this._scheduleUpdateCheck(), 60 * 1000);
|
|
@@ -772,10 +774,80 @@ class TelegramResponder {
|
|
|
772
774
|
this.log('[Telegram] Responder stopped');
|
|
773
775
|
}
|
|
774
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
|
+
|
|
775
814
|
async _scheduleUpdateCheck() {
|
|
776
815
|
await this._checkAndNotifyUpdate();
|
|
777
|
-
|
|
816
|
+
await this._checkLocalUpdateAndRestart();
|
|
817
|
+
// Then every 24h for the npm registry check + every 5 min for local install check
|
|
778
818
|
this._updateCheckTimer = setInterval(() => this._checkAndNotifyUpdate(), 24 * 60 * 60 * 1000);
|
|
819
|
+
setInterval(() => this._checkLocalUpdateAndRestart(), 5 * 60 * 1000);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Detect that a NEW version of nha-cli has been installed on disk while
|
|
824
|
+
* this process is still running the OLD code in memory. When detected,
|
|
825
|
+
* exit cleanly so PM2 / launchd / the dispatcher respawns us on the
|
|
826
|
+
* latest code. Without this, the user runs `npm i -g nothumanallowed@latest`
|
|
827
|
+
* but the Telegram bot keeps mentes-ing with the old logic forever.
|
|
828
|
+
*/
|
|
829
|
+
async _checkLocalUpdateAndRestart() {
|
|
830
|
+
try {
|
|
831
|
+
const fileURL = await import('url');
|
|
832
|
+
const here = fileURL.fileURLToPath(import.meta.url);
|
|
833
|
+
// ../../package.json relative to this file (services/message-responder.mjs)
|
|
834
|
+
const pkgPath = path.join(path.dirname(here), '..', '..', 'package.json');
|
|
835
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
836
|
+
const onDisk = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
837
|
+
if (!onDisk || onDisk === VERSION) return;
|
|
838
|
+
this.log(`[Telegram] Detected new install: running v${VERSION}, on-disk v${onDisk}. Restarting to pick up new code…`);
|
|
839
|
+
// Notify any active chat that we're restarting (best-effort, fire and forget)
|
|
840
|
+
try {
|
|
841
|
+
const chatIds = getAllTelegramChatIds();
|
|
842
|
+
for (const chatId of chatIds.slice(0, 3)) {
|
|
843
|
+
this._telegramCall('sendMessage', { chat_id: parseInt(chatId, 10), text: `🔄 Aggiornamento NHA v${VERSION} → v${onDisk} in corso. Torno tra 2 secondi.` }).catch(() => {});
|
|
844
|
+
}
|
|
845
|
+
} catch {}
|
|
846
|
+
// Give the message a moment to flush, then exit. PM2 / dispatcher restarts us.
|
|
847
|
+
setTimeout(() => process.exit(0), 800);
|
|
848
|
+
} catch (e) {
|
|
849
|
+
this.log(`[Telegram] Local update check failed: ${e.message}`);
|
|
850
|
+
}
|
|
779
851
|
}
|
|
780
852
|
|
|
781
853
|
async _checkAndNotifyUpdate() {
|
|
@@ -799,14 +871,10 @@ class TelegramResponder {
|
|
|
799
871
|
|
|
800
872
|
for (const chatId of chatIds) {
|
|
801
873
|
try {
|
|
802
|
-
await this.
|
|
803
|
-
chat_id: parseInt(chatId, 10),
|
|
804
|
-
text: msg,
|
|
805
|
-
});
|
|
874
|
+
await this._sendMessageSafe(parseInt(chatId, 10), msg);
|
|
806
875
|
} catch {
|
|
807
876
|
// User blocked bot or chat no longer exists — ignore
|
|
808
877
|
}
|
|
809
|
-
// Small delay to avoid Telegram rate limits
|
|
810
878
|
await this._sleep(300);
|
|
811
879
|
}
|
|
812
880
|
} catch (err) {
|
|
@@ -1002,12 +1070,11 @@ class TelegramResponder {
|
|
|
1002
1070
|
const prefix = personaMode === 'persona-only' && personaName ? ''
|
|
1003
1071
|
: personaName ? `[${personaName}]\n\n`
|
|
1004
1072
|
: `[HERALD]\n\n`;
|
|
1005
|
-
await this.
|
|
1073
|
+
await this._sendMessageSafe(chatId, prefix + description);
|
|
1006
1074
|
this.log(`[Telegram] Image vision response to ${fromUser} (${buf.length} bytes, ${description.length} chars)`);
|
|
1007
1075
|
} catch (err) {
|
|
1008
1076
|
this.log(`[Telegram] Vision failed: ${err.message}`);
|
|
1009
|
-
await this.
|
|
1010
|
-
text: `Non riesco ad analizzare l'immagine: ${err.message}` }).catch(() => {});
|
|
1077
|
+
await this._sendMessageSafe(chatId, `Non riesco ad analizzare l'immagine: ${err.message}`).catch(() => {});
|
|
1011
1078
|
}
|
|
1012
1079
|
return;
|
|
1013
1080
|
}
|
|
@@ -1020,16 +1087,13 @@ class TelegramResponder {
|
|
|
1020
1087
|
await this._telegramCall('sendChatAction', { chat_id: chatId, action: 'typing' });
|
|
1021
1088
|
rawText = await this._transcribeVoice(fileId);
|
|
1022
1089
|
if (!rawText.trim()) {
|
|
1023
|
-
await this.
|
|
1090
|
+
await this._sendMessageSafe(chatId, 'Non ho capito il vocale. Riprova.');
|
|
1024
1091
|
return;
|
|
1025
1092
|
}
|
|
1026
1093
|
this.log(`[Telegram] Voice transcribed for ${fromUser}: "${rawText.slice(0, 80)}"`);
|
|
1027
1094
|
} catch (err) {
|
|
1028
1095
|
this.log(`[Telegram] Voice transcription failed: ${err.message}`);
|
|
1029
|
-
await this.
|
|
1030
|
-
chat_id: chatId,
|
|
1031
|
-
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`,
|
|
1032
|
-
});
|
|
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`);
|
|
1033
1097
|
return;
|
|
1034
1098
|
}
|
|
1035
1099
|
}
|
|
@@ -1047,10 +1111,7 @@ class TelegramResponder {
|
|
|
1047
1111
|
|
|
1048
1112
|
// If voice: show transcription so user knows what was understood
|
|
1049
1113
|
if (isVoice) {
|
|
1050
|
-
await this.
|
|
1051
|
-
chat_id: chatId,
|
|
1052
|
-
text: `🎤 "${cleanText}"`,
|
|
1053
|
-
}).catch(() => {});
|
|
1114
|
+
await this._sendMessageSafe(chatId, `🎤 "${cleanText}"`).catch(() => {});
|
|
1054
1115
|
}
|
|
1055
1116
|
|
|
1056
1117
|
this.pendingRequests++;
|
|
@@ -1148,7 +1209,7 @@ class TelegramResponder {
|
|
|
1148
1209
|
} else {
|
|
1149
1210
|
reply = `[HERALD]\n\n${directResult.message}`;
|
|
1150
1211
|
}
|
|
1151
|
-
await this.
|
|
1212
|
+
await this._sendMessageSafe(chatId, reply);
|
|
1152
1213
|
|
|
1153
1214
|
// Update rolling memory + reset pending action (so a follow-up
|
|
1154
1215
|
// "Si" doesn't try to delete a second time).
|
|
@@ -1275,7 +1336,7 @@ class TelegramResponder {
|
|
|
1275
1336
|
} else {
|
|
1276
1337
|
reply = `[HERALD]\n\n${directFresh.message}`;
|
|
1277
1338
|
}
|
|
1278
|
-
await this.
|
|
1339
|
+
await this._sendMessageSafe(chatId, reply);
|
|
1279
1340
|
const MAX = 20;
|
|
1280
1341
|
const prevLog = (lastCtx && Array.isArray(lastCtx.conversationLog)) ? lastCtx.conversationLog : [];
|
|
1281
1342
|
this._lastContextByChatId[chatId] = {
|
|
@@ -1402,19 +1463,13 @@ class TelegramResponder {
|
|
|
1402
1463
|
prefixedText = `[${agentLabel}]\n\n${truncated}`;
|
|
1403
1464
|
}
|
|
1404
1465
|
|
|
1405
|
-
await this.
|
|
1406
|
-
chat_id: chatId,
|
|
1407
|
-
text: prefixedText,
|
|
1408
|
-
});
|
|
1466
|
+
await this._sendMessageSafe(chatId, prefixedText);
|
|
1409
1467
|
|
|
1410
1468
|
this.log(`[Telegram] Responded to ${fromUser} via ${agentLabel}${personaName ? ` (as "${personaName}")` : ''} (${responseText.length} chars)${isCompletedAction(responseText) ? ' [action completed — context reset]' : ''}`);
|
|
1411
1469
|
} catch (err) {
|
|
1412
1470
|
this.log(`[Telegram] Agent call failed: ${err.message}`);
|
|
1413
1471
|
// Send error message to user
|
|
1414
|
-
await this.
|
|
1415
|
-
chat_id: chatId,
|
|
1416
|
-
text: `Error: ${err.message}`,
|
|
1417
|
-
}).catch(() => {});
|
|
1472
|
+
await this._sendMessageSafe(chatId, `Error: ${err.message}`).catch(() => {});
|
|
1418
1473
|
} finally {
|
|
1419
1474
|
this.pendingRequests--;
|
|
1420
1475
|
}
|
|
@@ -2563,6 +2618,63 @@ class TelegramResponder {
|
|
|
2563
2618
|
const chatId = this._lastDirectAuditChatId;
|
|
2564
2619
|
const { executeTool: _executeToolPre } = await import('./tool-executor.mjs');
|
|
2565
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
|
+
|
|
2566
2678
|
// ─── ANAPHORIC delete + CONFIRMATION yes ────────────────────────────────
|
|
2567
2679
|
// If the previous turn ran a LIST/LAST-SHOWN and the user now says
|
|
2568
2680
|
// "cancellalo / eliminalo / quello / si / conferma / fallo", resolve the
|
|
@@ -2840,6 +2952,16 @@ class TelegramResponder {
|
|
|
2840
2952
|
const runListAndRemember = async (toolName, args, actionKey) => {
|
|
2841
2953
|
try {
|
|
2842
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 {}
|
|
2843
2965
|
// Parse from text first (cheap, works when tools include event IDs).
|
|
2844
2966
|
let events = this._parseEventsFromToolOutput(String(out));
|
|
2845
2967
|
// Fallback: tools like calendar_month don't print event IDs in their
|
|
@@ -2874,6 +2996,37 @@ class TelegramResponder {
|
|
|
2874
2996
|
};
|
|
2875
2997
|
this.log(`[direct] LIST stored: chatId=${persistKey} eventsCount=${events.length} tool=${toolName}`);
|
|
2876
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
|
+
}
|
|
2877
3030
|
return { action: actionKey, success: true, message: String(out) };
|
|
2878
3031
|
} catch (e) { return { action: actionKey, success: false, message: `Errore: ${e.message}` }; }
|
|
2879
3032
|
};
|
|
@@ -2018,11 +2018,45 @@ export async function executeTool(action, params, config) {
|
|
|
2018
2018
|
|
|
2019
2019
|
case 'calendar_move': {
|
|
2020
2020
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2021
|
+
// ── Resolve eventId from optional query/oldDate (v16.0.22) ──
|
|
2022
|
+
// Agent models often pass {query: 'BMW', newStart} without an eventId.
|
|
2023
|
+
// Instead of failing, resolve it here via listEvents so the move
|
|
2024
|
+
// actually happens. Returns honest error if multiple/no matches.
|
|
2025
|
+
let eventId = params.eventId;
|
|
2026
|
+
if (!eventId && (params.query || params.title || params.oldDate)) {
|
|
2027
|
+
const range = (() => {
|
|
2028
|
+
if (params.oldDate && /^\d{4}-\d{2}-\d{2}$/.test(params.oldDate)) {
|
|
2029
|
+
const [yy, mm, dd] = params.oldDate.split('-').map(n => parseInt(n, 10));
|
|
2030
|
+
const from = new Date(yy, mm - 1, dd);
|
|
2031
|
+
return { from, to: new Date(from.getTime() + 86400000) };
|
|
2032
|
+
}
|
|
2033
|
+
const from = new Date();
|
|
2034
|
+
return { from, to: new Date(from.getTime() + 90 * 86400000) };
|
|
2035
|
+
})();
|
|
2036
|
+
const candidates = await listEvents(config, 'primary', range.from, range.to);
|
|
2037
|
+
const q = String(params.query || params.title || '').toLowerCase();
|
|
2038
|
+
const matching = q
|
|
2039
|
+
? candidates.filter(e => String(e.summary || '').toLowerCase().includes(q))
|
|
2040
|
+
: candidates;
|
|
2041
|
+
if (matching.length === 0) {
|
|
2042
|
+
return `Error: no event found matching "${q || params.oldDate}" — calendar_move did NOT execute.`;
|
|
2043
|
+
}
|
|
2044
|
+
if (matching.length > 1) {
|
|
2045
|
+
const list = matching.slice(0, 5).map((e, i) =>
|
|
2046
|
+
`${i + 1}. ${(e.start || '').slice(0, 10)} ${e.summary}`).join('\n');
|
|
2047
|
+
return `Error: multiple events match "${q}" — be more specific. Candidates:\n${list}`;
|
|
2048
|
+
}
|
|
2049
|
+
eventId = matching[0].id;
|
|
2050
|
+
}
|
|
2051
|
+
if (!eventId) return 'Error: eventId or query/title required.';
|
|
2052
|
+
if (!params.newStart) return 'Error: newStart required.';
|
|
2053
|
+
const newStartISO = new Date(params.newStart).toISOString();
|
|
2054
|
+
const newEndISO = new Date(params.newEnd || (new Date(params.newStart).getTime() + 3600000)).toISOString();
|
|
2055
|
+
await updateEvent(config, 'primary', eventId, {
|
|
2056
|
+
start: { dateTime: newStartISO, timeZone: tz },
|
|
2057
|
+
end: { dateTime: newEndISO, timeZone: tz },
|
|
2024
2058
|
});
|
|
2025
|
-
return `Event rescheduled to ${formatTime(
|
|
2059
|
+
return `Event rescheduled to ${formatTime(newStartISO)} - ${formatTime(newEndISO)}.`;
|
|
2026
2060
|
}
|
|
2027
2061
|
|
|
2028
2062
|
case 'calendar_week': {
|