nothumanallowed 15.1.10 → 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
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/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
|
|
|
@@ -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': {
|