nothumanallowed 16.0.53 → 16.0.55

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.53",
3
+ "version": "16.0.55",
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.53';
8
+ export const VERSION = '16.0.55';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -794,12 +794,36 @@ ${original.slice(0, 12_000)}`;
794
794
  let fixed = '';
795
795
  await callLLMStream(loadConfig(), 'You are a precise code repair assistant. Output only the corrected file, no explanation.', fixPrompt, (c) => { fixed += c; }, { max_tokens: 16384 });
796
796
  fixed = fixed.replace(/^```[\w]*\n/, '').replace(/\n```$/, '').trim();
797
- if (fixed.length > 20 && fixed !== original) {
797
+
798
+ // VALIDATION: prevent rollback hell. Reject the LLM output if:
799
+ // 1. Empty / too short (< 20 chars or < 30% of original)
800
+ // 2. Identical to original (no fix attempted)
801
+ // 3. Looks like LLM error response leaked
802
+ // 4. Syntax-checkable file (.js/.mjs) that doesn't parse — would be worse than original
803
+ const tooShort = fixed.length < 20 || fixed.length < original.length * 0.3;
804
+ const isErrorLeak = _looksLikeLLMError(fixed);
805
+ let syntaxBroken = false;
806
+ if (!tooShort && !isErrorLeak && (rel.endsWith('.js') || rel.endsWith('.mjs') || rel.endsWith('.cjs'))) {
807
+ try { new Function(fixed); } catch (synErr) {
808
+ // Try with sourceType:module via acorn before declaring broken
809
+ try { acorn.parse(fixed, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true }); }
810
+ catch { syntaxBroken = true; }
811
+ }
812
+ }
813
+ if (tooShort || isErrorLeak || syntaxBroken || fixed === original) {
814
+ // Backup the LLM attempt for debugging, but DON'T overwrite the file
815
+ try { fs.writeFileSync(abs + '.llm-rejected-' + Date.now(), fixed, 'utf-8'); } catch {}
816
+ const reason = tooShort ? `output too short (${fixed.length} vs ${original.length} chars)`
817
+ : isErrorLeak ? 'output looks like an LLM/provider error response'
818
+ : syntaxBroken ? 'output has worse syntax errors than input'
819
+ : 'no change';
820
+ emit({ type: 'warn', msg: `Auto-fix REJECTED for ${rel}: ${reason}. Original file preserved.` });
821
+ } else {
822
+ // Backup original before overwriting
823
+ try { fs.writeFileSync(abs + '.before-autofix-' + Date.now(), original, 'utf-8'); } catch {}
798
824
  fs.writeFileSync(abs, fixed, 'utf-8');
799
- emit({ type: 'status', msg: `Auto-fix: ✓ repaired ${rel} (${fixed.length} chars)` });
825
+ emit({ type: 'status', msg: `Auto-fix: ✓ repaired ${rel} (${original.length} → ${fixed.length} chars)` });
800
826
  anyFixed = true;
801
- } else {
802
- emit({ type: 'warn', msg: `Auto-fix: LLM returned no useful change for ${rel}` });
803
827
  }
804
828
  } catch (e) {
805
829
  emit({ type: 'warn', msg: `Auto-fix: LLM repair of ${rel} failed — ${(e.message || '').slice(0, 200)}` });
@@ -4336,43 +4360,58 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
4336
4360
  const model = config?.llm?.model || config?.llm?.[provider]?.model || 'default';
4337
4361
  if (emit) emit({ type: 'status', msg: `CSS coverage ${(analysis.coverage * 100).toFixed(0)}% (${analysis.missing.length} selectors missing). Auto-extending ${targetRel} via ${provider}:${model} (timeout 60s)...` });
4338
4362
 
4339
- // Call LLM with timeout + progress heartbeat + early warning when no bytes arrive.
4340
- // Different providers have different latencies: Claude is fast (~5-15s),
4341
- // OpenAI similar, Liara/Qwen3 can be 20-40s under load, Gemini sometimes
4342
- // stuck on long prompts. Adapt max_tokens to 4096 (down from 8192) to keep
4343
- // Liara responsive.
4363
+ // Smart timeout: timeout ONLY if no bytes received for N seconds (provider
4364
+ // stuck/dead), NOT if total elapsed exceeds N (the LLM might be legitimately
4365
+ // streaming a large CSS file at 250 b/s for 90+ seconds). Absolute hard cap
4366
+ // at 300s to prevent runaway.
4344
4367
  let body = '';
4345
4368
  let lastChunkAt = Date.now();
4346
4369
  const startedAt = Date.now();
4347
- const timeoutMs = 60_000;
4370
+ const noProgressTimeoutMs = 30_000; // no bytes for 30s → timeout
4371
+ const absoluteTimeoutMs = 300_000; // 5min absolute cap
4348
4372
  let earlyWarningEmitted = false;
4373
+ let timedOut = false;
4374
+ let aborted = false;
4375
+ const abortController = new AbortController();
4376
+
4349
4377
  const heartbeatInterval = setInterval(() => {
4350
4378
  if (!emit) return;
4351
4379
  const elapsed = ((Date.now() - startedAt) / 1000).toFixed(0);
4352
4380
  const sinceLast = ((Date.now() - lastChunkAt) / 1000).toFixed(0);
4353
4381
  emit({ type: 'status', msg: `LLM extend: ${elapsed}s elapsed, ${body.length} bytes received (${sinceLast}s since last chunk)` });
4354
- // Early warning if no bytes at all after 15s — provider is likely stuck or rate-limited
4355
4382
  if (!earlyWarningEmitted && body.length === 0 && Date.now() - startedAt > 15_000) {
4356
4383
  earlyWarningEmitted = true;
4357
- 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, or check your API key.` });
4384
+ 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
+ }
4386
+ // No-progress timeout: only if STUCK (zero chunks for 30s)
4387
+ if (Date.now() - lastChunkAt > noProgressTimeoutMs && body.length > 0) {
4388
+ timedOut = true;
4389
+ aborted = true;
4390
+ abortController.abort();
4391
+ }
4392
+ // Absolute timeout
4393
+ if (Date.now() - startedAt > absoluteTimeoutMs) {
4394
+ timedOut = true;
4395
+ aborted = true;
4396
+ abortController.abort();
4358
4397
  }
4359
4398
  }, 5_000);
4360
4399
 
4361
4400
  try {
4362
- await Promise.race([
4363
- callLLMStream(config, sys, user, (chunk) => {
4364
- body += chunk;
4365
- lastChunkAt = Date.now();
4366
- }, { max_tokens: 4096 }),
4367
- new Promise((_, reject) => setTimeout(() => reject(new Error('LLM timeout after ' + (timeoutMs / 1000) + 's — provider ' + provider + ' did not respond')), timeoutMs)),
4368
- ]);
4401
+ await callLLMStream(config, sys, user, (chunk) => {
4402
+ if (aborted) return;
4403
+ body += chunk;
4404
+ lastChunkAt = Date.now();
4405
+ }, { max_tokens: 4096, signal: abortController.signal });
4369
4406
  } catch (e) {
4370
4407
  clearInterval(heartbeatInterval);
4371
- const errMsg = (e.message || String(e)).slice(0, 200);
4408
+ const errMsg = timedOut
4409
+ ? `no chunks received for ${(noProgressTimeoutMs / 1000)}s — provider ${provider} appears stuck (received ${body.length} bytes before stalling)`
4410
+ : (e.message || String(e)).slice(0, 200);
4372
4411
  if (emit) {
4373
4412
  emit({ type: 'warn', msg: `CSS extend failed: ${errMsg}` });
4374
- if (errMsg.includes('timeout') || body.length === 0) {
4375
- emit({ type: 'warn', msg: `Provider ${provider} unresponsive. Open NHA Settings and switch to a different provider (Anthropic Claude is fastest), or check that your API key is valid. Sandbox continues with current CSS.` });
4413
+ if (timedOut && body.length > 0) {
4414
+ emit({ type: 'warn', msg: `Got ${body.length} bytes of partial CSS but provider stalled. Keeping current file unchanged. Try again or switch provider.` });
4376
4415
  }
4377
4416
  }
4378
4417
  return { extended: false, reason: 'llm_failed', error: errMsg, partialBytes: body.length };