nothumanallowed 15.1.9 → 15.1.11
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/commands/ui.mjs +33 -2
- package/src/constants.mjs +1 -1
- package/src/server/index.mjs +29 -4
- package/src/server/routes/config.mjs +66 -34
- package/src/services/message-responder.mjs +57 -3
- package/src/services/tool-executor.mjs +153 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.11",
|
|
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/commands/ui.mjs
CHANGED
|
@@ -84,11 +84,42 @@ export async function cmdUI(args) {
|
|
|
84
84
|
}
|
|
85
85
|
} catch { /* port is free */ }
|
|
86
86
|
|
|
87
|
-
//
|
|
87
|
+
// Detect if we were spawned by the self-restart helper after an npm-update.
|
|
88
|
+
// The marker file is written by /api/npm-update right before exiting.
|
|
89
|
+
// This lets us:
|
|
90
|
+
// - confirm to the user that the upgrade landed (visible in the boot log)
|
|
91
|
+
// - guarantee the ops daemon (Telegram/Discord bot) is back up
|
|
92
|
+
let restartedFromUpdate = false;
|
|
93
|
+
try {
|
|
94
|
+
const fs = await import('fs');
|
|
95
|
+
const path = await import('path');
|
|
96
|
+
const os = await import('os');
|
|
97
|
+
const markerPath = path.join(os.default.homedir(), '.nha', 'last-restart.json');
|
|
98
|
+
if (fs.existsSync(markerPath)) {
|
|
99
|
+
const stat = fs.statSync(markerPath);
|
|
100
|
+
// Consider the marker fresh only if written within the last 5 minutes
|
|
101
|
+
if (Date.now() - stat.mtimeMs < 5 * 60_000) {
|
|
102
|
+
const meta = JSON.parse(fs.readFileSync(markerPath, 'utf-8'));
|
|
103
|
+
restartedFromUpdate = true;
|
|
104
|
+
console.log(` \x1b[0;32m↻\x1b[0m Restarted after update: v${meta.fromVersion} → v${meta.toVersion}`);
|
|
105
|
+
}
|
|
106
|
+
// Always clean up so we don't repeat the message on next manual restart
|
|
107
|
+
try { fs.unlinkSync(markerPath); } catch {}
|
|
108
|
+
}
|
|
109
|
+
} catch { /* non-fatal */ }
|
|
110
|
+
|
|
111
|
+
// Auto-start ops daemon (Telegram + cron) if not already running.
|
|
112
|
+
// After a self-restart this is what brings the Telegram bot back online —
|
|
113
|
+
// critical for VM users whose only control channel is Telegram.
|
|
88
114
|
if (!isRunning()) {
|
|
89
115
|
const result = startDaemon();
|
|
90
116
|
if (result.ok) {
|
|
91
|
-
console.log(` \x1b[0;32m✓\x1b[0m PAO daemon started (PID ${result.pid})`);
|
|
117
|
+
console.log(` \x1b[0;32m✓\x1b[0m PAO daemon ${restartedFromUpdate ? 'restarted' : 'started'} (PID ${result.pid})`);
|
|
118
|
+
} else {
|
|
119
|
+
console.log(` \x1b[0;31m✗\x1b[0m PAO daemon failed to start: ${result.error || 'unknown'}`);
|
|
120
|
+
if (restartedFromUpdate) {
|
|
121
|
+
console.log(` Telegram/Discord bot may be offline. Run "nha ops start" manually.`);
|
|
122
|
+
}
|
|
92
123
|
}
|
|
93
124
|
}
|
|
94
125
|
|
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.11';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/server/index.mjs
CHANGED
|
@@ -376,15 +376,40 @@ export async function startServer({ port = 3847, host = '127.0.0.1', noBrowser =
|
|
|
376
376
|
|
|
377
377
|
const server = http.createServer(handleRequest);
|
|
378
378
|
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
// Retry the bind for up to 12 s on EADDRINUSE. The classic case is a fresh
|
|
380
|
+
// self-restart right after the previous process died: the listen socket can
|
|
381
|
+
// be stuck in TIME_WAIT (no PID owns it, so the lsof-kill above can't help).
|
|
382
|
+
// Linear backoff 600 ms × 20 = 12 s — more than enough on every kernel I've
|
|
383
|
+
// seen for a localhost listen socket to clear.
|
|
384
|
+
const tryListen = () => new Promise((resolve, reject) => {
|
|
385
|
+
const onErr = (err) => reject(err);
|
|
386
|
+
server.once('error', onErr);
|
|
382
387
|
server.listen(port, host, () => {
|
|
383
|
-
server.removeListener('error',
|
|
388
|
+
server.removeListener('error', onErr);
|
|
384
389
|
resolve(undefined);
|
|
385
390
|
});
|
|
386
391
|
});
|
|
387
392
|
|
|
393
|
+
let listenAttempts = 0;
|
|
394
|
+
const MAX_LISTEN_ATTEMPTS = 20;
|
|
395
|
+
while (true) {
|
|
396
|
+
try {
|
|
397
|
+
await tryListen();
|
|
398
|
+
break;
|
|
399
|
+
} catch (err) {
|
|
400
|
+
listenAttempts++;
|
|
401
|
+
const isAddrInUse = err && (err.code === 'EADDRINUSE' || err.code === 'EACCES');
|
|
402
|
+
if (!isAddrInUse || listenAttempts >= MAX_LISTEN_ATTEMPTS) {
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
// Brief notice every 3 s so the user knows we're not stuck
|
|
406
|
+
if (listenAttempts % 5 === 0) {
|
|
407
|
+
console.log(`\x1b[33m ! Port ${port} still busy (TIME_WAIT?), retry ${listenAttempts}/${MAX_LISTEN_ATTEMPTS}...\x1b[0m`);
|
|
408
|
+
}
|
|
409
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
388
413
|
setupWebSocket(server);
|
|
389
414
|
|
|
390
415
|
const G = '\x1b[0;32m', NC = '\x1b[0m', D = '\x1b[2m', BOLD = '\x1b[1m', Y = '\x1b[33m', R = '\x1b[31m';
|
|
@@ -146,10 +146,17 @@ export function register(router) {
|
|
|
146
146
|
}
|
|
147
147
|
} catch { /* ignore */ }
|
|
148
148
|
|
|
149
|
-
//
|
|
149
|
+
// Remember if the ops daemon (Telegram/Discord bot) was running so the
|
|
150
|
+
// newly-spawned server can restart it. Without this, a VM user who
|
|
151
|
+
// controls everything via Telegram would lose the bot until they ran
|
|
152
|
+
// `nha ops start` manually.
|
|
153
|
+
let daemonWasRunning = false;
|
|
150
154
|
try {
|
|
151
155
|
const { isRunning, stopDaemon } = await import('../../services/ops-daemon.mjs');
|
|
152
|
-
if (isRunning())
|
|
156
|
+
if (isRunning()) {
|
|
157
|
+
daemonWasRunning = true;
|
|
158
|
+
await stopDaemon();
|
|
159
|
+
}
|
|
153
160
|
} catch { /* ignore */ }
|
|
154
161
|
|
|
155
162
|
// Send the response NOW, before we kick off the restart. The client
|
|
@@ -166,62 +173,87 @@ export function register(router) {
|
|
|
166
173
|
});
|
|
167
174
|
|
|
168
175
|
// ── Schedule the self-restart ──
|
|
169
|
-
// We do this OUTSIDE the response cycle: a detached child that survives
|
|
170
|
-
// our death, watches our PID, then re-spawns with the same argv.
|
|
171
|
-
// On POSIX we use `sh -c`. On Windows we use a detached spawn directly
|
|
172
|
-
// because cmd.exe handles the "wait for PID" idiom differently.
|
|
173
176
|
const parentPid = process.pid;
|
|
174
|
-
const respawnNode = process.execPath;
|
|
175
|
-
const respawnArgs = process.argv.slice(1);
|
|
176
|
-
const
|
|
177
|
+
const respawnNode = process.execPath;
|
|
178
|
+
const respawnArgs = process.argv.slice(1);
|
|
179
|
+
const home = os.homedir();
|
|
180
|
+
const nhaDir = path.join(home, '.nha');
|
|
181
|
+
if (!fs.existsSync(nhaDir)) fs.mkdirSync(nhaDir, { recursive: true });
|
|
182
|
+
|
|
183
|
+
// Write the restart marker BEFORE spawning the helper. The new server
|
|
184
|
+
// reads this on boot to know:
|
|
185
|
+
// - the update happened (so it can show "Updated to v…" on first load)
|
|
186
|
+
// - whether the ops daemon (Telegram bot) was running and must be restarted
|
|
187
|
+
const markerPath = path.join(nhaDir, 'last-restart.json');
|
|
188
|
+
try {
|
|
189
|
+
fs.writeFileSync(
|
|
190
|
+
markerPath,
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
at: new Date().toISOString(),
|
|
193
|
+
fromVersion: VERSION,
|
|
194
|
+
toVersion: newVersion,
|
|
195
|
+
daemonWasRunning,
|
|
196
|
+
parentPid,
|
|
197
|
+
}, null, 2),
|
|
198
|
+
);
|
|
199
|
+
} catch { /* non-fatal */ }
|
|
200
|
+
|
|
201
|
+
// Open log file for the detached child — if anything goes wrong on boot
|
|
202
|
+
// we want the user to be able to read why instead of staring at a black
|
|
203
|
+
// screen. Append mode so previous restart logs survive.
|
|
204
|
+
const logPath = path.join(nhaDir, 'server.log');
|
|
205
|
+
let logFd;
|
|
206
|
+
try {
|
|
207
|
+
logFd = fs.openSync(logPath, 'a');
|
|
208
|
+
fs.writeSync(logFd, `\n[${new Date().toISOString()}] === Restart helper spawning. Parent PID: ${parentPid}. Target version: ${newVersion} ===\n`);
|
|
209
|
+
} catch { /* will fall back to 'ignore' */ }
|
|
177
210
|
|
|
178
211
|
setTimeout(() => {
|
|
179
212
|
try {
|
|
180
213
|
if (process.platform === 'win32') {
|
|
181
|
-
// Windows:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
Start-
|
|
186
|
-
|
|
214
|
+
// Windows: PowerShell waits for the parent to die, frees the port,
|
|
215
|
+
// then starts node hidden. Output is redirected to server.log.
|
|
216
|
+
const psScript = [
|
|
217
|
+
`try { Wait-Process -Id ${parentPid} -Timeout 30 } catch {};`,
|
|
218
|
+
`Start-Sleep -Milliseconds 1500;`,
|
|
219
|
+
`$logPath = "${logPath.replace(/\\/g, '\\\\')}";`,
|
|
220
|
+
`Start-Process -FilePath "${respawnNode.replace(/\\/g, '\\\\')}"`,
|
|
221
|
+
` -ArgumentList ${JSON.stringify(respawnArgs).replace(/"/g, '\\"')}`,
|
|
222
|
+
` -RedirectStandardOutput $logPath -RedirectStandardError $logPath`,
|
|
223
|
+
` -WindowStyle Hidden`,
|
|
224
|
+
].join('').replace(/\s+/g, ' ').trim();
|
|
187
225
|
spawn('powershell.exe', ['-NoProfile', '-Command', psScript], {
|
|
188
226
|
detached: true,
|
|
189
227
|
stdio: 'ignore',
|
|
190
228
|
windowsHide: true,
|
|
191
229
|
}).unref();
|
|
192
230
|
} else {
|
|
193
|
-
// POSIX:
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
const
|
|
231
|
+
// POSIX: poll for parent death (max ~12s), then wait an extra 2s
|
|
232
|
+
// for TIME_WAIT to clear on the listen socket, then exec node
|
|
233
|
+
// with stdio redirected to the log file.
|
|
234
|
+
const escArg = a => `'${a.replace(/'/g, `'\\''`)}'`;
|
|
235
|
+
const escapedArgs = respawnArgs.map(escArg).join(' ');
|
|
236
|
+
const escapedNode = escArg(respawnNode);
|
|
237
|
+
const escapedLog = escArg(logPath);
|
|
197
238
|
const sh = `
|
|
198
239
|
i=0
|
|
199
240
|
while kill -0 ${parentPid} 2>/dev/null && [ $i -lt 60 ]; do
|
|
200
241
|
sleep 0.2
|
|
201
242
|
i=$((i+1))
|
|
202
243
|
done
|
|
203
|
-
|
|
204
|
-
|
|
244
|
+
# Extra wait: lets TIME_WAIT on the listen socket release and
|
|
245
|
+
# gives the kernel a moment to fully reap the parent.
|
|
246
|
+
sleep 2
|
|
247
|
+
exec ${escapedNode} ${escapedArgs} >> ${escapedLog} 2>&1
|
|
205
248
|
`;
|
|
249
|
+
const childStdio = logFd ? ['ignore', logFd, logFd] : 'ignore';
|
|
206
250
|
spawn('sh', ['-c', sh], {
|
|
207
251
|
detached: true,
|
|
208
|
-
stdio:
|
|
252
|
+
stdio: childStdio,
|
|
209
253
|
}).unref();
|
|
210
254
|
}
|
|
211
|
-
|
|
212
|
-
// Mark the restart so the next process knows it was self-spawned
|
|
213
|
-
try {
|
|
214
|
-
const home = os.homedir();
|
|
215
|
-
const nhaDir = path.join(home, '.nha');
|
|
216
|
-
if (!fs.existsSync(nhaDir)) fs.mkdirSync(nhaDir, { recursive: true });
|
|
217
|
-
fs.writeFileSync(
|
|
218
|
-
path.join(nhaDir, 'last-restart.json'),
|
|
219
|
-
JSON.stringify({ at: new Date().toISOString(), fromVersion: VERSION, toVersion: newVersion, meta: argvJson }, null, 2),
|
|
220
|
-
);
|
|
221
|
-
} catch { /* non-fatal */ }
|
|
222
255
|
} catch { /* spawn failed — user will need to restart manually */ }
|
|
223
256
|
|
|
224
|
-
// Give the response a moment, then exit so the helper can take over.
|
|
225
257
|
setTimeout(() => process.exit(0), 200);
|
|
226
258
|
}, 600);
|
|
227
259
|
} catch (e) {
|
|
@@ -374,6 +374,40 @@ async function checkNpmVersion() {
|
|
|
374
374
|
|
|
375
375
|
// ── Telegram Bot (Long Polling via native fetch) ─────────────────────────────
|
|
376
376
|
|
|
377
|
+
// Persistent storage for per-chat conversational state. Without this, every
|
|
378
|
+
// process restart (npm self-update, crash, "nha ops stop && start") loses the
|
|
379
|
+
// sticky-agent + turn history, and the user's next "Procedi" / "Si" routes
|
|
380
|
+
// to a random agent because the planner has no thread to anchor on.
|
|
381
|
+
const TELEGRAM_CTX_FILE = path.join(os.homedir(), '.nha', 'telegram-context.json');
|
|
382
|
+
const TELEGRAM_CTX_MAX_AGE_MS = 30 * 60 * 1000; // 30 min — older than that, discard
|
|
383
|
+
|
|
384
|
+
function loadTelegramContext() {
|
|
385
|
+
try {
|
|
386
|
+
if (!fs.existsSync(TELEGRAM_CTX_FILE)) return {};
|
|
387
|
+
const raw = JSON.parse(fs.readFileSync(TELEGRAM_CTX_FILE, 'utf-8'));
|
|
388
|
+
const now = Date.now();
|
|
389
|
+
const fresh = {};
|
|
390
|
+
for (const [chatId, ctx] of Object.entries(raw)) {
|
|
391
|
+
if (ctx && typeof ctx.ts === 'number' && (now - ctx.ts) < TELEGRAM_CTX_MAX_AGE_MS) {
|
|
392
|
+
fresh[chatId] = ctx;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return fresh;
|
|
396
|
+
} catch { return {}; }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function saveTelegramContext(byChatId) {
|
|
400
|
+
try {
|
|
401
|
+
const dir = path.dirname(TELEGRAM_CTX_FILE);
|
|
402
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
403
|
+
// Atomic write — temp file + rename so a crash during write doesn't leave
|
|
404
|
+
// half-written JSON that breaks the next boot.
|
|
405
|
+
const tmp = TELEGRAM_CTX_FILE + '.tmp';
|
|
406
|
+
fs.writeFileSync(tmp, JSON.stringify(byChatId, null, 2));
|
|
407
|
+
fs.renameSync(tmp, TELEGRAM_CTX_FILE);
|
|
408
|
+
} catch { /* non-fatal */ }
|
|
409
|
+
}
|
|
410
|
+
|
|
377
411
|
class TelegramResponder {
|
|
378
412
|
constructor(config, log, wsBroadcast) {
|
|
379
413
|
this.config = config;
|
|
@@ -389,9 +423,25 @@ class TelegramResponder {
|
|
|
389
423
|
this.maxConcurrent = 3;
|
|
390
424
|
this._updateCheckTimer = null;
|
|
391
425
|
this._lastNotifiedVersion = null;
|
|
392
|
-
// Per-chat sticky agent
|
|
393
|
-
|
|
394
|
-
this._lastContextByChatId =
|
|
426
|
+
// Per-chat sticky agent — restored from disk so self-restarts and npm
|
|
427
|
+
// updates don't break in-flight conversations.
|
|
428
|
+
this._lastContextByChatId = loadTelegramContext();
|
|
429
|
+
this._lastAgentByChatId = {};
|
|
430
|
+
for (const [chatId, ctx] of Object.entries(this._lastContextByChatId)) {
|
|
431
|
+
if (ctx && ctx.agent) this._lastAgentByChatId[chatId] = ctx.agent;
|
|
432
|
+
}
|
|
433
|
+
const restoredCount = Object.keys(this._lastContextByChatId).length;
|
|
434
|
+
if (restoredCount > 0) {
|
|
435
|
+
this.log(`[Telegram] Restored conversational context for ${restoredCount} chat(s) from disk`);
|
|
436
|
+
}
|
|
437
|
+
this._saveCtxTimer = null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
_persistContext() {
|
|
441
|
+
// Synchronous write — guarantees the context is on disk even if the
|
|
442
|
+
// process exits immediately after (npm self-update, SIGTERM, crash).
|
|
443
|
+
// The file is tiny (< 10 KB typical), so this stays sub-millisecond.
|
|
444
|
+
saveTelegramContext(this._lastContextByChatId);
|
|
395
445
|
}
|
|
396
446
|
|
|
397
447
|
get enabled() {
|
|
@@ -718,6 +768,10 @@ class TelegramResponder {
|
|
|
718
768
|
ts: Date.now(),
|
|
719
769
|
};
|
|
720
770
|
this._lastAgentByChatId[chatId] = agent;
|
|
771
|
+
// Persist to disk so the context survives npm self-update / crashes.
|
|
772
|
+
// Without this, a restart between "HERALD proposes" and user's "Procedi"
|
|
773
|
+
// routes the confirmation to a random agent (FORGE in the reported bug).
|
|
774
|
+
this._persistContext();
|
|
721
775
|
|
|
722
776
|
await this._telegramCall('sendMessage', {
|
|
723
777
|
chat_id: chatId,
|
|
@@ -163,8 +163,18 @@ TOOLS:
|
|
|
163
163
|
14. calendar_create(summary: string, start: string, end: string, attendees?: string[], description?: string)
|
|
164
164
|
Create a NEW calendar event. start/end are ISO 8601 datetime strings.
|
|
165
165
|
Use this when the user says: "inserisci", "aggiungi", "crea", "metti", "fissa", "prenota", "add", "create", "schedule", "book".
|
|
166
|
+
|
|
167
|
+
PARAMETER MEANING (CRITICAL — most common mistake):
|
|
168
|
+
- summary = the TITLE of the event (what you'd write on a calendar grid). Short, like "Tagliando BMW", "Riunione Cliente X", "Cena con Marta".
|
|
169
|
+
- description = optional NOTES/details. Leave empty unless the user gave extra context that doesn't fit in the title.
|
|
170
|
+
- NEVER put the title in description. NEVER leave summary empty.
|
|
171
|
+
|
|
172
|
+
Example — user says "fissa tagliando BMW per il 15 maggio alle 17":
|
|
173
|
+
{ "action": "calendar_create", "params": { "summary": "Tagliando BMW", "start": "2026-05-15T17:00:00", "end": "2026-05-15T18:00:00" } }
|
|
174
|
+
|
|
166
175
|
IMPORTANT: When user says "inserisci appuntamento" or "crea evento" → use calendar_create, NOT calendar_find.
|
|
167
176
|
Extract the summary, date, and time from the user message. If end time is not specified, default to 1 hour after start.
|
|
177
|
+
The tool RESPONSE will include "(eventId: ABC123)" — REMEMBER this ID. If the user later says "correggi", "modifica", "cambia", "sposta", "elimina" referring to this same event, use calendar_update / calendar_move / calendar_delete with that exact eventId — do NOT create a second event.
|
|
168
178
|
|
|
169
179
|
15. calendar_move(eventId: string, newStart: string, newEnd: string)
|
|
170
180
|
Reschedule an event. ALWAYS confirm before moving.
|
|
@@ -178,7 +188,16 @@ TOOLS:
|
|
|
178
188
|
|
|
179
189
|
18. calendar_update(eventId: string, summary?: string, location?: string, description?: string, start?: string, end?: string)
|
|
180
190
|
Update ANY field of an existing calendar event: title, location, description, start time, end time.
|
|
181
|
-
|
|
191
|
+
Only include fields that need to change.
|
|
192
|
+
|
|
193
|
+
HOW TO GET eventId — IN ORDER OF PREFERENCE:
|
|
194
|
+
1. From your OWN previous tool response in this conversation: when you ran calendar_create earlier, the response said "Event \"X\" created ... (eventId: ABC123)". Use that ABC123 directly. Do NOT search again.
|
|
195
|
+
2. If step 1 doesn't apply (event was created before this conversation), call calendar_find FIRST to look it up.
|
|
196
|
+
|
|
197
|
+
CRITICAL — "CORREGGI" / "MODIFICA" / "CAMBIA TITOLO" mappings:
|
|
198
|
+
When the user says "correggi", "modifica", "cambia", "rinomina", "sposta", "aggiorna" referring to the most recent event you just created, you MUST use calendar_update with the eventId from step 1 above.
|
|
199
|
+
Do NOT call calendar_create a second time — that would create a DUPLICATE event.
|
|
200
|
+
Example: you just created event ABC123 ("Visita BMW"). User says "correggi, il titolo è Tagliando BMW" → call calendar_update(eventId: "ABC123", summary: "Tagliando BMW"). Don't create a new event, don't delete-then-create.
|
|
182
201
|
|
|
183
202
|
19. calendar_delete(eventId: string)
|
|
184
203
|
Delete (permanently remove) a calendar event by its eventId.
|
|
@@ -657,29 +676,95 @@ When the user's request requires an action, output one or more fenced JSON block
|
|
|
657
676
|
\`\`\`
|
|
658
677
|
Multiple blocks allowed for chaining. Include natural text before/between/after blocks.
|
|
659
678
|
Never output a JSON block as a suggestion — every block executes immediately.
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
679
|
+
|
|
680
|
+
## ABSOLUTE RULES (violate these and the user loses trust)
|
|
681
|
+
|
|
682
|
+
1. NEVER invent tool output. If you need data (an event ID, a price, a search result), you MUST emit a tool JSON block and wait for its real response. Do NOT write fake "(eventId: 123456789)" or fake results.
|
|
683
|
+
2. NEVER duplicate. If the user says "correggi/modifica/sposta/aggiorna" referring to an item you just created in this conversation, use the corresponding *_update / *_move tool with the eventId/taskId from your previous tool response. Do NOT create a second item.
|
|
684
|
+
3. NEVER put the TITLE in the description field. The "summary" / "title" field is the SHORT name (what shows on a calendar grid). The "description" field is ONLY for extra notes.
|
|
685
|
+
4. When in doubt, ASK ONE concise question — do not invent details.
|
|
686
|
+
|
|
687
|
+
## TOOL SIGNATURES (parameters and how to use them)
|
|
688
|
+
|
|
689
|
+
### Calendar
|
|
690
|
+
calendar_create(summary, start, end, description?, attendees?, location?)
|
|
691
|
+
- summary = SHORT title (e.g. "Tagliando BMW"). REQUIRED. Do NOT leave empty.
|
|
692
|
+
- description = optional notes only. Do NOT put the title here.
|
|
693
|
+
- start/end = ISO 8601 datetimes. Default end = start + 1 hour.
|
|
694
|
+
- The tool response includes "(eventId: ABC123)" — REMEMBER it for later updates.
|
|
695
|
+
Example — user: "fissa tagliando BMW 15 maggio ore 17":
|
|
696
|
+
{"action":"calendar_create","params":{"summary":"Tagliando BMW","start":"2026-05-15T17:00:00","end":"2026-05-15T18:00:00"}}
|
|
697
|
+
|
|
698
|
+
calendar_update(eventId, summary?, location?, description?, start?, end?)
|
|
699
|
+
- Use this for "correggi", "modifica", "rinomina", "cambia titolo", "sposta", "aggiorna" referring to an event you JUST created or found.
|
|
700
|
+
- eventId comes from the previous calendar_create response or calendar_find result. NEVER guess it.
|
|
701
|
+
- Example — you just created event ABC123. User: "correggi, il titolo è Tagliando BMW":
|
|
702
|
+
{"action":"calendar_update","params":{"eventId":"ABC123","summary":"Tagliando BMW"}}
|
|
703
|
+
|
|
704
|
+
calendar_delete(eventId) — Delete an event you have the eventId for. Confirm first.
|
|
705
|
+
calendar_move(eventId, newStart, newEnd) — Reschedule.
|
|
706
|
+
calendar_find(query, daysAhead?) — Search events by name. Returns eventIds. Use ONLY when you don't already have the id.
|
|
707
|
+
calendar_today() / calendar_tomorrow() / calendar_date(date) / calendar_upcoming(hours?) / calendar_week(startDate?) / calendar_month(month?) — Read-only listings.
|
|
708
|
+
schedule_meeting(clientName, subject, location, durationMinutes, dateFrom, dateTo) — Find optimal slots considering travel time.
|
|
709
|
+
|
|
710
|
+
### Gmail / IMAP
|
|
711
|
+
gmail_list(limit?, query?) — List recent emails. query supports Gmail search syntax.
|
|
712
|
+
gmail_read(messageId) — Read full email body.
|
|
713
|
+
gmail_send(to, subject, body, cc?, bcc?) — Send a new email.
|
|
714
|
+
gmail_reply(messageId, body) — Reply to a thread.
|
|
715
|
+
gmail_draft(to, subject, body) — Save a draft without sending.
|
|
716
|
+
gmail_mark_read(messageId) / gmail_mark_unread(messageId) / gmail_archive(messageId) / gmail_delete(messageId)
|
|
717
|
+
gmail_send_attach(to, subject, body, filePath) — Send with attachment.
|
|
718
|
+
imap_* — Same operations for non-Gmail accounts. imap_send_template/imap_bulk_send for templated sends.
|
|
719
|
+
|
|
720
|
+
### Tasks
|
|
721
|
+
task_list() / task_add(text, dueDate?) / task_done(index) / task_edit(index, newText) / task_move(fromIndex, toIndex) / task_delete(index) / task_clear()
|
|
722
|
+
gtask_list() / gtask_add(text) / gtask_complete(taskId) — Google Tasks.
|
|
723
|
+
|
|
724
|
+
### Contacts
|
|
725
|
+
contact_search(query) / contact_add(name, email?, phone?) / contact_update(id, fields) / contact_delete(id)
|
|
726
|
+
|
|
727
|
+
### Web + research
|
|
728
|
+
web_search(query) — Search the web (returns title/URL/snippet for top results).
|
|
729
|
+
fetch_url(url) — Fetch a page; returns title, meta description, OG tags, JSON-LD, main content. Use the full URL with https://. For naked domains (www.x.com), prepend https://.
|
|
730
|
+
get_weather(location) — Returns current conditions + 3-day forecast.
|
|
731
|
+
maps_directions(origin, destination)
|
|
732
|
+
|
|
733
|
+
### Browser automation (for JS-rendered pages or interaction)
|
|
734
|
+
browser_open(url) → returns sessionId. browser_screenshot(sessionId) · browser_click(sessionId, selector) · browser_type(sessionId, selector, text) · browser_extract(sessionId, selector) · browser_js(sessionId, code) · browser_wait(sessionId, ms) · browser_scroll(sessionId, deltaY) · browser_key(sessionId, key) · browser_close(sessionId)
|
|
735
|
+
|
|
736
|
+
### Notes / GitHub / Notion / Slack
|
|
737
|
+
note_add(text) · note_list()
|
|
738
|
+
github_issues(repo) · github_prs(repo) · github_notifications() · github_create_issue(repo, title, body)
|
|
739
|
+
notion_search(query) · notion_page(pageId)
|
|
740
|
+
slack_channels() · slack_messages(channel) · slack_send(channel, text)
|
|
741
|
+
|
|
742
|
+
### Files / Drive
|
|
743
|
+
file_list(dir?) · file_read(path) · file_write(path, content) · file_info(path) · file_search(query)
|
|
744
|
+
drive_list(folderId?) · drive_read(fileId) · drive_upload(name, content, folderId?) · drive_update(fileId, content) · drive_delete(fileId) · drive_info(fileId) · drive_folder(name, parentId?) · drive_download(fileId, savePath)
|
|
745
|
+
|
|
746
|
+
### Reminders / automation
|
|
747
|
+
notify_remind(text, when) — Notify the user at a future time.
|
|
748
|
+
birthdays_upcoming(days?) · birthday_add(name, date)
|
|
749
|
+
cron_add(schedule, prompt) · cron_list() · cron_remove(index)
|
|
750
|
+
|
|
751
|
+
### Screen / canvas / collab
|
|
752
|
+
screen_capture() / screen_analyze(question?)
|
|
753
|
+
canvas_render(html, title?) — Render an HTML report/dashboard in the canvas panel.
|
|
754
|
+
collab_send(channel, text) · collab_read(channel)
|
|
755
|
+
|
|
756
|
+
### Finance
|
|
757
|
+
market_price(symbol) · market_chart(symbol, period?) · market_indicators(symbol) · macro_data(metric, country?) · crypto_data(coin) · market_news(symbol?)
|
|
758
|
+
FINANCE FLOW: always call real data tools first (market_price → market_chart → market_indicators OR crypto_data + macro_data), then canvas_render with a Chart.js HTML dashboard (dark theme: bg #070b0f, green #00ff41, amber #f59e0b, red #ff4444, blue #00e5ff). Never invent prices.
|
|
759
|
+
|
|
760
|
+
### Code
|
|
761
|
+
execute_code(language, code) — Run Python/JS/shell in a sandbox.
|
|
762
|
+
|
|
763
|
+
## CONFIRMATIONS
|
|
764
|
+
|
|
765
|
+
Write operations that change state (gmail_send/reply/delete, calendar_create/update/move/delete, contact_delete, task_done/delete, notify_remind, file_write, drive_upload/update/delete) — describe what you're about to do in natural language first, then emit the JSON block. The system shows the user what's pending and acts on confirmation.
|
|
766
|
+
|
|
767
|
+
If the user already confirmed in the previous turn ("procedi", "sì", "fallo"), execute the pending action directly using the parameters from that prior turn — do NOT ask again or restart from scratch.`.trim();
|
|
683
768
|
|
|
684
769
|
// ── Action Parser ────────────────────────────────────────────────────────────
|
|
685
770
|
|
|
@@ -1309,14 +1394,43 @@ export async function executeTool(action, params, config) {
|
|
|
1309
1394
|
}
|
|
1310
1395
|
|
|
1311
1396
|
case 'calendar_create': {
|
|
1312
|
-
|
|
1313
|
-
|
|
1397
|
+
// Robustness: accept common param-name aliases that the model often
|
|
1398
|
+
// emits instead of `summary` (Google Calendar's official field name
|
|
1399
|
+
// for the event title — but LLMs trained on natural language confuse
|
|
1400
|
+
// "summary" with "description/note", so they sometimes put the actual
|
|
1401
|
+
// title under `title`/`name`/`subject` or even leave summary empty
|
|
1402
|
+
// and put the title text in `description`).
|
|
1403
|
+
let summary = params.summary || params.title || params.name || params.subject || '';
|
|
1404
|
+
let description = params.description || params.notes || '';
|
|
1405
|
+
|
|
1406
|
+
// Last-resort fallback: if summary is still empty but description
|
|
1407
|
+
// looks like a title (one line, < 120 chars, no period), promote it.
|
|
1408
|
+
if (!summary.trim() && description.trim()) {
|
|
1409
|
+
const firstLine = description.split('\n')[0].trim();
|
|
1410
|
+
if (firstLine.length > 0 && firstLine.length < 120) {
|
|
1411
|
+
summary = firstLine;
|
|
1412
|
+
// If description was JUST the title, clear it — otherwise keep the rest
|
|
1413
|
+
const rest = description.split('\n').slice(1).join('\n').trim();
|
|
1414
|
+
description = rest;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if (!summary.trim()) {
|
|
1419
|
+
return 'Error: event title (summary) is required. Please specify what the event is about.';
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const created = await createEvent(config, {
|
|
1423
|
+
summary,
|
|
1314
1424
|
start: params.start,
|
|
1315
1425
|
end: params.end,
|
|
1316
|
-
description
|
|
1426
|
+
description,
|
|
1317
1427
|
attendees: params.attendees || [],
|
|
1318
1428
|
});
|
|
1319
|
-
|
|
1429
|
+
// Surface the eventId so the model/Telegram responder can use it for
|
|
1430
|
+
// subsequent calendar_update or calendar_delete in the same turn.
|
|
1431
|
+
const eventId = created?.id || created?.eventId || '';
|
|
1432
|
+
const idHint = eventId ? ` (eventId: ${eventId})` : '';
|
|
1433
|
+
return `Event "${summary}" created for ${formatTime(params.start)} - ${formatTime(params.end)}.${idHint}`;
|
|
1320
1434
|
}
|
|
1321
1435
|
|
|
1322
1436
|
case 'calendar_move': {
|
|
@@ -1432,10 +1546,15 @@ export async function executeTool(action, params, config) {
|
|
|
1432
1546
|
}
|
|
1433
1547
|
}
|
|
1434
1548
|
|
|
1549
|
+
// Accept same param-name aliases as calendar_create so the model can
|
|
1550
|
+
// be sloppy about title vs summary without breaking things.
|
|
1551
|
+
const newSummary = params.summary || params.title || params.name || params.subject;
|
|
1552
|
+
const newDescription = params.description ?? params.notes;
|
|
1553
|
+
|
|
1435
1554
|
const patch = {};
|
|
1436
|
-
if (
|
|
1555
|
+
if (newSummary) patch.summary = newSummary;
|
|
1437
1556
|
if (params.location) patch.location = params.location;
|
|
1438
|
-
if (
|
|
1557
|
+
if (newDescription !== undefined) patch.description = newDescription;
|
|
1439
1558
|
if (params.start) {
|
|
1440
1559
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1441
1560
|
patch.start = { dateTime: new Date(params.start).toISOString(), timeZone: tz };
|
|
@@ -1444,9 +1563,12 @@ export async function executeTool(action, params, config) {
|
|
|
1444
1563
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1445
1564
|
patch.end = { dateTime: new Date(params.end).toISOString(), timeZone: tz };
|
|
1446
1565
|
}
|
|
1566
|
+
if (Object.keys(patch).length === 0) {
|
|
1567
|
+
return 'No fields to update. Specify at least one of: summary (title), location, description, start, end.';
|
|
1568
|
+
}
|
|
1447
1569
|
await updateEvent(config, 'primary', eventId, patch);
|
|
1448
1570
|
const changes = Object.keys(patch).join(', ');
|
|
1449
|
-
return `Event updated successfully (${changes})
|
|
1571
|
+
return `Event updated successfully (changed: ${changes})${newSummary ? `. New title: "${newSummary}"` : ''}${params.location ? `. New location: ${params.location}` : ''}`;
|
|
1450
1572
|
}
|
|
1451
1573
|
|
|
1452
1574
|
case 'calendar_delete': {
|