nothumanallowed 16.0.48 → 16.0.50
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 +127 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.50",
|
|
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.50';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -1286,7 +1286,9 @@ RULES:
|
|
|
1286
1286
|
return 'OK — edit applied (fuzzy match)';
|
|
1287
1287
|
}
|
|
1288
1288
|
emit({ type: 'tool', op: 'edit', path: relPath, result: 'old_not_found' });
|
|
1289
|
-
|
|
1289
|
+
// Include the actual file content in the error response so the LLM can
|
|
1290
|
+
// produce a correct old_text on retry without needing a separate read_file.
|
|
1291
|
+
return `Error: old_text not found in file.\n\nCURRENT CONTENT OF ${relPath}:\n\`\`\`\n${src.slice(0, 16000)}\n\`\`\`\n\nPick the EXACT lines you want to replace from above, copy them as old_text, and retry edit_file.`;
|
|
1290
1292
|
}
|
|
1291
1293
|
|
|
1292
1294
|
if (toolName === 'create_file') {
|
|
@@ -1562,8 +1564,18 @@ After ALL fixes are done, emit <done/> on its own line.
|
|
|
1562
1564
|
toolResults.push({ op: 'edit', path: relPath, result: 'ok' });
|
|
1563
1565
|
emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 2000), newSnippet: newStr?.slice(0, 2000) });
|
|
1564
1566
|
} else {
|
|
1565
|
-
// No match —
|
|
1566
|
-
|
|
1567
|
+
// No match — auto-read the file and INCLUDE its content in the
|
|
1568
|
+
// feedback. Without this, the LLM at the next step is blind and
|
|
1569
|
+
// can't correct the old_text. Critical fix for text-based mode
|
|
1570
|
+
// where the LLM doesn't see tool results between calls.
|
|
1571
|
+
const fileContentForRetry = src.slice(0, 16000);
|
|
1572
|
+
toolResults.push({
|
|
1573
|
+
op: 'edit',
|
|
1574
|
+
path: relPath,
|
|
1575
|
+
result: 'old_not_found',
|
|
1576
|
+
hint: 'Your old_text did NOT match the file. The CURRENT file content is included below — copy the EXACT lines you want to replace and retry edit with that exact text as old.',
|
|
1577
|
+
content: fileContentForRetry,
|
|
1578
|
+
});
|
|
1567
1579
|
emit({ type: 'tool', op: 'edit', path: relPath, result: 'old_not_found', oldSnippet: oldStr.slice(0, 200) });
|
|
1568
1580
|
}
|
|
1569
1581
|
}
|
|
@@ -1825,11 +1837,16 @@ After ALL fixes are done, emit <done/> on its own line.
|
|
|
1825
1837
|
break;
|
|
1826
1838
|
}
|
|
1827
1839
|
|
|
1828
|
-
// Build tool results feedback for next iteration
|
|
1840
|
+
// Build tool results feedback for next iteration.
|
|
1841
|
+
// CRITICAL: when edit fails with old_not_found, include the CURRENT
|
|
1842
|
+
// file content so the LLM can produce a correct old_text on retry.
|
|
1843
|
+
// Without this, text-based mode loops forever on the same wrong old_text.
|
|
1829
1844
|
const feedbackParts = toolResults.map((r) => {
|
|
1830
1845
|
let msg = `[${r.op}] ${r.path || ''}: ${r.result}`;
|
|
1831
|
-
if (r.
|
|
1832
|
-
if (r.
|
|
1846
|
+
if (r.hint) msg += `\n HINT: ${r.hint}`;
|
|
1847
|
+
if (r.content) {
|
|
1848
|
+
msg += `\n\nCURRENT CONTENT OF ${r.path}:\n\`\`\`\n${r.content}\n\`\`\`\n\nTo fix: pick the exact lines you want to replace from above, use them as "old", and retry the edit.`;
|
|
1849
|
+
}
|
|
1833
1850
|
return msg;
|
|
1834
1851
|
});
|
|
1835
1852
|
|
|
@@ -4313,19 +4330,30 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
|
|
|
4313
4330
|
`- Hover/focus/transition states for buttons, links, cards\n` +
|
|
4314
4331
|
`- Color contrast must pass WCAG AA: text-on-bg >= 4.5:1`;
|
|
4315
4332
|
|
|
4316
|
-
|
|
4333
|
+
const provider = config?.llm?.provider || 'unknown';
|
|
4334
|
+
const model = config?.llm?.model || config?.llm?.[provider]?.model || 'default';
|
|
4335
|
+
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)...` });
|
|
4317
4336
|
|
|
4318
|
-
// Call LLM with timeout + progress heartbeat
|
|
4319
|
-
//
|
|
4337
|
+
// Call LLM with timeout + progress heartbeat + early warning when no bytes arrive.
|
|
4338
|
+
// Different providers have different latencies: Claude is fast (~5-15s),
|
|
4339
|
+
// OpenAI similar, Liara/Qwen3 can be 20-40s under load, Gemini sometimes
|
|
4340
|
+
// stuck on long prompts. Adapt max_tokens to 4096 (down from 8192) to keep
|
|
4341
|
+
// Liara responsive.
|
|
4320
4342
|
let body = '';
|
|
4321
4343
|
let lastChunkAt = Date.now();
|
|
4322
4344
|
const startedAt = Date.now();
|
|
4323
4345
|
const timeoutMs = 60_000;
|
|
4346
|
+
let earlyWarningEmitted = false;
|
|
4324
4347
|
const heartbeatInterval = setInterval(() => {
|
|
4325
4348
|
if (!emit) return;
|
|
4326
4349
|
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(0);
|
|
4327
4350
|
const sinceLast = ((Date.now() - lastChunkAt) / 1000).toFixed(0);
|
|
4328
4351
|
emit({ type: 'status', msg: `LLM extend: ${elapsed}s elapsed, ${body.length} bytes received (${sinceLast}s since last chunk)` });
|
|
4352
|
+
// Early warning if no bytes at all after 15s — provider is likely stuck or rate-limited
|
|
4353
|
+
if (!earlyWarningEmitted && body.length === 0 && Date.now() - startedAt > 15_000) {
|
|
4354
|
+
earlyWarningEmitted = true;
|
|
4355
|
+
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.` });
|
|
4356
|
+
}
|
|
4329
4357
|
}, 5_000);
|
|
4330
4358
|
|
|
4331
4359
|
try {
|
|
@@ -4333,13 +4361,19 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
|
|
|
4333
4361
|
callLLMStream(config, sys, user, (chunk) => {
|
|
4334
4362
|
body += chunk;
|
|
4335
4363
|
lastChunkAt = Date.now();
|
|
4336
|
-
}, { max_tokens:
|
|
4337
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('LLM timeout after ' + (timeoutMs / 1000) + 's')), timeoutMs)),
|
|
4364
|
+
}, { max_tokens: 4096 }),
|
|
4365
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('LLM timeout after ' + (timeoutMs / 1000) + 's — provider ' + provider + ' did not respond')), timeoutMs)),
|
|
4338
4366
|
]);
|
|
4339
4367
|
} catch (e) {
|
|
4340
4368
|
clearInterval(heartbeatInterval);
|
|
4341
|
-
|
|
4342
|
-
|
|
4369
|
+
const errMsg = (e.message || String(e)).slice(0, 200);
|
|
4370
|
+
if (emit) {
|
|
4371
|
+
emit({ type: 'warn', msg: `CSS extend failed: ${errMsg}` });
|
|
4372
|
+
if (errMsg.includes('timeout') || body.length === 0) {
|
|
4373
|
+
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.` });
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
return { extended: false, reason: 'llm_failed', error: errMsg, partialBytes: body.length };
|
|
4343
4377
|
}
|
|
4344
4378
|
clearInterval(heartbeatInterval);
|
|
4345
4379
|
|
|
@@ -5021,6 +5055,7 @@ export const _SHIMMED_MODULES = new Set([
|
|
|
5021
5055
|
'dotenv', 'cors', 'morgan', 'body-parser', 'cookie-parser',
|
|
5022
5056
|
'compression', 'express-rate-limit', 'jsonwebtoken', 'bcryptjs', 'bcrypt',
|
|
5023
5057
|
'uuid', 'lodash', 'debug', 'chalk', 'multer', 'axios', 'express',
|
|
5058
|
+
'marked', 'markdown-it',
|
|
5024
5059
|
]);
|
|
5025
5060
|
|
|
5026
5061
|
/** Read declared dependencies from package.json (deps + devDeps + peer). */
|
|
@@ -5148,9 +5183,16 @@ export function _classifyInstallError(err) {
|
|
|
5148
5183
|
return { reason: 'offline (npm registry unreachable)', offlineFallback: true,
|
|
5149
5184
|
hint: 'Check VM network bridge/NAT, DNS, or corporate proxy (HTTP_PROXY/HTTPS_PROXY). Activating shim fallback.' };
|
|
5150
5185
|
}
|
|
5151
|
-
|
|
5186
|
+
// Distinguish "true E404" (package doesn't exist) from "offline cache miss"
|
|
5187
|
+
// (--prefer-offline can produce 404-looking errors when registry is unreachable).
|
|
5188
|
+
// True E404 also mentions "not in registry"; cache miss mentions "not in cache" or "ENETUNREACH".
|
|
5189
|
+
if (/not in.*(cache|local)|ENETUNREACH|prefer.?offline.*not.*found/i.test(msg)) {
|
|
5190
|
+
return { reason: 'offline cache miss (registry not reached, package not cached)', offlineFallback: true,
|
|
5191
|
+
hint: 'npm --prefer-offline could not find the package in local cache and registry is unreachable. Activating shim fallback.' };
|
|
5192
|
+
}
|
|
5193
|
+
if (/e404|"npm error code e404"|notarget|404 not found|not found in the npm registry|package.*does not exist/i.test(msg)) {
|
|
5152
5194
|
return { reason: 'package does not exist on npm', offlineFallback: false,
|
|
5153
|
-
hint: 'The package name from the LLM-generated code is likely a hallucination. Tier 2 LLM-rewrite will rename it.' };
|
|
5195
|
+
hint: 'The package name from the LLM-generated code is likely a hallucination, or you are offline. Tier 2 LLM-rewrite will rename it, or shim layer will take over if available.' };
|
|
5154
5196
|
}
|
|
5155
5197
|
if (/eacces|eperm|permission denied/.test(msg)) {
|
|
5156
5198
|
return { reason: 'permissions denied', offlineFallback: false,
|
|
@@ -5826,6 +5868,74 @@ express.static = function (root, opts) {
|
|
|
5826
5868
|
express.Router = function () { const r = createApp(); return r; };
|
|
5827
5869
|
module.exports = express;
|
|
5828
5870
|
module.exports.default = express;
|
|
5871
|
+
`;
|
|
5872
|
+
|
|
5873
|
+
// marked — minimal Markdown → HTML parser. Real `marked` is 200KB+ but we
|
|
5874
|
+
// only need basic parsing. Covers: headings, bold/italic, links, code blocks,
|
|
5875
|
+
// lists, paragraphs, line breaks. Good enough for blog-style content.
|
|
5876
|
+
const markedShim = `
|
|
5877
|
+
function escapeHtml(s) {
|
|
5878
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
5879
|
+
}
|
|
5880
|
+
function parseMarkdown(md) {
|
|
5881
|
+
if (!md) return '';
|
|
5882
|
+
let html = String(md);
|
|
5883
|
+
// Code blocks (must come first)
|
|
5884
|
+
html = html.replace(/\\\`\\\`\\\`(\\\\w+)?\\n([\\s\\S]*?)\\n\\\`\\\`\\\`/g, (_, lang, code) => '<pre><code' + (lang ? ' class="language-' + lang + '"' : '') + '>' + escapeHtml(code) + '</code></pre>');
|
|
5885
|
+
// Inline code
|
|
5886
|
+
html = html.replace(/\\\`([^\\\`\\n]+)\\\`/g, '<code>$1</code>');
|
|
5887
|
+
// Headings
|
|
5888
|
+
html = html.replace(/^######\\s+(.+)$/gm, '<h6>$1</h6>');
|
|
5889
|
+
html = html.replace(/^#####\\s+(.+)$/gm, '<h5>$1</h5>');
|
|
5890
|
+
html = html.replace(/^####\\s+(.+)$/gm, '<h4>$1</h4>');
|
|
5891
|
+
html = html.replace(/^###\\s+(.+)$/gm, '<h3>$1</h3>');
|
|
5892
|
+
html = html.replace(/^##\\s+(.+)$/gm, '<h2>$1</h2>');
|
|
5893
|
+
html = html.replace(/^#\\s+(.+)$/gm, '<h1>$1</h1>');
|
|
5894
|
+
// Bold + italic
|
|
5895
|
+
html = html.replace(/\\*\\*\\*([^*]+)\\*\\*\\*/g, '<strong><em>$1</em></strong>');
|
|
5896
|
+
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
5897
|
+
html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
5898
|
+
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
5899
|
+
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
5900
|
+
// Links + images
|
|
5901
|
+
html = html.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src="$2" alt="$1">');
|
|
5902
|
+
html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
|
|
5903
|
+
// Blockquotes
|
|
5904
|
+
html = html.replace(/^>\\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
|
5905
|
+
// Horizontal rules
|
|
5906
|
+
html = html.replace(/^---+$/gm, '<hr>');
|
|
5907
|
+
// Lists (simple)
|
|
5908
|
+
html = html.replace(/^[\\*\\-]\\s+(.+)$/gm, '<li>$1</li>');
|
|
5909
|
+
html = html.replace(/(<li>[\\s\\S]*?<\\/li>\\n?)+/g, (m) => '<ul>' + m.replace(/\\n/g, '') + '</ul>');
|
|
5910
|
+
html = html.replace(/^\\d+\\.\\s+(.+)$/gm, '<li>$1</li>');
|
|
5911
|
+
// Paragraphs (lines not in other elements)
|
|
5912
|
+
const lines = html.split(/\\n\\n+/);
|
|
5913
|
+
html = lines.map(line => {
|
|
5914
|
+
line = line.trim();
|
|
5915
|
+
if (!line) return '';
|
|
5916
|
+
if (/^<(h[1-6]|ul|ol|pre|blockquote|hr|p|div)/.test(line)) return line;
|
|
5917
|
+
return '<p>' + line + '</p>';
|
|
5918
|
+
}).join('\\n');
|
|
5919
|
+
return html;
|
|
5920
|
+
}
|
|
5921
|
+
function marked(md, opts) {
|
|
5922
|
+
return parseMarkdown(md);
|
|
5923
|
+
}
|
|
5924
|
+
marked.parse = parseMarkdown;
|
|
5925
|
+
marked.setOptions = function () { return marked; };
|
|
5926
|
+
marked.use = function () { return marked; };
|
|
5927
|
+
marked.Renderer = function () {};
|
|
5928
|
+
marked.parseInline = function (md) {
|
|
5929
|
+
let html = String(md || '');
|
|
5930
|
+
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
5931
|
+
html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
5932
|
+
html = html.replace(/\\\`([^\\\`]+)\\\`/g, '<code>$1</code>');
|
|
5933
|
+
html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
|
|
5934
|
+
return html;
|
|
5935
|
+
};
|
|
5936
|
+
module.exports = marked;
|
|
5937
|
+
module.exports.marked = marked;
|
|
5938
|
+
module.exports.default = marked;
|
|
5829
5939
|
`;
|
|
5830
5940
|
|
|
5831
5941
|
// axios — minimal fetch-based replacement
|
|
@@ -5903,6 +6013,8 @@ module.exports.default = proxy;
|
|
|
5903
6013
|
'multer.js': multerShim,
|
|
5904
6014
|
'axios.js': axiosShim,
|
|
5905
6015
|
'express.js': expressShim,
|
|
6016
|
+
'marked.js': markedShim,
|
|
6017
|
+
'markdown-it.js': markedShim,
|
|
5906
6018
|
'noop.js': noopShim,
|
|
5907
6019
|
};
|
|
5908
6020
|
for (const [name, content] of Object.entries(shimFiles)) {
|