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.57",
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.57';
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(/&nbsp;/g, ' ')
736
+ .replace(/&amp;/g, '&')
737
+ .replace(/&lt;/g, '<')
738
+ .replace(/&gt;/g, '>')
739
+ .replace(/&quot;/g, '"')
740
+ .replace(/&#39;/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
- const browser = await getBrowser();
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 published OAuth client (Desktop app type client_id is not a secret)
16
- const DEFAULT_CLIENT_ID = '516893094132-8u2jf6h6h3j6h8j9k0l1m2n3o4p5q6r7.apps.googleusercontent.com'; // NHA Official OAuth Client
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('Get credentials from Google Cloud Console:');
225
- info(' 1. Go to https://console.cloud.google.com/apis/credentials');
226
- info(' 2. Create an OAuth 2.0 Client ID (Desktop app type)');
227
- info(' 3. Enable Gmail API and Calendar API');
228
- info(' 4. Run:');
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(' 5. Run: nha google auth');
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
- // Fall through if no cached list
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 ────────────────────────────────