nothumanallowed 14.2.4 → 14.2.6

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.4",
3
+ "version": "14.2.6",
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.4';
8
+ export const VERSION = '14.2.6';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -106,7 +106,7 @@ export function register(router) {
106
106
  // Restart daemon (Telegram/Discord) + self-restart UI server
107
107
  setTimeout(async () => {
108
108
  try {
109
- const { spawn, execSync } = await import('child_process');
109
+ const { execSync } = await import('child_process');
110
110
 
111
111
  // Restart ops daemon if running (so Telegram/Discord reconnect with new code)
112
112
  try {
@@ -117,22 +117,20 @@ export function register(router) {
117
117
  }
118
118
  } catch { /* daemon restart is best-effort */ }
119
119
 
120
- // Find the nha binary in PATH (points to the newly installed version)
121
- let nhaBin = 'nha';
122
- try {
123
- nhaBin = execSync('which nha', { encoding: 'utf-8', timeout: 3000 }).trim() || 'nha';
124
- } catch { /* fallback to bare 'nha' */ }
125
-
126
- const child = spawn(nhaBin, ['ui'], {
120
+ // Self-restart: launch a shell script that waits for us to die, then starts nha ui.
121
+ // This avoids the port-in-use race condition.
122
+ // Detect current port from the server's address
123
+ let port = '3847';
124
+ try { port = String(req.socket?.localPort || 3847); } catch {}
125
+ const { spawn } = await import('child_process');
126
+ const restartScript = `sleep 2 && nha ui --port ${port}`;
127
+ const child = spawn('sh', ['-c', restartScript], {
127
128
  detached: true,
128
129
  stdio: 'ignore',
129
130
  env: { ...process.env, NHA_RESTARTED: '1' },
130
- shell: true,
131
131
  });
132
132
  child.unref();
133
- // Give the child process time to bind the port before we die
134
- await new Promise((r) => setTimeout(r, 2000));
135
- } catch { /* ignore spawn errors */ }
133
+ } catch { /* ignore errors */ }
136
134
  process.exit(0);
137
135
  }, 1500);
138
136
  } catch (e) {
@@ -848,20 +848,17 @@ function isFileTruncated(content, filename) {
848
848
  const trimmed = content.trimEnd();
849
849
  const ext = filename.split('.').pop()?.toLowerCase();
850
850
 
851
- // Common truncation signs: ends mid-statement without closing bracket/brace
852
851
  if (ext === 'js' || ext === 'mjs' || ext === 'ts') {
853
- // Balanced braces check (fast approximation)
854
852
  const open = (trimmed.match(/\{/g) || []).length;
855
853
  const close = (trimmed.match(/\}/g) || []).length;
856
- if (open > close + 2) return true;
857
- // Last meaningful line should not end with an operator or comma
854
+ if (open > close) return true; // ANY unbalanced brace = truncated
858
855
  const lastLine = trimmed.split('\n').pop()?.trim() ?? '';
859
856
  if (/[,({=+\-*/<>|&]$/.test(lastLine)) return true;
860
857
  }
861
858
  if (ext === 'css') {
862
859
  const open = (trimmed.match(/\{/g) || []).length;
863
860
  const close = (trimmed.match(/\}/g) || []).length;
864
- if (open > close + 1) return true;
861
+ if (open > close) return true;
865
862
  }
866
863
  if (ext === 'html') {
867
864
  if (!trimmed.includes('</html>') && !trimmed.includes('</body>')) return true;
@@ -872,6 +869,69 @@ function isFileTruncated(content, filename) {
872
869
  return false;
873
870
  }
874
871
 
872
+ /**
873
+ * Deterministic repair of common truncation artifacts.
874
+ * Adds missing closing braces, tags, etc. WITHOUT calling the LLM.
875
+ */
876
+ function repairTruncation(content, filename) {
877
+ if (!content) return content;
878
+ const ext = filename.split('.').pop()?.toLowerCase();
879
+ let result = content.trimEnd();
880
+
881
+ if (ext === 'js' || ext === 'mjs' || ext === 'ts') {
882
+ // Balance curly braces
883
+ const open = (result.match(/\{/g) || []).length;
884
+ const close = (result.match(/\}/g) || []).length;
885
+ const missing = open - close;
886
+ if (missing > 0 && missing <= 10) {
887
+ result += '\n' + '}\n'.repeat(missing);
888
+ }
889
+ // Balance parentheses
890
+ const openP = (result.match(/\(/g) || []).length;
891
+ const closeP = (result.match(/\)/g) || []).length;
892
+ const missingP = openP - closeP;
893
+ if (missingP > 0 && missingP <= 5) {
894
+ // Find last line and append
895
+ const lines = result.split('\n');
896
+ const lastIdx = lines.length - 1;
897
+ lines[lastIdx] = lines[lastIdx] + ')'.repeat(missingP) + ';';
898
+ result = lines.join('\n');
899
+ }
900
+ }
901
+
902
+ if (ext === 'css') {
903
+ const open = (result.match(/\{/g) || []).length;
904
+ const close = (result.match(/\}/g) || []).length;
905
+ const missing = open - close;
906
+ if (missing > 0 && missing <= 10) {
907
+ result += '\n' + '}\n'.repeat(missing);
908
+ }
909
+ }
910
+
911
+ if (ext === 'html' || ext === 'htm') {
912
+ if (!result.includes('</body>')) result += '\n</body>';
913
+ if (!result.includes('</html>')) result += '\n</html>';
914
+ }
915
+
916
+ if (ext === 'json') {
917
+ // Try to fix unclosed JSON
918
+ try { JSON.parse(result); } catch {
919
+ // Count brackets
920
+ const openB = (result.match(/\[/g) || []).length;
921
+ const closeB = (result.match(/\]/g) || []).length;
922
+ const openC = (result.match(/\{/g) || []).length;
923
+ const closeC = (result.match(/\}/g) || []).length;
924
+ // Remove trailing comma if present
925
+ result = result.replace(/,\s*$/, '');
926
+ // Add missing closers
927
+ if (openB > closeB) result += '\n' + ']'.repeat(openB - closeB);
928
+ if (openC > closeC) result += '\n' + '}'.repeat(openC - closeC);
929
+ }
930
+ }
931
+
932
+ return result;
933
+ }
934
+
875
935
  async function runGenerate(config, projectName, description, blocks, authFields, emit) {
876
936
  const blocksDesc = Object.entries(blocks)
877
937
  .filter(([, enabled]) => enabled)
@@ -1058,7 +1118,8 @@ Continue from here:`;
1058
1118
  }
1059
1119
  }
1060
1120
 
1061
- fileContent = rawOutput;
1121
+ // Deterministic repair of truncation artifacts (missing braces, tags)
1122
+ fileContent = repairTruncation(rawOutput, fileSpec.name);
1062
1123
  syntaxError = null;
1063
1124
 
1064
1125
  const fileTokensOut = countTokens(fileContent);
@@ -1109,10 +1170,27 @@ Continue from here:`;
1109
1170
  return isFileTruncated(f.content, f.name);
1110
1171
  });
1111
1172
 
1112
- if (brokenFiles.length > 0 && brokenFiles.length <= 10) {
1173
+ if (brokenFiles.length > 0) {
1113
1174
  emit({ type: 'phase', phase: 'autofix', msg: `Post-generation fix: ${brokenFiles.length} file(s) need repair...` });
1114
1175
  for (const broken of brokenFiles) {
1115
1176
  try {
1177
+ // Step 1: Try deterministic repair first (fast, no LLM call)
1178
+ const deterministicFix = repairTruncation(broken.content, broken.name);
1179
+ let isFixed = false;
1180
+ if (deterministicFix !== broken.content) {
1181
+ // Verify the fix actually resolved the issue
1182
+ if (!isFileTruncated(deterministicFix, broken.name)) {
1183
+ emit({ type: 'status', msg: `Auto-fixed ${broken.name} (deterministic repair)` });
1184
+ broken.content = deterministicFix;
1185
+ const abs = path.join(projectDir, broken.name);
1186
+ fs.writeFileSync(abs, deterministicFix, 'utf-8');
1187
+ isFixed = true;
1188
+ }
1189
+ }
1190
+ if (isFixed) continue;
1191
+
1192
+ // Step 2: If deterministic repair wasn't enough, regenerate with LLM
1193
+ if (brokenFiles.length > 15) continue; // don't LLM-regenerate too many files
1116
1194
  emit({ type: 'status', msg: `Regenerating ${broken.name}...` });
1117
1195
  let fixedContent = '';
1118
1196
  const fixPrompt = `Regenerate this file COMPLETELY. It was truncated or has errors.\n\nFile: ${broken.name}\nProject: ${projectName}\nDescription: ${description}\nFull file list: ${allFileNames}\n\nOutput the COMPLETE file content only, no explanation.`;
@@ -1121,7 +1199,8 @@ Continue from here:`;
1121
1199
  }, { max_tokens: 16384 });
1122
1200
  fixedContent = fixedContent
1123
1201
  .replace(/^```[\w]*\n/, '').replace(/\n```$/, '').trim();
1124
- if (fixedContent.length > broken.content.length) {
1202
+ fixedContent = repairTruncation(fixedContent, broken.name); // repair the regenerated content too
1203
+ if (fixedContent.length > 50) {
1125
1204
  broken.content = fixedContent;
1126
1205
  const abs = path.join(projectDir, broken.name);
1127
1206
  fs.writeFileSync(abs, fixedContent, 'utf-8');