nothumanallowed 16.0.36 → 16.0.38
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 +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/webcraft.mjs +121 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.38",
|
|
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.
|
|
8
|
+
export const VERSION = '16.0.38';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -218,6 +218,17 @@ class SandboxManager {
|
|
|
218
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.` });
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
// Pre-flight: create missing data dirs/files referenced by code.
|
|
222
|
+
// LLMs frequently generate `fs.writeFile('data/users.json', ...)` without
|
|
223
|
+
// creating data/ first → ENOENT crash on boot. Scan code for these
|
|
224
|
+
// references and seed empty placeholders BEFORE the sandbox starts.
|
|
225
|
+
try {
|
|
226
|
+
const dataFiles = _detectMissingDataFiles(projectDir);
|
|
227
|
+
if (dataFiles.length > 0) {
|
|
228
|
+
emit({ type: 'status', msg: `Pre-flight: seeded ${dataFiles.length} data file${dataFiles.length === 1 ? '' : 's'} → ${dataFiles.slice(0, 5).join(', ')}${dataFiles.length > 5 ? '...' : ''}` });
|
|
229
|
+
}
|
|
230
|
+
} catch {}
|
|
231
|
+
|
|
221
232
|
// ── Phase 2: Dependencies (pre-scan + batch install) ──────────────────
|
|
222
233
|
// Pre-scan the project source files for require()/import statements and
|
|
223
234
|
// diff against package.json + node_modules. Install everything missing in
|
|
@@ -369,7 +380,16 @@ class SandboxManager {
|
|
|
369
380
|
const preview = body.slice(0, 200).replace(/\s+/g, ' ').trim();
|
|
370
381
|
emit({ type: 'log', msg: `[probe] GET / → ${status} (${ct}, ${len} bytes)` });
|
|
371
382
|
if (status >= 400) {
|
|
372
|
-
|
|
383
|
+
// Show the actual error body so the user knows WHAT went wrong,
|
|
384
|
+
// not just the status code. 500s from Express handlers often
|
|
385
|
+
// include the stack trace or error message in the body.
|
|
386
|
+
const bodyPreview = body.slice(0, 800).trim();
|
|
387
|
+
emit({ type: 'error', msg: `Probe: GET / returned ${status}. Response body:\n${bodyPreview}` });
|
|
388
|
+
if (status === 500) {
|
|
389
|
+
emit({ type: 'warn', msg: `500 Internal Server Error usually means the route handler threw. Common causes: missing await on async function, JSON.parse on empty file, undefined variable, DB connection failure. Check stderr logs above for the actual stack trace.` });
|
|
390
|
+
} else if (status === 404) {
|
|
391
|
+
emit({ type: 'warn', msg: `404 means no route matches "/". Add app.get('/', ...) returning HTML, or app.use(express.static('public')) if you have an index.html in public/.` });
|
|
392
|
+
}
|
|
373
393
|
} else if (len === 0) {
|
|
374
394
|
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
395
|
} else if (isHtml && !/<\w+/.test(body)) {
|
|
@@ -454,6 +474,36 @@ class SandboxManager {
|
|
|
454
474
|
return this.start(projectName, projectDir, emit, _attempt + 1);
|
|
455
475
|
}
|
|
456
476
|
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── Tier 1b: ENOENT on file write → create missing path + retry ──────
|
|
480
|
+
// Deterministic. LLM SaaS code often writes to data/users.json without
|
|
481
|
+
// mkdir -p data/ first → ENOENT. We detect, create the dir + empty
|
|
482
|
+
// placeholder JSON, and retry. Zero LLM call needed.
|
|
483
|
+
const enoentMatch = stderrBuf.match(/ENOENT[^']*open\s+['"]([^'"]+)['"]/);
|
|
484
|
+
if (enoentMatch && _attempt < MAX_RETRIES) {
|
|
485
|
+
const missingPath = enoentMatch[1];
|
|
486
|
+
// Only auto-create if path is inside the project
|
|
487
|
+
const projectAbs = path.resolve(projectDir);
|
|
488
|
+
const missingAbs = path.resolve(missingPath);
|
|
489
|
+
if (missingAbs.startsWith(projectAbs)) {
|
|
490
|
+
emit({ type: 'phase', phase: 'autofix', msg: `ENOENT on ${missingPath} — creating directory + empty placeholder...` });
|
|
491
|
+
try {
|
|
492
|
+
ensureDir(path.dirname(missingAbs));
|
|
493
|
+
if (!fs.existsSync(missingAbs)) {
|
|
494
|
+
const ext = path.extname(missingAbs).toLowerCase();
|
|
495
|
+
let stub = '';
|
|
496
|
+
if (ext === '.json') stub = /users|posts|items|list|todos|comments|orders|products/i.test(missingPath) ? '[]' : '{}';
|
|
497
|
+
else if (ext === '.sqlite' || ext === '.db') { /* skip binary */ }
|
|
498
|
+
else stub = '';
|
|
499
|
+
if (ext !== '.sqlite' && ext !== '.db') fs.writeFileSync(missingAbs, stub, 'utf-8');
|
|
500
|
+
}
|
|
501
|
+
emit({ type: 'status', msg: `Created ${missingPath} — retrying (attempt ${_attempt + 1}/${MAX_RETRIES})...` });
|
|
502
|
+
return this.start(projectName, projectDir, emit, _attempt + 1);
|
|
503
|
+
} catch (e) {
|
|
504
|
+
emit({ type: 'warn', msg: `Failed to create ${missingPath}: ${e.message.slice(0, 200)}` });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
457
507
|
} else if (missingModules.length > 0 && uniqueMissing.length === 0) {
|
|
458
508
|
// All missing modules are shimmable in THIS version of the CLI. If we
|
|
459
509
|
// reached this branch, the user has an OLDER CLI installed that doesn't
|
|
@@ -3554,6 +3604,61 @@ function _placeholderContent(kind, refPath) {
|
|
|
3554
3604
|
return '';
|
|
3555
3605
|
}
|
|
3556
3606
|
|
|
3607
|
+
/**
|
|
3608
|
+
* Scan all .js/.mjs/.ts files for filesystem paths referenced in string
|
|
3609
|
+
* literals (typically used for file-based storage by LLM-generated SaaS code:
|
|
3610
|
+
* fs.writeFile('data/users.json', ...), fs.readFile('db/posts.json'), etc).
|
|
3611
|
+
* Creates missing directories + empty JSON placeholders so the app can boot
|
|
3612
|
+
* instead of crashing with ENOENT on first write.
|
|
3613
|
+
*/
|
|
3614
|
+
export function _detectMissingDataFiles(projectDir) {
|
|
3615
|
+
const created = [];
|
|
3616
|
+
const exts = new Set(['.js', '.mjs', '.cjs', '.ts']);
|
|
3617
|
+
const skipDirs = new Set(['node_modules', '.git', '.nha-shims', 'dist', 'build', '.next', 'public']);
|
|
3618
|
+
// Match string literals that look like project-relative storage paths:
|
|
3619
|
+
// "data/users.json", './db/posts.json', "uploads/", "logs/app.log"
|
|
3620
|
+
// Skip absolute paths, URLs, node_modules/, dotfiles.
|
|
3621
|
+
const re = /['"`](?:\.\/)?((?:data|db|storage|uploads|logs|tmp|cache|sessions)\/[A-Za-z0-9_\-./]+)['"`]/g;
|
|
3622
|
+
const stack = [projectDir];
|
|
3623
|
+
const seen = new Set();
|
|
3624
|
+
while (stack.length) {
|
|
3625
|
+
const cur = stack.pop();
|
|
3626
|
+
let entries;
|
|
3627
|
+
try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
3628
|
+
for (const ent of entries) {
|
|
3629
|
+
if (skipDirs.has(ent.name) || ent.name.startsWith('.')) continue;
|
|
3630
|
+
const abs = path.join(cur, ent.name);
|
|
3631
|
+
if (ent.isDirectory()) { stack.push(abs); continue; }
|
|
3632
|
+
if (!exts.has(path.extname(ent.name))) continue;
|
|
3633
|
+
let content;
|
|
3634
|
+
try { content = fs.readFileSync(abs, 'utf-8'); } catch { continue; }
|
|
3635
|
+
let m;
|
|
3636
|
+
re.lastIndex = 0;
|
|
3637
|
+
while ((m = re.exec(content)) !== null) {
|
|
3638
|
+
const rel = m[1].replace(/\\/g, '/');
|
|
3639
|
+
if (seen.has(rel)) continue;
|
|
3640
|
+
seen.add(rel);
|
|
3641
|
+
const fullPath = path.join(projectDir, rel);
|
|
3642
|
+
try {
|
|
3643
|
+
ensureDir(path.dirname(fullPath));
|
|
3644
|
+
// Only create file if it looks like a file (has extension) and doesn't exist
|
|
3645
|
+
if (path.extname(rel) && !fs.existsSync(fullPath)) {
|
|
3646
|
+
const ext = path.extname(rel).toLowerCase();
|
|
3647
|
+
let stub = '';
|
|
3648
|
+
if (ext === '.json') stub = rel.includes('users') || rel.includes('posts') || rel.includes('items') || rel.includes('list') ? '[]' : '{}';
|
|
3649
|
+
else if (ext === '.txt' || ext === '.log') stub = '';
|
|
3650
|
+
else if (ext === '.sqlite' || ext === '.db') continue; // skip binary
|
|
3651
|
+
else stub = '';
|
|
3652
|
+
fs.writeFileSync(fullPath, stub, 'utf-8');
|
|
3653
|
+
created.push(rel);
|
|
3654
|
+
}
|
|
3655
|
+
} catch {}
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
return created;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3557
3662
|
export function autoRepairProject(projectName) {
|
|
3558
3663
|
const dir = ProjectStore.dir(projectName);
|
|
3559
3664
|
if (!fs.existsSync(dir)) throw new Error('project not found');
|
|
@@ -3562,6 +3667,13 @@ export function autoRepairProject(projectName) {
|
|
|
3562
3667
|
const filesRepaired = new Set();
|
|
3563
3668
|
const filesCreated = [];
|
|
3564
3669
|
|
|
3670
|
+
// Phase 0: create missing data files referenced by code (data/X.json, etc)
|
|
3671
|
+
const dataCreated = _detectMissingDataFiles(dir);
|
|
3672
|
+
for (const f of dataCreated) {
|
|
3673
|
+
filesCreated.push(f);
|
|
3674
|
+
repairs.push({ file: f, kind: 'missing-data-file', source: 'code-reference' });
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3565
3677
|
// Walk every HTML file in the project
|
|
3566
3678
|
const stack = [dir];
|
|
3567
3679
|
const skipDirs = new Set(['node_modules', '.git', '.nha-shims', 'dist', 'build', '.next']);
|
|
@@ -4031,6 +4143,14 @@ const _LLM_ERROR_MARKERS = [
|
|
|
4031
4143
|
/^\s*error code:\s*\d{3,4}/i,
|
|
4032
4144
|
// Standalone HTTP status phrase at the start of a code file is suspicious
|
|
4033
4145
|
/^\s*(HTTP\/[\d.]+\s+)?[45]\d{2}\s+/,
|
|
4146
|
+
// NHA/Liara backend retry-wrapper errors leaked as file content:
|
|
4147
|
+
// "/* Retry: NHA Free 502: {"error":"Failed to reach Liara",...}"
|
|
4148
|
+
// "Retry: NHA 503", "// NHA Free Error 429"
|
|
4149
|
+
/^\s*(?:\/\*\s*|\/\/\s*)?Retry:?\s*NHA(\s+Free)?(\s+\d{3})?/i,
|
|
4150
|
+
/^\s*(?:\/\*\s*|\/\/\s*)?Failed to reach (Liara|NHA|OpenAI|Anthropic|Gemini|provider)/i,
|
|
4151
|
+
/^\s*\{?\s*"error"\s*:\s*"Failed to reach/i,
|
|
4152
|
+
// Plain "fetch failed", "ECONNRESET" etc. as first line of a code file
|
|
4153
|
+
/^\s*(?:\/\*\s*|\/\/\s*)?(?:fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT)\b/i,
|
|
4034
4154
|
];
|
|
4035
4155
|
|
|
4036
4156
|
function _looksLikeLLMError(content) {
|