nothumanallowed 16.0.28 → 16.0.30

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.28",
3
+ "version": "16.0.30",
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.28';
8
+ export const VERSION = '16.0.30';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -183,21 +183,39 @@ 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 {}
186
+ // Pre-flight: validate ALL .js/.mjs/.cjs/.jsx files in the project with
187
+ // acorn. If any are corrupted (partial stream / HTTP error leaked as code),
188
+ // quarantine them with a minimal stub. This catches LLM stream interruptions
189
+ // in route handlers (routes/auth.js, routes/billing.js, etc.) not just
190
+ // the entry file.
191
+ const repaired = [];
192
+ const codeExts = new Set(['.js', '.mjs', '.cjs', '.jsx']);
193
+ const scanSkip = new Set(['node_modules', '.git', '.nha-shims', 'dist', 'build', '.next', 'coverage']);
194
+ const projectBase = path.basename(projectDir);
195
+ const stack = [projectDir];
196
+ while (stack.length) {
197
+ const cur = stack.pop();
198
+ let entries;
199
+ try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
200
+ for (const ent of entries) {
201
+ if (scanSkip.has(ent.name) || ent.name.startsWith('.')) continue;
202
+ const abs = path.join(cur, ent.name);
203
+ if (ent.isDirectory()) { stack.push(abs); continue; }
204
+ if (!codeExts.has(path.extname(ent.name))) continue;
205
+ try {
206
+ const content = fs.readFileSync(abs, 'utf-8');
207
+ const relName = path.relative(projectDir, abs);
208
+ const sanitized = _sanitizeGeneratedFile(relName, content, projectBase);
209
+ if (sanitized !== content) {
210
+ fs.writeFileSync(abs + '.corrupt-' + Date.now(), content, 'utf-8');
211
+ fs.writeFileSync(abs, sanitized, 'utf-8');
212
+ repaired.push(relName);
213
+ }
214
+ } catch {}
215
+ }
216
+ }
217
+ if (repaired.length > 0) {
218
+ emit({ type: 'warn', msg: `Pre-flight repair: ${repaired.length} file${repaired.length === 1 ? '' : 's'} quarantined (corrupted content) → ${repaired.join(', ')}. Re-generate from chat to restore them.` });
201
219
  }
202
220
 
203
221
  // ── Phase 2: Dependencies (pre-scan + batch install) ──────────────────
@@ -330,6 +348,43 @@ class SandboxManager {
330
348
  emit({ type: 'phase', phase: 'ready', msg: `Server running on port ${port}` });
331
349
  emit({ type: 'ready', port });
332
350
  }
351
+ // ── HTTP probe: actually fetch GET / and report what came back ─────
352
+ // Tells the user IMMEDIATELY if the sandbox bound the port but serves
353
+ // a 404 / empty body / wrong content-type. Otherwise they see "ready"
354
+ // but the iframe is black/blank and can't tell why.
355
+ try {
356
+ const probeRes = await fetch(`http://127.0.0.1:${port}/`, {
357
+ signal: AbortSignal.timeout(5000),
358
+ redirect: 'manual',
359
+ }).catch(e => ({ _err: e.message }));
360
+ if (probeRes._err) {
361
+ emit({ type: 'warn', msg: `Probe GET /: ${probeRes._err}. The port is bound but the app doesn't respond on / — check your route handlers.` });
362
+ } else {
363
+ const ct = probeRes.headers.get('content-type') || '(none)';
364
+ const status = probeRes.status;
365
+ const body = await probeRes.text().catch(() => '');
366
+ const len = body.length;
367
+ const isHtml = /text\/html/i.test(ct);
368
+ const isJson = /application\/json/i.test(ct);
369
+ const preview = body.slice(0, 200).replace(/\s+/g, ' ').trim();
370
+ emit({ type: 'log', msg: `[probe] GET / → ${status} (${ct}, ${len} bytes)` });
371
+ if (status >= 400) {
372
+ emit({ type: 'warn', msg: `Probe: GET / returned ${status}. The sandbox is running but has no route for "/". Add app.get('/', ...) in your code or generate an index.html.` });
373
+ } else if (len === 0) {
374
+ emit({ type: 'warn', msg: `Probe: GET / returned empty body (${status}). The route exists but sends no response — check that you call res.send() / res.json() / res.end().` });
375
+ } else if (isHtml && !/<\w+/.test(body)) {
376
+ emit({ type: 'warn', msg: `Probe: GET / claims text/html but has no HTML tags. Preview: "${preview}". Likely a plain text leaked into the response.` });
377
+ } else if (isJson) {
378
+ emit({ type: 'status', msg: `Probe: GET / returns JSON. Browser preview will show raw JSON, not a rendered page. Consider serving an index.html with app.use(express.static('public')) or add a / route returning HTML.` });
379
+ } else if (isHtml) {
380
+ emit({ type: 'status', msg: `Probe: GET / returns HTML (${len} bytes). Iframe preview should render fine.` });
381
+ } else {
382
+ emit({ type: 'log', msg: `[probe] body preview: ${preview}` });
383
+ }
384
+ }
385
+ } catch (e) {
386
+ emit({ type: 'log', msg: `[probe] failed: ${e.message}` });
387
+ }
333
388
  return;
334
389
  }
335
390
 
@@ -363,10 +418,22 @@ class SandboxManager {
363
418
  // The pre-scan in Phase 2 catches most cases. Tier 1 here is the safety
364
419
  // net for files generated AFTER pre-scan (e.g. user added a require()
365
420
  // during chat) or for transitive crashes that surface only at runtime.
366
- const missingModules = [...stderrBuf.matchAll(/Cannot find module ['"]([^'"]+)['"]/g)]
421
+ const rawMissing = [...stderrBuf.matchAll(/Cannot find module ['"]([^'"]+)['"]/g)]
367
422
  .map(m => m[1])
368
423
  .filter(m => !m.startsWith('.') && !m.startsWith('/') && !m.startsWith('node:'))
369
- .map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0]);
424
+ .map(m => m.startsWith('@') ? m.split('/').slice(0, 2).join('/') : m.split('/')[0])
425
+ // Drop Node.js built-ins — they're in the runtime, can't be installed
426
+ .filter(m => !_NODE_BUILTINS.has(m));
427
+ // Resolve LLM hallucinated aliases (jwt → jsonwebtoken, bcrypt → bcryptjs)
428
+ const missingModules = rawMissing.map(m => _PACKAGE_ALIASES.get(m) || m);
429
+ // Detect aliases — if the shim already covers the resolved name, no install needed
430
+ const aliasedFromShim = rawMissing.filter(m => {
431
+ const real = _PACKAGE_ALIASES.get(m);
432
+ return real && _SHIMMED_MODULES.has(real);
433
+ });
434
+ if (aliasedFromShim.length > 0) {
435
+ 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".` });
436
+ }
370
437
  const uniqueMissing = [...new Set(missingModules)].filter(m => !_SHIMMED_MODULES.has(m));
371
438
  if (uniqueMissing.length > 0 && _attempt < MAX_RETRIES) {
372
439
  emit({ type: 'phase', phase: 'autofix', msg: `Missing module${uniqueMissing.length === 1 ? '' : 's'}: ${uniqueMissing.join(', ')} — batch installing...` });
@@ -3658,6 +3725,39 @@ function _installedDeps(projectDir) {
3658
3725
  } catch { return new Set(); }
3659
3726
  }
3660
3727
 
3728
+ // Node.js built-in modules — these are part of the runtime, not npm packages.
3729
+ // `require('fs')` always works without installation. The pre-scan MUST exclude
3730
+ // these or it tries to `npm install fs` which is meaningless (or worse, picks
3731
+ // up a malicious typosquat). Authoritative list from Node 22 LTS docs.
3732
+ export const _NODE_BUILTINS = new Set([
3733
+ 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
3734
+ 'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
3735
+ 'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector',
3736
+ 'inspector/promises', 'module', 'net', 'os', 'path', 'path/posix',
3737
+ 'path/win32', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline',
3738
+ 'readline/promises', 'repl', 'stream', 'stream/consumers', 'stream/promises',
3739
+ 'stream/web', 'string_decoder', 'sys', 'timers', 'timers/promises', 'tls',
3740
+ 'trace_events', 'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'wasi',
3741
+ 'worker_threads', 'zlib',
3742
+ ]);
3743
+
3744
+ // Common LLM hallucinations → real package name. The LLM sometimes invents
3745
+ // shorter aliases for popular packages. We map them transparently so the
3746
+ // generated code "just works" without npm install of fake packages.
3747
+ export const _PACKAGE_ALIASES = new Map([
3748
+ ['jwt', 'jsonwebtoken'],
3749
+ ['bcrypt', 'bcryptjs'],
3750
+ ['mongo', 'mongoose'],
3751
+ ['postgres', 'pg'],
3752
+ ['postgresql', 'pg'],
3753
+ ['mysql', 'mysql2'],
3754
+ ['env', 'dotenv'],
3755
+ ['util-lodash', 'lodash'],
3756
+ ['express-cors', 'cors'],
3757
+ ['express-helmet', 'helmet'],
3758
+ ['express-body-parser', 'body-parser'],
3759
+ ]);
3760
+
3661
3761
  /** Walk project files and extract all bare-import module names. */
3662
3762
  export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_000) {
3663
3763
  const found = new Set();
@@ -3690,9 +3790,16 @@ export function _scanProjectImports(projectDir, maxFiles = 500, maxBytes = 200_0
3690
3790
  let m;
3691
3791
  while ((m = re.exec(content)) !== null) {
3692
3792
  const spec = m[1];
3793
+ // Filter 1: node:fs, node:path etc.
3693
3794
  if (spec.startsWith('node:')) continue;
3795
+ // Get the bare package name (handle @scope/name and subpaths)
3694
3796
  const pkg = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
3695
- if (pkg) found.add(pkg);
3797
+ if (!pkg) continue;
3798
+ // Filter 2: skip Node built-ins (fs, path, crypto, http, etc.)
3799
+ if (_NODE_BUILTINS.has(pkg)) continue;
3800
+ // Filter 3: resolve LLM-hallucinated aliases to real packages
3801
+ const real = _PACKAGE_ALIASES.get(pkg) || pkg;
3802
+ found.add(real);
3696
3803
  }
3697
3804
  }
3698
3805
  }
@@ -4357,12 +4464,26 @@ module.exports.default = proxy;
4357
4464
  // real implementation, and only falls back to our shim when resolution fails.
4358
4465
  // Also supports NHA_OFFLINE_SHIM=1 to no-op any unresolvable module.
4359
4466
  const shimList = JSON.stringify(Object.keys(shimFiles).filter(f => f !== 'noop.js').map(f => f.replace(/\.js$/, '')));
4467
+ // ALIAS map: bare names the LLM tends to invent → real package name we shim.
4468
+ // Must stay in sync with _PACKAGE_ALIASES in the parent module so pre-scan
4469
+ // and runtime shim agree on what to resolve.
4470
+ const aliasJson = JSON.stringify({
4471
+ 'bcrypt': 'bcryptjs',
4472
+ 'jwt': 'jsonwebtoken',
4473
+ 'mongo': 'mongoose',
4474
+ 'postgres': 'pg',
4475
+ 'postgresql': 'pg',
4476
+ 'env': 'dotenv',
4477
+ 'express-cors': 'cors',
4478
+ 'express-helmet': 'helmet',
4479
+ 'express-body-parser': 'body-parser',
4480
+ });
4360
4481
  const shimIndex = `
4361
4482
  const Module = require('module');
4362
4483
  const path = require('path');
4363
4484
  const __shimDir = ${JSON.stringify(shimDir)};
4364
4485
  const SHIM_NAMES = new Set(${shimList});
4365
- const ALIAS = { 'bcrypt': 'bcryptjs' };
4486
+ const ALIAS = ${aliasJson};
4366
4487
  const OFFLINE = process.env.NHA_OFFLINE_SHIM === '1';
4367
4488
  const _original = Module._resolveFilename.bind(Module);
4368
4489