nothumanallowed 16.0.29 → 16.0.31

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.29",
3
+ "version": "16.0.31",
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.29';
8
+ export const VERSION = '16.0.31';
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
 
@@ -3474,6 +3529,38 @@ function _patchEntry(projectDir, entryFile, shimDir, port) {
3474
3529
  `process.env.HOST = '0.0.0.0';`,
3475
3530
  `process.env.NODE_ENV = 'development';`,
3476
3531
  `require('${shimAbs}');`,
3532
+ `// Force-bind to NHA's assigned port even when the app hardcodes a port`,
3533
+ `// like 3000/5000/8080. Monkey-patch http.Server.prototype.listen so the`,
3534
+ `// FIRST positional numeric arg is replaced with our port. This prevents`,
3535
+ `// "Connessione negata" when the iframe targets ${port} but the app picked`,
3536
+ `// a different port internally.`,
3537
+ `(function(){`,
3538
+ ` const http = require('http');`,
3539
+ ` const NHA_PORT = ${port};`,
3540
+ ` const _origListen = http.Server.prototype.listen;`,
3541
+ ` let _bound = false;`,
3542
+ ` http.Server.prototype.listen = function(...args) {`,
3543
+ ` if (_bound) return _origListen.apply(this, args);`,
3544
+ ` if (typeof args[0] === 'object' && args[0] !== null && 'port' in args[0]) {`,
3545
+ ` const requested = args[0].port;`,
3546
+ ` if (requested !== NHA_PORT && requested !== 0) {`,
3547
+ ` console.log('[nha-launcher] app requested port ' + requested + ', forcing to NHA_PORT=' + NHA_PORT);`,
3548
+ ` args[0] = Object.assign({}, args[0], { port: NHA_PORT, host: '0.0.0.0' });`,
3549
+ ` }`,
3550
+ ` } else if (typeof args[0] === 'number' || typeof args[0] === 'string') {`,
3551
+ ` const requested = parseInt(args[0]);`,
3552
+ ` if (!isNaN(requested) && requested !== NHA_PORT && requested !== 0) {`,
3553
+ ` console.log('[nha-launcher] app requested port ' + requested + ', forcing to NHA_PORT=' + NHA_PORT);`,
3554
+ ` args[0] = NHA_PORT;`,
3555
+ ` // If second positional was host, replace with 0.0.0.0; otherwise insert`,
3556
+ ` if (typeof args[1] === 'string') args[1] = '0.0.0.0';`,
3557
+ ` else if (typeof args[1] === 'function') args.splice(1, 0, '0.0.0.0');`,
3558
+ ` }`,
3559
+ ` }`,
3560
+ ` _bound = true;`,
3561
+ ` return _origListen.apply(this, args);`,
3562
+ ` };`,
3563
+ `})();`,
3477
3564
  `// Inject error reporter into HTML responses`,
3478
3565
  `const _nhaErrScript = require('fs').readFileSync('${path.join(shimDir, 'error-reporter.html').replace(/\\/g, '/')}', 'utf-8');`,
3479
3566
  `const _origWrite = require('http').ServerResponse.prototype.write;`,
@@ -3674,7 +3761,7 @@ function _installedDeps(projectDir) {
3674
3761
  // `require('fs')` always works without installation. The pre-scan MUST exclude
3675
3762
  // these or it tries to `npm install fs` which is meaningless (or worse, picks
3676
3763
  // up a malicious typosquat). Authoritative list from Node 22 LTS docs.
3677
- const _NODE_BUILTINS = new Set([
3764
+ export const _NODE_BUILTINS = new Set([
3678
3765
  'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
3679
3766
  'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
3680
3767
  'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector',
@@ -3689,7 +3776,7 @@ const _NODE_BUILTINS = new Set([
3689
3776
  // Common LLM hallucinations → real package name. The LLM sometimes invents
3690
3777
  // shorter aliases for popular packages. We map them transparently so the
3691
3778
  // generated code "just works" without npm install of fake packages.
3692
- const _PACKAGE_ALIASES = new Map([
3779
+ export const _PACKAGE_ALIASES = new Map([
3693
3780
  ['jwt', 'jsonwebtoken'],
3694
3781
  ['bcrypt', 'bcryptjs'],
3695
3782
  ['mongo', 'mongoose'],