nothumanallowed 16.0.57 → 16.0.59
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.59",
|
|
4
4
|
"description": "Local AI assistant: 80 tools (Gmail, Calendar, Drive, GitHub, Slack, browser, code, files), 38 agents, visual workflows (Studio, AWF, WebCraft). Install with `npm i -g nothumanallowed`, run with `nha ui`. Free tier built-in (Liara), no API key required. Your data stays on your PC — OAuth tokens local, no cloud. Open-source MIT.",
|
|
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.59';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -125,6 +125,10 @@ function findChromePath() {
|
|
|
125
125
|
'/snap/bin/chromium',
|
|
126
126
|
'/usr/bin/brave-browser',
|
|
127
127
|
'/usr/bin/microsoft-edge',
|
|
128
|
+
// Termux on Android — chromium installed via "pkg install chromium"
|
|
129
|
+
'/data/data/com.termux/files/usr/bin/chromium',
|
|
130
|
+
'/data/data/com.termux/files/usr/bin/chromium-browser',
|
|
131
|
+
'/data/data/com.termux/files/usr/bin/google-chrome',
|
|
128
132
|
],
|
|
129
133
|
win32: [
|
|
130
134
|
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
@@ -657,6 +661,104 @@ async function getBrowser() {
|
|
|
657
661
|
* @param {boolean} [options.waitForLoad] - Wait for page load event (default true)
|
|
658
662
|
* @returns {Promise<{ title: string, url: string, status: number }>}
|
|
659
663
|
*/
|
|
664
|
+
/**
|
|
665
|
+
* Lightweight HTTP fallback when Chrome/Chromium is not available.
|
|
666
|
+
* Uses fetch() + regex-based HTML→text extraction. No JS rendering, no clicks.
|
|
667
|
+
* Good enough for: news sites, blog posts, static pages, API responses,
|
|
668
|
+
* documentation pages. NOT good for: SPAs, login flows, dynamic dashboards.
|
|
669
|
+
*/
|
|
670
|
+
async function browserOpenViaFetch(url, options = {}) {
|
|
671
|
+
const timeout = options.timeout || 15000;
|
|
672
|
+
try {
|
|
673
|
+
const ac = new AbortController();
|
|
674
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
675
|
+
const res = await fetch(url, {
|
|
676
|
+
redirect: 'follow',
|
|
677
|
+
signal: ac.signal,
|
|
678
|
+
headers: {
|
|
679
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
680
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
681
|
+
'Accept-Language': 'en-US,en;q=0.9,it;q=0.8',
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
clearTimeout(timer);
|
|
685
|
+
|
|
686
|
+
const status = res.status;
|
|
687
|
+
const finalUrl = res.url || url;
|
|
688
|
+
const ct = res.headers.get('content-type') || '';
|
|
689
|
+
const isHtml = /html/i.test(ct);
|
|
690
|
+
const raw = await res.text();
|
|
691
|
+
|
|
692
|
+
if (!isHtml) {
|
|
693
|
+
return {
|
|
694
|
+
title: finalUrl,
|
|
695
|
+
url: finalUrl,
|
|
696
|
+
status,
|
|
697
|
+
mode: 'fetch-fallback',
|
|
698
|
+
warning: 'Chrome not installed — used HTTP fetch. No JS rendering. Limited interactivity.',
|
|
699
|
+
content: raw.slice(0, 50_000),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Extract title
|
|
704
|
+
const titleMatch = raw.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
705
|
+
const title = titleMatch ? titleMatch[1].replace(/\s+/g, ' ').trim() : finalUrl;
|
|
706
|
+
|
|
707
|
+
// Extract main text content: strip script/style/svg/comments, then strip tags
|
|
708
|
+
let textContent = raw
|
|
709
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
710
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
711
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
712
|
+
.replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, ' ')
|
|
713
|
+
.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, ' ')
|
|
714
|
+
.replace(/<header\b[^>]*>[\s\S]*?<\/header>/gi, ' ') // strip nav/header noise
|
|
715
|
+
.replace(/<nav\b[^>]*>[\s\S]*?<\/nav>/gi, ' ')
|
|
716
|
+
.replace(/<footer\b[^>]*>[\s\S]*?<\/footer>/gi, ' ')
|
|
717
|
+
.replace(/<aside\b[^>]*>[\s\S]*?<\/aside>/gi, ' ');
|
|
718
|
+
|
|
719
|
+
// Extract headlines + links separately for news sites
|
|
720
|
+
const headlines = [];
|
|
721
|
+
const linkRe = /<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
|
722
|
+
let lm;
|
|
723
|
+
while ((lm = linkRe.exec(raw)) !== null && headlines.length < 50) {
|
|
724
|
+
const linkText = lm[2].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
725
|
+
const href = lm[1];
|
|
726
|
+
if (linkText.length > 20 && linkText.length < 200 && !href.startsWith('#') && !href.startsWith('javascript:')) {
|
|
727
|
+
const absHref = href.startsWith('http') ? href : new URL(href, finalUrl).toString();
|
|
728
|
+
headlines.push({ text: linkText, url: absHref });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Strip remaining tags to get plain text
|
|
733
|
+
textContent = textContent
|
|
734
|
+
.replace(/<[^>]+>/g, ' ')
|
|
735
|
+
.replace(/ /g, ' ')
|
|
736
|
+
.replace(/&/g, '&')
|
|
737
|
+
.replace(/</g, '<')
|
|
738
|
+
.replace(/>/g, '>')
|
|
739
|
+
.replace(/"/g, '"')
|
|
740
|
+
.replace(/'/g, "'")
|
|
741
|
+
.replace(/&#x?[0-9a-f]+;/gi, '')
|
|
742
|
+
.replace(/\s+/g, ' ')
|
|
743
|
+
.trim();
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
title,
|
|
747
|
+
url: finalUrl,
|
|
748
|
+
status,
|
|
749
|
+
mode: 'fetch-fallback',
|
|
750
|
+
warning: 'Chrome/Chromium not installed — used HTTP fetch fallback. No JS rendering, no interactive clicks/forms. To install: macOS use brew, Linux apt-get, Termux "pkg install chromium".',
|
|
751
|
+
headlines: headlines.slice(0, 30),
|
|
752
|
+
content: textContent.slice(0, 30_000),
|
|
753
|
+
};
|
|
754
|
+
} catch (e) {
|
|
755
|
+
if (e.name === 'AbortError') {
|
|
756
|
+
return { error: true, message: `HTTP fetch timeout after ${timeout / 1000}s. The site may be slow or blocking the request.` };
|
|
757
|
+
}
|
|
758
|
+
return { error: true, message: `HTTP fetch failed: ${e.message}. ${/ENOTFOUND|ECONNREFUSED/.test(e.message) ? 'Network or DNS issue.' : ''}` };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
660
762
|
export async function browserOpen(url, options = {}) {
|
|
661
763
|
// SSRF check
|
|
662
764
|
const check = await isSafeUrl(url);
|
|
@@ -664,7 +766,23 @@ export async function browserOpen(url, options = {}) {
|
|
|
664
766
|
return { error: true, message: `SSRF blocked: ${check.reason}` };
|
|
665
767
|
}
|
|
666
768
|
|
|
667
|
-
|
|
769
|
+
// Fast fallback: if Chrome/Chromium is not installed (e.g. Termux on Android),
|
|
770
|
+
// do a plain HTTP fetch and extract text content. Limited (no JS rendering,
|
|
771
|
+
// no clicks), but works for news sites / blog posts / static pages.
|
|
772
|
+
if (!findChromePath()) {
|
|
773
|
+
return browserOpenViaFetch(url, options);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let browser;
|
|
777
|
+
try {
|
|
778
|
+
browser = await getBrowser();
|
|
779
|
+
} catch (e) {
|
|
780
|
+
// Chrome detection failed at launch — same fallback
|
|
781
|
+
if (/Chrome\/Chromium not found/i.test(e.message || '')) {
|
|
782
|
+
return browserOpenViaFetch(url, options);
|
|
783
|
+
}
|
|
784
|
+
throw e;
|
|
785
|
+
}
|
|
668
786
|
const timeout = options.timeout || NAV_TIMEOUT_MS;
|
|
669
787
|
const waitForLoad = options.waitForLoad !== false;
|
|
670
788
|
|
|
@@ -12,8 +12,13 @@ import os from 'os';
|
|
|
12
12
|
import { saveTokens, loadTokens, deleteTokens } from './token-store.mjs';
|
|
13
13
|
import { info, ok, fail, warn } from '../ui.mjs';
|
|
14
14
|
|
|
15
|
-
// NHA
|
|
16
|
-
|
|
15
|
+
// IMPORTANT: NHA does NOT ship a default Google OAuth client ID.
|
|
16
|
+
// The previous placeholder (516893094132-8u2jf...) was a fake-looking value
|
|
17
|
+
// that always returned `invalid_client` from Google. Each user must register
|
|
18
|
+
// their own OAuth client in Google Cloud Console — this is by design for
|
|
19
|
+
// privacy (no shared client app), and it's a one-time 3-minute setup.
|
|
20
|
+
// See https://nothumanallowed.com/docs/google for the full guide.
|
|
21
|
+
const DEFAULT_CLIENT_ID = '';
|
|
17
22
|
const SCOPES = [
|
|
18
23
|
'https://www.googleapis.com/auth/gmail.modify',
|
|
19
24
|
'https://www.googleapis.com/auth/gmail.send',
|
|
@@ -221,14 +226,27 @@ export async function runAuthFlow(config, manual = false) {
|
|
|
221
226
|
|
|
222
227
|
if (!clientId) {
|
|
223
228
|
fail('Google OAuth client ID not configured.');
|
|
224
|
-
info('
|
|
225
|
-
info('
|
|
226
|
-
info('
|
|
227
|
-
info('
|
|
228
|
-
info('
|
|
229
|
+
info('');
|
|
230
|
+
info('NHA does not ship a shared OAuth client (your data never goes through');
|
|
231
|
+
info('our servers — Gmail/Calendar API calls go from your PC directly to');
|
|
232
|
+
info('Google). You need a 3-minute one-time setup of your own OAuth client.');
|
|
233
|
+
info('');
|
|
234
|
+
info('STEPS:');
|
|
235
|
+
info(' 1. Open https://console.cloud.google.com/apis/credentials');
|
|
236
|
+
info(' 2. Click + CREATE CREDENTIALS → OAuth client ID');
|
|
237
|
+
info(' 3. Application type: "Desktop app", give it a name (e.g. "NHA local")');
|
|
238
|
+
info(' 4. Click CREATE. Google shows you Client ID + Client Secret.');
|
|
239
|
+
info(' 5. Enable the APIs you need:');
|
|
240
|
+
info(' - Gmail API: https://console.cloud.google.com/apis/library/gmail.googleapis.com');
|
|
241
|
+
info(' - Calendar: https://console.cloud.google.com/apis/library/calendar-json.googleapis.com');
|
|
242
|
+
info(' - Drive: https://console.cloud.google.com/apis/library/drive.googleapis.com');
|
|
243
|
+
info(' - People API: https://console.cloud.google.com/apis/library/people.googleapis.com');
|
|
244
|
+
info(' 6. Save the credentials in NHA:');
|
|
229
245
|
info(' nha config set google-client-id YOUR_CLIENT_ID');
|
|
230
246
|
info(' nha config set google-client-secret YOUR_CLIENT_SECRET');
|
|
231
|
-
info('
|
|
247
|
+
info(' 7. Re-run: nha google auth');
|
|
248
|
+
info('');
|
|
249
|
+
info('Full guide with screenshots: https://nothumanallowed.com/docs/google');
|
|
232
250
|
return false;
|
|
233
251
|
}
|
|
234
252
|
|
|
@@ -2672,7 +2672,69 @@ class TelegramResponder {
|
|
|
2672
2672
|
}
|
|
2673
2673
|
}
|
|
2674
2674
|
} catch (e) { this.log(`[direct] pagination failed: ${e.message}`); }
|
|
2675
|
-
|
|
2675
|
+
|
|
2676
|
+
// NO CACHE FALLBACK (Giovanni's recurring bug): if user asks "mostra i
|
|
2677
|
+
// prossimi" but no calendar list was cached in this session, the LLM
|
|
2678
|
+
// hallucinates plausible-but-fake events. Instead, run calendar_upcoming
|
|
2679
|
+
// for the next 7 days directly here. Deterministic, zero LLM call.
|
|
2680
|
+
try {
|
|
2681
|
+
const { listEvents } = await import('./google-calendar.mjs');
|
|
2682
|
+
const now = new Date();
|
|
2683
|
+
const weekAhead = new Date(now.getTime() + 7 * 86400000);
|
|
2684
|
+
const evs = await listEvents(config, 'primary', now, weekAhead);
|
|
2685
|
+
if (Array.isArray(evs) && evs.length > 0) {
|
|
2686
|
+
const items = evs.map(e => ({
|
|
2687
|
+
eventId: e.id,
|
|
2688
|
+
id: e.id,
|
|
2689
|
+
summary: e.summary || '(senza titolo)',
|
|
2690
|
+
time: (e.start || '').slice(11, 16),
|
|
2691
|
+
date: (e.start || '').slice(0, 10),
|
|
2692
|
+
start: e.start,
|
|
2693
|
+
}));
|
|
2694
|
+
// Persist into list-cache so subsequent "mostra i prossimi" paginates
|
|
2695
|
+
try {
|
|
2696
|
+
const { rememberList } = await import('./list-cache.mjs');
|
|
2697
|
+
rememberList(chatId || '__last_list__', 'calendar', items);
|
|
2698
|
+
} catch {}
|
|
2699
|
+
const pageSize = 8;
|
|
2700
|
+
const firstSlice = items.slice(0, pageSize);
|
|
2701
|
+
const lines = [`📅 Prossimi eventi (7 giorni, ${items.length} totali):`];
|
|
2702
|
+
const byDay = new Map();
|
|
2703
|
+
for (const e of firstSlice) {
|
|
2704
|
+
const day = e.date || 'misc';
|
|
2705
|
+
if (!byDay.has(day)) byDay.set(day, []);
|
|
2706
|
+
byDay.get(day).push(e);
|
|
2707
|
+
}
|
|
2708
|
+
for (const [day, evs] of [...byDay.entries()].sort()) {
|
|
2709
|
+
const d = day !== 'misc' ? new Date(day + 'T12:00:00').toLocaleDateString('it-IT', { weekday: 'short', day: 'numeric', month: 'short' }) : '';
|
|
2710
|
+
if (d) lines.push(`\n${d}:`);
|
|
2711
|
+
for (const e of evs) lines.push(` ${e.time || '—'} — ${e.summary}`);
|
|
2712
|
+
}
|
|
2713
|
+
if (items.length > pageSize) {
|
|
2714
|
+
lines.push(`\n... ${items.length - pageSize} eventi rimanenti. Scrivi "mostra i prossimi" per continuare.`);
|
|
2715
|
+
} else {
|
|
2716
|
+
lines.push(`\n✓ Fine elenco.`);
|
|
2717
|
+
}
|
|
2718
|
+
// Track shownCount so next "mostra i prossimi" works
|
|
2719
|
+
try {
|
|
2720
|
+
const cacheFile = path.join(os.homedir(), '.nha', 'list-cache.json');
|
|
2721
|
+
const cacheNow = fs.existsSync(cacheFile) ? JSON.parse(fs.readFileSync(cacheFile, 'utf-8')) : {};
|
|
2722
|
+
const key = chatId || '__last_list__';
|
|
2723
|
+
if (!cacheNow[key]) cacheNow[key] = {};
|
|
2724
|
+
cacheNow[key].lastList_calendar_shownCount = pageSize;
|
|
2725
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cacheNow, null, 2));
|
|
2726
|
+
} catch {}
|
|
2727
|
+
this.log(`[direct] PAGINATION fallback: fetched ${items.length} events from calendar_upcoming(7d)`);
|
|
2728
|
+
return { action: 'calendar_page', success: true, message: lines.join('\n') };
|
|
2729
|
+
}
|
|
2730
|
+
// No events in next 7 days — say so explicitly, do NOT fall to LLM
|
|
2731
|
+
return { action: 'calendar_page', success: true, message: '📅 Nessun evento programmato nei prossimi 7 giorni. Scrivi "appuntamenti di [data/mese]" per cercare in un altro periodo.' };
|
|
2732
|
+
} catch (e) {
|
|
2733
|
+
this.log(`[direct] pagination fallback failed: ${e.message}`);
|
|
2734
|
+
// CRITICAL: do NOT fall through to LLM — return error instead of
|
|
2735
|
+
// letting HERALD hallucinate fake events (Giovanni's bug).
|
|
2736
|
+
return { action: 'calendar_page', success: false, message: `Non riesco a leggere il calendario in questo momento (${e.message.slice(0, 100)}). Verifica la connessione Google in Settings.` };
|
|
2737
|
+
}
|
|
2676
2738
|
}
|
|
2677
2739
|
|
|
2678
2740
|
// ─── ANAPHORIC delete + CONFIRMATION yes ────────────────────────────────
|