nothumanallowed 16.0.27 → 16.0.29

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.27",
3
+ "version": "16.0.29",
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.27';
8
+ export const VERSION = '16.0.29';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -183,6 +183,23 @@ class SandboxManager {
183
183
  }
184
184
  emit({ type: 'status', msg: `Entry point: ${entryFile}` });
185
185
 
186
+ // Pre-flight: validate entry file with acorn. If the file content was
187
+ // corrupted by a partial LLM stream (HTTP error leaked as code), quarantine
188
+ // it with a minimal stub so the sandbox can still boot. The user sees a
189
+ // clear "re-generate from chat" message instead of a SyntaxError crash.
190
+ const entryAbs = path.join(projectDir, entryFile);
191
+ if (fs.existsSync(entryAbs)) {
192
+ try {
193
+ const entryContent = fs.readFileSync(entryAbs, 'utf-8');
194
+ const sanitized = _sanitizeGeneratedFile(entryFile, entryContent, path.basename(projectDir));
195
+ if (sanitized !== entryContent) {
196
+ emit({ type: 'warn', msg: `Entry file ${entryFile} was corrupted (likely partial stream / HTTP error leaked into content). Auto-quarantined — re-generate from chat.` });
197
+ fs.writeFileSync(entryAbs + '.corrupt-' + Date.now(), entryContent, 'utf-8');
198
+ fs.writeFileSync(entryAbs, sanitized, 'utf-8');
199
+ }
200
+ } catch {}
201
+ }
202
+
186
203
  // ── Phase 2: Dependencies (pre-scan + batch install) ──────────────────
187
204
  // Pre-scan the project source files for require()/import statements and
188
205
  // diff against package.json + node_modules. Install everything missing in
@@ -346,10 +363,22 @@ class SandboxManager {
346
363
  // The pre-scan in Phase 2 catches most cases. Tier 1 here is the safety
347
364
  // net for files generated AFTER pre-scan (e.g. user added a require()
348
365
  // during chat) or for transitive crashes that surface only at runtime.
349
- const missingModules = [...stderrBuf.matchAll(/Cannot find module ['"]([^'"]+)['"]/g)]
366
+ const rawMissing = [...stderrBuf.matchAll(/Cannot find module ['"]([^'"]+)['"]/g)]
350
367
  .map(m => m[1])
351
368
  .filter(m => !m.startsWith('.') && !m.startsWith('/') && !m.startsWith('node:'))
352
- .map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0]);
369
+ .map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0])
370
+ // Drop Node.js built-ins — they're in the runtime, can't be installed
371
+ .filter(m => !_NODE_BUILTINS.has(m));
372
+ // Resolve LLM hallucinated aliases (jwt → jsonwebtoken, bcrypt → bcryptjs)
373
+ const missingModules = rawMissing.map(m => _PACKAGE_ALIASES.get(m) || m);
374
+ // Detect aliases — if the shim already covers the resolved name, no install needed
375
+ const aliasedFromShim = rawMissing.filter(m => {
376
+ const real = _PACKAGE_ALIASES.get(m);
377
+ return real && _SHIMMED_MODULES.has(real);
378
+ });
379
+ if (aliasedFromShim.length > 0) {
380
+ emit({ type: 'warn', msg: `LLM hallucinated package names: ${aliasedFromShim.join(', ')} — the runtime shim already aliases these. If you still see this crash, the shim wasn't re-generated. Restart "nha ui".` });
381
+ }
353
382
  const uniqueMissing = [...new Set(missingModules)].filter(m => !_SHIMMED_MODULES.has(m));
354
383
  if (uniqueMissing.length > 0 && _attempt < MAX_RETRIES) {
355
384
  emit({ type: 'phase', phase: 'autofix', msg: `Missing module${uniqueMissing.length === 1 ? '' : 's'}: ${uniqueMissing.join(', ')} — batch installing...` });
@@ -3484,17 +3513,27 @@ function _patchEntry(projectDir, entryFile, shimDir, port) {
3484
3513
  // file content. Those strings are short, don't start with valid syntax for
3485
3514
  // the file type, and contain specific marker phrases.
3486
3515
  const _LLM_ERROR_MARKERS = [
3516
+ // "Access denied" / "Access temporarily denied"
3487
3517
  /^\s*Access (temporarily )?denied/i,
3488
- /^\s*Rate ?limit(ed)?/i,
3518
+ // "Service Temporarily Unavailable", "Server Temporarily Unavailable",
3519
+ // "Temporarily Unavailable" — common nginx/cloudflare/CDN 503 responses
3520
+ /^\s*(Service |Server )?Temporarily Unavailable/i,
3489
3521
  /^\s*Service Unavailable/i,
3522
+ /^\s*Rate ?limit(ed)?/i,
3490
3523
  /^\s*Internal Server Error/i,
3491
3524
  /^\s*Too Many Requests/i,
3492
3525
  /^\s*Bad Gateway/i,
3493
3526
  /^\s*Gateway Timeout/i,
3527
+ /^\s*Request Timeout/i,
3528
+ /^\s*Not Found/i,
3529
+ /^\s*Forbidden\b/i,
3530
+ /^\s*Unauthorized\b/i,
3494
3531
  /^\s*<!DOCTYPE html.*Cloudflare/is,
3495
- /^\s*<html.*<title>.*(Access|Forbidden|Error)/is,
3496
- /^\s*\{?\s*"error"\s*:\s*"(rate.?limit|quota|unauthorized)"/i,
3532
+ /^\s*<html.*<title>.*(Access|Forbidden|Error|Unavailable|Cloudflare)/is,
3533
+ /^\s*\{?\s*"error"\s*:\s*"(rate.?limit|quota|unauthorized|temporarily)"/i,
3497
3534
  /^\s*error code:\s*\d{3,4}/i,
3535
+ // Standalone HTTP status phrase at the start of a code file is suspicious
3536
+ /^\s*(HTTP\/[\d.]+\s+)?[45]\d{2}\s+/,
3498
3537
  ];
3499
3538
 
3500
3539
  function _looksLikeLLMError(content) {
@@ -3517,17 +3556,25 @@ function _fallbackPackageJson(projectName) {
3517
3556
  }
3518
3557
 
3519
3558
  // Sanitize content before writing to disk. Rejects LLM error responses,
3520
- // repairs corrupt JSON manifests, and falls back to a minimal template when
3521
- // content is unsalvageable. This is the LAST line of defense between the LLM
3522
- // API and the user's filesystem.
3559
+ // repairs corrupt JSON manifests, validates JS/TS with acorn, and falls back
3560
+ // to a safe placeholder when content is unsalvageable. This is the LAST line
3561
+ // of defense between the LLM API and the user's filesystem.
3523
3562
  function _sanitizeGeneratedFile(name, content, projectName) {
3524
- const isJson = /\.json$/i.test(name) || name.endsWith('package.json');
3563
+ const ext = (name.split('.').pop() || '').toLowerCase();
3564
+ const isJson = ext === 'json' || name.endsWith('package.json');
3525
3565
  const isPkg = name === 'package.json' || name.endsWith('/package.json');
3566
+ const isCode = ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx'].includes(ext);
3526
3567
 
3568
+ // Layer A: detect HTTP error responses leaked as file content
3527
3569
  if (_looksLikeLLMError(content)) {
3528
3570
  if (isPkg) return _fallbackPackageJson(projectName);
3529
- return '/* nha-webcraft: file content from LLM looked like an HTTP error response and was discarded. Re-generate from chat. */';
3571
+ if (isCode) {
3572
+ return `// nha-webcraft: this file's content from the LLM looked like an HTTP error response\n// (status leaked into stream — likely '${_extractLLMErrorHint(content)}').\n// File quarantined to keep the sandbox bootable. Re-generate from chat.\nmodule.exports = {};\n`;
3573
+ }
3574
+ return '<!-- nha-webcraft: LLM error response detected, content discarded. Re-generate from chat. -->';
3530
3575
  }
3576
+
3577
+ // Layer B: validate JSON files
3531
3578
  if (isJson) {
3532
3579
  try { JSON.parse(content); }
3533
3580
  catch {
@@ -3543,9 +3590,40 @@ function _sanitizeGeneratedFile(name, content, projectName) {
3543
3590
  }
3544
3591
  }
3545
3592
  }
3593
+
3594
+ // Layer C: validate JS/JSX/TS files with acorn — catches partial streams
3595
+ // that ended mid-token, leftover HTML/error text mixed with code, etc.
3596
+ if (isCode && content && content.trim()) {
3597
+ try {
3598
+ const parser = ext === 'jsx' || ext === 'tsx' ? acorn.Parser.extend(acornJsx()) : acorn;
3599
+ parser.parse(content, {
3600
+ ecmaVersion: 'latest',
3601
+ sourceType: ['mjs'].includes(ext) ? 'module' : (/\bimport\s+|\bexport\s+/.test(content) ? 'module' : 'script'),
3602
+ allowReturnOutsideFunction: true,
3603
+ allowAwaitOutsideFunction: true,
3604
+ allowHashBang: true,
3605
+ });
3606
+ } catch (parseErr) {
3607
+ // Only quarantine if the error is at the VERY START of the file —
3608
+ // this signals "the whole file is junk" (HTTP error / partial stream)
3609
+ // rather than a normal bug at line 50 that user can fix.
3610
+ const lineMatch = parseErr.message.match(/\((\d+):(\d+)\)/);
3611
+ const startsAtTop = !lineMatch || parseInt(lineMatch[1]) <= 2;
3612
+ if (startsAtTop && content.length < 5000) {
3613
+ return `// nha-webcraft: this file failed to parse near the start (${parseErr.message}).\n// Likely a partial/corrupted stream from the LLM. Quarantined to keep sandbox bootable.\n// Re-generate from chat.\nmodule.exports = {};\n`;
3614
+ }
3615
+ // Otherwise let it through — it's a real code bug, the user will see it
3616
+ }
3617
+ }
3618
+
3546
3619
  return content;
3547
3620
  }
3548
3621
 
3622
+ function _extractLLMErrorHint(content) {
3623
+ const head = content.slice(0, 200).replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
3624
+ return head.slice(0, 80) + (head.length > 80 ? '...' : '');
3625
+ }
3626
+
3549
3627
  // Authoritative list of modules covered by our offline-safe shims.
3550
3628
  // Used by both the pre-scan (to skip them from npm install) and the Tier 1
3551
3629
  // retry (to detect "missing module" stderr that's already covered by a shim).
@@ -3592,6 +3670,39 @@ function _installedDeps(projectDir) {
3592
3670
  } catch { return new Set(); }
3593
3671
  }
3594
3672
 
3673
+ // Node.js built-in modules — these are part of the runtime, not npm packages.
3674
+ // `require('fs')` always works without installation. The pre-scan MUST exclude
3675
+ // these or it tries to `npm install fs` which is meaningless (or worse, picks
3676
+ // up a malicious typosquat). Authoritative list from Node 22 LTS docs.
3677
+ const _NODE_BUILTINS = new Set([
3678
+ 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
3679
+ 'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
3680
+ 'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector',
3681
+ 'inspector/promises', 'module', 'net', 'os', 'path', 'path/posix',
3682
+ 'path/win32', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline',
3683
+ 'readline/promises', 'repl', 'stream', 'stream/consumers', 'stream/promises',
3684
+ 'stream/web', 'string_decoder', 'sys', 'timers', 'timers/promises', 'tls',
3685
+ 'trace_events', 'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'wasi',
3686
+ 'worker_threads', 'zlib',
3687
+ ]);
3688
+
3689
+ // Common LLM hallucinations → real package name. The LLM sometimes invents
3690
+ // shorter aliases for popular packages. We map them transparently so the
3691
+ // generated code "just works" without npm install of fake packages.
3692
+ const _PACKAGE_ALIASES = new Map([
3693
+ ['jwt', 'jsonwebtoken'],
3694
+ ['bcrypt', 'bcryptjs'],
3695
+ ['mongo', 'mongoose'],
3696
+ ['postgres', 'pg'],
3697
+ ['postgresql', 'pg'],
3698
+ ['mysql', 'mysql2'],
3699
+ ['env', 'dotenv'],
3700
+ ['util-lodash', 'lodash'],
3701
+ ['express-cors', 'cors'],
3702
+ ['express-helmet', 'helmet'],
3703
+ ['express-body-parser', 'body-parser'],
3704
+ ]);
3705
+
3595
3706
  /** Walk project files and extract all bare-import module names. */
3596
3707
  export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_000) {
3597
3708
  const found = new Set();
@@ -3624,9 +3735,16 @@ export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_0
3624
3735
  let m;
3625
3736
  while ((m = re.exec(content)) !== null) {
3626
3737
  const spec = m[1];
3738
+ // Filter 1: node:fs, node:path etc.
3627
3739
  if (spec.startsWith('node:')) continue;
3740
+ // Get the bare package name (handle @scope/name and subpaths)
3628
3741
  const pkg = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
3629
- if (pkg) found.add(pkg);
3742
+ if (!pkg) continue;
3743
+ // Filter 2: skip Node built-ins (fs, path, crypto, http, etc.)
3744
+ if (_NODE_BUILTINS.has(pkg)) continue;
3745
+ // Filter 3: resolve LLM-hallucinated aliases to real packages
3746
+ const real = _PACKAGE_ALIASES.get(pkg) || pkg;
3747
+ found.add(real);
3630
3748
  }
3631
3749
  }
3632
3750
  }
@@ -4291,12 +4409,26 @@ module.exports.default = proxy;
4291
4409
  // real implementation, and only falls back to our shim when resolution fails.
4292
4410
  // Also supports NHA_OFFLINE_SHIM=1 to no-op any unresolvable module.
4293
4411
  const shimList = JSON.stringify(Object.keys(shimFiles).filter(f => f !== 'noop.js').map(f => f.replace(/\.js$/, '')));
4412
+ // ALIAS map: bare names the LLM tends to invent → real package name we shim.
4413
+ // Must stay in sync with _PACKAGE_ALIASES in the parent module so pre-scan
4414
+ // and runtime shim agree on what to resolve.
4415
+ const aliasJson = JSON.stringify({
4416
+ 'bcrypt': 'bcryptjs',
4417
+ 'jwt': 'jsonwebtoken',
4418
+ 'mongo': 'mongoose',
4419
+ 'postgres': 'pg',
4420
+ 'postgresql': 'pg',
4421
+ 'env': 'dotenv',
4422
+ 'express-cors': 'cors',
4423
+ 'express-helmet': 'helmet',
4424
+ 'express-body-parser': 'body-parser',
4425
+ });
4294
4426
  const shimIndex = `
4295
4427
  const Module = require('module');
4296
4428
  const path = require('path');
4297
4429
  const __shimDir = ${JSON.stringify(shimDir)};
4298
4430
  const SHIM_NAMES = new Set(${shimList});
4299
- const ALIAS = { 'bcrypt': 'bcryptjs' };
4431
+ const ALIAS = ${aliasJson};
4300
4432
  const OFFLINE = process.env.NHA_OFFLINE_SHIM === '1';
4301
4433
  const _original = Module._resolveFilename.bind(Module);
4302
4434