nothumanallowed 14.2.7 → 14.2.8

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": "14.2.7",
3
+ "version": "14.2.8",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
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 = '14.2.7';
8
+ export const VERSION = '14.2.8';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -826,19 +826,6 @@ function countTokens(text) {
826
826
  return Math.ceil((text || '').length / 4);
827
827
  }
828
828
 
829
- /**
830
- * Simulated streaming: for providers that return full text at once (NHA/Liara),
831
- * we split the text into ~20-char chunks and emit them with setImmediate gaps
832
- * so the browser receives a real byte-by-byte stream over SSE.
833
- */
834
- async function emitTextAsStream(text, onChunk) {
835
- const CHUNK_SIZE = 20;
836
- for (let i = 0; i < text.length; i += CHUNK_SIZE) {
837
- onChunk(text.slice(i, i + CHUNK_SIZE));
838
- await new Promise((r) => setImmediate(r));
839
- }
840
- }
841
-
842
829
  /**
843
830
  * Detects if an LLM output appears to be truncated / incomplete.
844
831
  * Returns true if the file likely needs a continuation call.
@@ -932,7 +919,33 @@ function repairTruncation(content, filename) {
932
919
  return result;
933
920
  }
934
921
 
935
- async function runGenerate(config, projectName, description, blocks, authFields, emit) {
922
+ /** Severe truncation = needs LLM continuation. Minor diffs handled by repairTruncation(). */
923
+ function _isSeverelyTruncated(content, filename) {
924
+ if (!content || content.length < 10) return true;
925
+ const trimmed = content.trimEnd();
926
+ const ext = filename.split('.').pop()?.toLowerCase();
927
+ // Last line ends mid-expression (clear cut-off)
928
+ const lastLine = trimmed.split('\n').pop()?.trim() ?? '';
929
+ if (/[,({=+\-*/<>|&:]$/.test(lastLine) && lastLine.length > 3) return true;
930
+ // Large brace imbalance (>3) — repairTruncation can't reliably fix this
931
+ if (ext === 'js' || ext === 'mjs' || ext === 'ts') {
932
+ const diff = (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length;
933
+ if (diff > 3) return true;
934
+ }
935
+ if (ext === 'css') {
936
+ const diff = (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length;
937
+ if (diff > 3) return true;
938
+ }
939
+ // HTML completely missing closing tags (not just </html> — entire sections missing)
940
+ if (ext === 'html' && !trimmed.includes('</body>') && !trimmed.includes('</html>') && trimmed.length > 500) {
941
+ // Check if it ends mid-tag
942
+ const lastAngle = trimmed.lastIndexOf('<');
943
+ if (lastAngle > trimmed.lastIndexOf('>')) return true; // mid-tag
944
+ }
945
+ return false;
946
+ }
947
+
948
+ async function runGenerate(config, projectName, description, blocks, authFields, emit, abortSignal) {
936
949
  const blocksDesc = Object.entries(blocks)
937
950
  .filter(([, enabled]) => enabled)
938
951
  .map(([key]) => key)
@@ -1079,6 +1092,12 @@ ${prevContext ? `Recent files generated (for consistency):\n${prevContext}\n\n`
1079
1092
  let syntaxError = null;
1080
1093
  const fileTokensIn = countTokens(fileSys) + countTokens(filePrompt);
1081
1094
 
1095
+ // Check abort before each file
1096
+ if (abortSignal?.aborted) {
1097
+ emit({ type: 'status', msg: 'Generation stopped by user.' });
1098
+ break;
1099
+ }
1100
+
1082
1101
  // Retry loop — up to 2 attempts per file
1083
1102
  for (let attempt = 0; attempt < 2; attempt++) {
1084
1103
  try {
@@ -1095,8 +1114,8 @@ ${prevContext ? `Recent files generated (for consistency):\n${prevContext}\n\n`
1095
1114
  .replace(/^```[\w]*\n/, '').replace(/\n```$/, '')
1096
1115
  .replace(/^```[\w]*\r\n/, '').replace(/\r\n```$/, '').trim();
1097
1116
 
1098
- // Continuation loop — up to 3 rounds if file appears truncated
1099
- for (let contRound = 0; contRound < 3 && isFileTruncated(rawOutput, fileSpec.name); contRound++) {
1117
+ // Continuation loop — only for SEVERE truncation (>3 missing braces or mid-line cut)
1118
+ for (let contRound = 0; contRound < 2 && !abortSignal?.aborted && _isSeverelyTruncated(rawOutput, fileSpec.name); contRound++) {
1100
1119
  emit({ type: 'file_chunk', name: fileSpec.name, chunk: `\n/* ... continuing (${contRound + 1}) ... */\n`, fi: fi + 1, total: filePlan.length });
1101
1120
  const contPrompt = `The file ${fileSpec.name} was truncated. Continue EXACTLY from the last line. Output ONLY the remaining code (no repetition, no explanation):
1102
1121
 
@@ -1325,10 +1344,14 @@ export function register(router) {
1325
1344
  if (!projectName || !description) return sendError(res, 400, 'projectName and description required');
1326
1345
 
1327
1346
  const sse = sendSSE(res);
1347
+ // Abort signal: triggered when client disconnects (user clicks Stop)
1348
+ const ac = new AbortController();
1349
+ req.on('close', () => ac.abort());
1350
+ res.on('close', () => ac.abort());
1328
1351
  try {
1329
- await runGenerate(config, projectName, description, blocks, authFields, sse.send);
1352
+ await runGenerate(config, projectName, description, blocks, authFields, sse.send, ac.signal);
1330
1353
  } catch (e) {
1331
- sse.send({ type: 'error', msg: e.message });
1354
+ if (e.name !== 'AbortError') sse.send({ type: 'error', msg: e.message });
1332
1355
  }
1333
1356
  sse.end();
1334
1357
  });