nothumanallowed 16.0.56 → 16.0.58

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.56",
3
+ "version": "16.0.58",
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.56';
8
+ export const VERSION = '16.0.58';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -4334,27 +4334,36 @@ export async function _autoExtendStylesIfNeeded(projectName, config, emit, opts)
4334
4334
  } catch {}
4335
4335
  }
4336
4336
 
4337
- const sys = `You are an expert frontend designer producing PRODUCTION-QUALITY CSS.
4337
+ // APPEND mode (16.0.57): output ONLY the new rules server appends to
4338
+ // existing file. Prevents monotonic regression (LLM truncating + losing
4339
+ // existing rules) and dramatically reduces output token cost.
4340
+ const sys = `You are an expert frontend designer. Generate ONLY new CSS rules to ADD to an existing stylesheet.
4338
4341
 
4339
- DESIGN REQUIREMENTS (NON-NEGOTIABLE):
4340
- - WCAG AA contrast: text-on-background ratio >= 4.5:1. NO washed-out pastels for text. Body text must be near-black on light bg, or near-white on dark bg.
4341
- - Use VIBRANT accent colors with sufficient saturation (HSL S >= 60%, L between 35-65% for accents).
4342
- - Cover EVERY selector listed including footer, header, nav, hero, sections, cards, buttons, forms, modals, tooltips.
4343
- - Use modern CSS: flex/grid layouts, custom properties for colors, smooth transitions (200-300ms), subtle shadows.
4344
-
4345
- OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no explanations, no preamble. The output will be written directly to disk.`;
4342
+ CRITICAL RULES:
4343
+ - Output ONLY the new CSS rules do NOT repeat any existing rules.
4344
+ - Do NOT output markdown fences, explanations, or comments about what you're doing.
4345
+ - The output will be APPENDED to an existing CSS file. Do not include @import, @charset, or any preamble.
4346
+ - Generate at least one rule for EVERY listed missing selector.
4347
+
4348
+ DESIGN REQUIREMENTS:
4349
+ - WCAG AA contrast: text-on-bg ratio >= 4.5:1. NO washed-out pastels for text.
4350
+ - Vibrant accent colors (HSL S >= 60%, L 35-65%).
4351
+ - Match the existing CSS's design language (look at the colors/spacing in the existing rules).
4352
+ - Include responsive breakpoints (768px, 480px) where layout matters.
4353
+ - Hover/focus/transition states for interactive elements.`;
4354
+ // Pick a sample of missing selectors that fits in token budget. We cap at
4355
+ // 200 to keep one pass reasonable; remaining are picked up by next pass.
4356
+ const passSelectors = analysis.missing.slice(0, 200);
4346
4357
  const user =
4347
- `Extend this CSS file: \`${targetRel}\`\n\n` +
4348
- `Current CSS (${target.size} bytes, ${analysis.cssRuleCount} rules):\n\`\`\`css\n${target.content.slice(0, 6000)}\n\`\`\`\n\n` +
4349
- `ALL missing selectors (${analysis.missing.length} cover every single one):\n${analysis.missing.join(', ')}\n\n` +
4350
- `HTML files for context:\n${htmlSample}\n\n` +
4351
- `Output the COMPLETE extended CSS file. Required additions:\n` +
4358
+ `Existing CSS file: \`${targetRel}\` (${target.size} bytes, ${analysis.cssRuleCount} rules).\n\n` +
4359
+ `Sample of existing rules (for design-language reference — DO NOT repeat in output):\n\`\`\`css\n${target.content.slice(0, 3000)}\n\`\`\`\n\n` +
4360
+ `HTML context (snippet, for layout reference):\n${htmlSample.slice(0, 2500)}\n\n` +
4361
+ `Generate NEW CSS rules that cover these ${passSelectors.length} currently-uncovered selectors (out of ${analysis.missing.length} total):\n${passSelectors.join(', ')}\n\n` +
4362
+ `Output ONLY the new rules. Required additions if not already in existing CSS:\n` +
4352
4363
  `- img { max-width: 100%; height: auto; object-fit: cover; display: block; }\n` +
4353
- `- Footer, header, nav with proper layout and visible styling (NOT transparent backgrounds with low-contrast text)\n` +
4354
- `- Rules for EVERY one of the ${analysis.missing.length} missing selectors above\n` +
4355
- `- Responsive breakpoints at 1024px, 768px, 480px (mobile-first)\n` +
4356
- `- Hover/focus/transition states for buttons, links, cards\n` +
4357
- `- Color contrast must pass WCAG AA: text-on-bg >= 4.5:1`;
4364
+ `- footer { padding: 32px 16px; background: ...; color: ...; } (or similar with visible contrast)\n` +
4365
+ `- Responsive @media queries at 768px and 480px for grid/flex sections.\n` +
4366
+ `Begin your output directly with the first CSS rule. No preamble.`;
4358
4367
 
4359
4368
  const provider = config?.llm?.provider || 'unknown';
4360
4369
  const model = config?.llm?.model || config?.llm?.[provider]?.model || 'default';
@@ -4374,23 +4383,30 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
4374
4383
  let aborted = false;
4375
4384
  const abortController = new AbortController();
4376
4385
 
4386
+ // Track byte velocity for an informative heartbeat: show b/s instead of
4387
+ // misleading "0s since last chunk" (which is almost always 0 when streaming).
4388
+ let prevBytes = 0;
4389
+ let prevHeartbeatAt = startedAt;
4377
4390
  const heartbeatInterval = setInterval(() => {
4378
4391
  if (!emit) return;
4379
- const elapsed = ((Date.now() - startedAt) / 1000).toFixed(0);
4380
- const sinceLast = ((Date.now() - lastChunkAt) / 1000).toFixed(0);
4381
- emit({ type: 'status', msg: `LLM extend: ${elapsed}s elapsed, ${body.length} bytes received (${sinceLast}s since last chunk)` });
4382
- if (!earlyWarningEmitted && body.length === 0 && Date.now() - startedAt > 15_000) {
4392
+ const now = Date.now();
4393
+ const elapsed = ((now - startedAt) / 1000).toFixed(0);
4394
+ const bytesPerSec = Math.round((body.length - prevBytes) / Math.max(1, (now - prevHeartbeatAt) / 1000));
4395
+ prevBytes = body.length;
4396
+ prevHeartbeatAt = now;
4397
+ emit({ type: 'status', msg: `LLM extend: ${elapsed}s elapsed, ${body.length} bytes received (${bytesPerSec} b/s)` });
4398
+ if (!earlyWarningEmitted && body.length === 0 && now - startedAt > 15_000) {
4383
4399
  earlyWarningEmitted = true;
4384
4400
  emit({ type: 'warn', msg: `Provider ${provider} hasn't sent any data in 15s. If this is Liara, the free tier may be under load — try switching to Anthropic/OpenAI in Settings.` });
4385
4401
  }
4386
4402
  // No-progress timeout: only if STUCK (zero chunks for 30s)
4387
- if (Date.now() - lastChunkAt > noProgressTimeoutMs && body.length > 0) {
4403
+ if (now - lastChunkAt > noProgressTimeoutMs && body.length > 0) {
4388
4404
  timedOut = true;
4389
4405
  aborted = true;
4390
4406
  abortController.abort();
4391
4407
  }
4392
4408
  // Absolute timeout
4393
- if (Date.now() - startedAt > absoluteTimeoutMs) {
4409
+ if (now - startedAt > absoluteTimeoutMs) {
4394
4410
  timedOut = true;
4395
4411
  aborted = true;
4396
4412
  abortController.abort();
@@ -4418,30 +4434,47 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
4418
4434
  }
4419
4435
  clearInterval(heartbeatInterval);
4420
4436
 
4421
- // Strip markdown fences
4437
+ // Strip markdown fences from the appended rules (LLM sometimes adds them)
4422
4438
  body = body.replace(/^```[a-zA-Z]*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
4423
- if (_looksLikeLLMError(body) || body.length < target.size * 0.5) {
4424
- if (emit) emit({ type: 'warn', msg: `CSS extend produced suspicious output (${body.length} bytes vs ${target.size} original) — keeping original.` });
4439
+ if (_looksLikeLLMError(body) || body.length < 200) {
4440
+ if (emit) emit({ type: 'warn', msg: `CSS extend produced suspicious output (${body.length} bytes of new rules) — keeping original.` });
4425
4441
  return { extended: false, reason: 'output_too_short_or_error' };
4426
4442
  }
4427
4443
 
4428
- // Backup + write
4444
+ // APPEND mode (16.0.57): keep ALL existing rules intact, add new ones at end.
4445
+ // Prevents monotonic regression where pass N replaces pass N-1's work with
4446
+ // a smaller file. Combined content = original + delimiter comment + new rules.
4447
+ const combined = target.content
4448
+ + '\n\n/* === nha-webcraft: auto-extended rules (' + new Date().toISOString() + ') === */\n'
4449
+ + body
4450
+ + '\n';
4451
+
4429
4452
  try {
4430
4453
  fs.writeFileSync(target.abs + '.before-extend-' + Date.now(), target.content, 'utf-8');
4431
- fs.writeFileSync(target.abs, body, 'utf-8');
4454
+ fs.writeFileSync(target.abs, combined, 'utf-8');
4432
4455
  } catch (e) {
4433
4456
  return { extended: false, reason: 'write_failed', error: e.message };
4434
4457
  }
4435
4458
 
4436
- // Re-analyze to confirm improvement
4459
+ // Re-analyze to confirm improvement. APPEND mode guarantees coverage
4460
+ // monotonically increases (or stays equal), never decreases.
4437
4461
  const after = _analyzeCssCoverage(dir);
4438
- if (emit) emit({ type: 'status', msg: `CSS extended: ${targetRel} ${target.size} → ${body.length} bytes. Coverage ${(analysis.coverage * 100).toFixed(0)}% → ${(after.coverage * 100).toFixed(0)}%.` });
4462
+ if (after.missing.length >= analysis.missing.length) {
4463
+ // No progress despite append — LLM produced rules that don't match selectors.
4464
+ // Roll back so the file doesn't bloat with useless rules.
4465
+ try { fs.writeFileSync(target.abs, target.content, 'utf-8'); } catch {}
4466
+ if (emit) emit({ type: 'warn', msg: `CSS extend rolled back: ${body.length} bytes of new rules added but no selectors covered (model output didn't match needed selectors).` });
4467
+ return { extended: false, reason: 'no_coverage_gain', missingBefore: analysis.missing.length, missingAfter: after.missing.length };
4468
+ }
4469
+
4470
+ if (emit) emit({ type: 'status', msg: `CSS extended: ${targetRel} ${target.size} → ${combined.length} bytes (appended ${body.length} bytes). Coverage ${(analysis.coverage * 100).toFixed(0)}% → ${(after.coverage * 100).toFixed(0)}%, ${analysis.missing.length} → ${after.missing.length} selectors missing.` });
4439
4471
 
4440
4472
  return {
4441
4473
  extended: true,
4442
4474
  file: targetRel,
4443
4475
  sizeBefore: target.size,
4444
- sizeAfter: body.length,
4476
+ sizeAfter: combined.length,
4477
+ appendedBytes: body.length,
4445
4478
  coverageBefore: analysis.coverage,
4446
4479
  coverageAfter: after.coverage,
4447
4480
  missingBefore: analysis.missing.length,
@@ -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