nothumanallowed 16.0.49 → 16.0.51
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/commands/ask.mjs +2 -2
- package/src/constants.mjs +1 -1
- package/src/server/routes/webcraft.mjs +108 -11
- package/src/services/llm.mjs +54 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.51",
|
|
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/commands/ask.mjs
CHANGED
|
@@ -182,11 +182,11 @@ export async function cmdAsk(args) {
|
|
|
182
182
|
const callFn = getProviderCall(provider);
|
|
183
183
|
if (!callFn) {
|
|
184
184
|
fail(`Unknown provider: ${provider}`);
|
|
185
|
-
info('Supported: anthropic, openai, gemini, deepseek, grok, mistral, cohere');
|
|
185
|
+
info('Supported: anthropic, openai, gemini, deepseek, grok, mistral, cohere, openrouter');
|
|
186
186
|
process.exit(1);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
const useStream = stream && (provider === 'anthropic' || provider === 'openai' || provider === 'deepseek' || provider === 'grok' || provider === 'mistral');
|
|
189
|
+
const useStream = stream && (provider === 'anthropic' || provider === 'openai' || provider === 'deepseek' || provider === 'grok' || provider === 'mistral' || provider === 'openrouter');
|
|
190
190
|
const result = await callFn(apiKey, model, systemPrompt, userMessage, useStream);
|
|
191
191
|
|
|
192
192
|
if (!useStream && result) {
|
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.51';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -1373,9 +1373,11 @@ RULES:
|
|
|
1373
1373
|
return `Error: unknown tool ${toolName}`;
|
|
1374
1374
|
}
|
|
1375
1375
|
|
|
1376
|
-
// Try native tool calling first
|
|
1376
|
+
// Try native tool calling first. Providers that support OpenAI-style native
|
|
1377
|
+
// tool_use: Anthropic, OpenAI, and OpenRouter (which proxies to Claude/GPT/etc.
|
|
1378
|
+
// with the same OpenAI-compatible schema).
|
|
1377
1379
|
const provider = config.llm?.provider || 'anthropic';
|
|
1378
|
-
const useNativeTools = provider === 'anthropic' || provider === 'openai';
|
|
1380
|
+
const useNativeTools = provider === 'anthropic' || provider === 'openai' || provider === 'openrouter';
|
|
1379
1381
|
|
|
1380
1382
|
if (useNativeTools) {
|
|
1381
1383
|
const systemPrompt = buildSystemPrompt();
|
|
@@ -4330,19 +4332,30 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
|
|
|
4330
4332
|
`- Hover/focus/transition states for buttons, links, cards\n` +
|
|
4331
4333
|
`- Color contrast must pass WCAG AA: text-on-bg >= 4.5:1`;
|
|
4332
4334
|
|
|
4333
|
-
|
|
4335
|
+
const provider = config?.llm?.provider || 'unknown';
|
|
4336
|
+
const model = config?.llm?.model || config?.llm?.[provider]?.model || 'default';
|
|
4337
|
+
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)...` });
|
|
4334
4338
|
|
|
4335
|
-
// Call LLM with timeout + progress heartbeat
|
|
4336
|
-
//
|
|
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.
|
|
4337
4344
|
let body = '';
|
|
4338
4345
|
let lastChunkAt = Date.now();
|
|
4339
4346
|
const startedAt = Date.now();
|
|
4340
4347
|
const timeoutMs = 60_000;
|
|
4348
|
+
let earlyWarningEmitted = false;
|
|
4341
4349
|
const heartbeatInterval = setInterval(() => {
|
|
4342
4350
|
if (!emit) return;
|
|
4343
4351
|
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(0);
|
|
4344
4352
|
const sinceLast = ((Date.now() - lastChunkAt) / 1000).toFixed(0);
|
|
4345
4353
|
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
|
+
if (!earlyWarningEmitted && body.length === 0 && Date.now() - startedAt > 15_000) {
|
|
4356
|
+
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.` });
|
|
4358
|
+
}
|
|
4346
4359
|
}, 5_000);
|
|
4347
4360
|
|
|
4348
4361
|
try {
|
|
@@ -4350,13 +4363,19 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
|
|
|
4350
4363
|
callLLMStream(config, sys, user, (chunk) => {
|
|
4351
4364
|
body += chunk;
|
|
4352
4365
|
lastChunkAt = Date.now();
|
|
4353
|
-
}, { max_tokens:
|
|
4354
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('LLM timeout after ' + (timeoutMs / 1000) + 's')), timeoutMs)),
|
|
4366
|
+
}, { max_tokens: 4096 }),
|
|
4367
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('LLM timeout after ' + (timeoutMs / 1000) + 's — provider ' + provider + ' did not respond')), timeoutMs)),
|
|
4355
4368
|
]);
|
|
4356
4369
|
} catch (e) {
|
|
4357
4370
|
clearInterval(heartbeatInterval);
|
|
4358
|
-
|
|
4359
|
-
|
|
4371
|
+
const errMsg = (e.message || String(e)).slice(0, 200);
|
|
4372
|
+
if (emit) {
|
|
4373
|
+
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.` });
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
return { extended: false, reason: 'llm_failed', error: errMsg, partialBytes: body.length };
|
|
4360
4379
|
}
|
|
4361
4380
|
clearInterval(heartbeatInterval);
|
|
4362
4381
|
|
|
@@ -5038,6 +5057,7 @@ export const _SHIMMED_MODULES = new Set([
|
|
|
5038
5057
|
'dotenv', 'cors', 'morgan', 'body-parser', 'cookie-parser',
|
|
5039
5058
|
'compression', 'express-rate-limit', 'jsonwebtoken', 'bcryptjs', 'bcrypt',
|
|
5040
5059
|
'uuid', 'lodash', 'debug', 'chalk', 'multer', 'axios', 'express',
|
|
5060
|
+
'marked', 'markdown-it',
|
|
5041
5061
|
]);
|
|
5042
5062
|
|
|
5043
5063
|
/** Read declared dependencies from package.json (deps + devDeps + peer). */
|
|
@@ -5165,9 +5185,16 @@ export function _classifyInstallError(err) {
|
|
|
5165
5185
|
return { reason: 'offline (npm registry unreachable)', offlineFallback: true,
|
|
5166
5186
|
hint: 'Check VM network bridge/NAT, DNS, or corporate proxy (HTTP_PROXY/HTTPS_PROXY). Activating shim fallback.' };
|
|
5167
5187
|
}
|
|
5168
|
-
|
|
5188
|
+
// Distinguish "true E404" (package doesn't exist) from "offline cache miss"
|
|
5189
|
+
// (--prefer-offline can produce 404-looking errors when registry is unreachable).
|
|
5190
|
+
// True E404 also mentions "not in registry"; cache miss mentions "not in cache" or "ENETUNREACH".
|
|
5191
|
+
if (/not in.*(cache|local)|ENETUNREACH|prefer.?offline.*not.*found/i.test(msg)) {
|
|
5192
|
+
return { reason: 'offline cache miss (registry not reached, package not cached)', offlineFallback: true,
|
|
5193
|
+
hint: 'npm --prefer-offline could not find the package in local cache and registry is unreachable. Activating shim fallback.' };
|
|
5194
|
+
}
|
|
5195
|
+
if (/e404|"npm error code e404"|notarget|404 not found|not found in the npm registry|package.*does not exist/i.test(msg)) {
|
|
5169
5196
|
return { reason: 'package does not exist on npm', offlineFallback: false,
|
|
5170
|
-
hint: 'The package name from the LLM-generated code is likely a hallucination. Tier 2 LLM-rewrite will rename it.' };
|
|
5197
|
+
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.' };
|
|
5171
5198
|
}
|
|
5172
5199
|
if (/eacces|eperm|permission denied/.test(msg)) {
|
|
5173
5200
|
return { reason: 'permissions denied', offlineFallback: false,
|
|
@@ -5843,6 +5870,74 @@ express.static = function (root, opts) {
|
|
|
5843
5870
|
express.Router = function () { const r = createApp(); return r; };
|
|
5844
5871
|
module.exports = express;
|
|
5845
5872
|
module.exports.default = express;
|
|
5873
|
+
`;
|
|
5874
|
+
|
|
5875
|
+
// marked — minimal Markdown → HTML parser. Real `marked` is 200KB+ but we
|
|
5876
|
+
// only need basic parsing. Covers: headings, bold/italic, links, code blocks,
|
|
5877
|
+
// lists, paragraphs, line breaks. Good enough for blog-style content.
|
|
5878
|
+
const markedShim = `
|
|
5879
|
+
function escapeHtml(s) {
|
|
5880
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
5881
|
+
}
|
|
5882
|
+
function parseMarkdown(md) {
|
|
5883
|
+
if (!md) return '';
|
|
5884
|
+
let html = String(md);
|
|
5885
|
+
// Code blocks (must come first)
|
|
5886
|
+
html = html.replace(/\\\`\\\`\\\`(\\\\w+)?\\n([\\s\\S]*?)\\n\\\`\\\`\\\`/g, (_, lang, code) => '<pre><code' + (lang ? ' class="language-' + lang + '"' : '') + '>' + escapeHtml(code) + '</code></pre>');
|
|
5887
|
+
// Inline code
|
|
5888
|
+
html = html.replace(/\\\`([^\\\`\\n]+)\\\`/g, '<code>$1</code>');
|
|
5889
|
+
// Headings
|
|
5890
|
+
html = html.replace(/^######\\s+(.+)$/gm, '<h6>$1</h6>');
|
|
5891
|
+
html = html.replace(/^#####\\s+(.+)$/gm, '<h5>$1</h5>');
|
|
5892
|
+
html = html.replace(/^####\\s+(.+)$/gm, '<h4>$1</h4>');
|
|
5893
|
+
html = html.replace(/^###\\s+(.+)$/gm, '<h3>$1</h3>');
|
|
5894
|
+
html = html.replace(/^##\\s+(.+)$/gm, '<h2>$1</h2>');
|
|
5895
|
+
html = html.replace(/^#\\s+(.+)$/gm, '<h1>$1</h1>');
|
|
5896
|
+
// Bold + italic
|
|
5897
|
+
html = html.replace(/\\*\\*\\*([^*]+)\\*\\*\\*/g, '<strong><em>$1</em></strong>');
|
|
5898
|
+
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
5899
|
+
html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
5900
|
+
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
5901
|
+
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
5902
|
+
// Links + images
|
|
5903
|
+
html = html.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src="$2" alt="$1">');
|
|
5904
|
+
html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
|
|
5905
|
+
// Blockquotes
|
|
5906
|
+
html = html.replace(/^>\\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
|
5907
|
+
// Horizontal rules
|
|
5908
|
+
html = html.replace(/^---+$/gm, '<hr>');
|
|
5909
|
+
// Lists (simple)
|
|
5910
|
+
html = html.replace(/^[\\*\\-]\\s+(.+)$/gm, '<li>$1</li>');
|
|
5911
|
+
html = html.replace(/(<li>[\\s\\S]*?<\\/li>\\n?)+/g, (m) => '<ul>' + m.replace(/\\n/g, '') + '</ul>');
|
|
5912
|
+
html = html.replace(/^\\d+\\.\\s+(.+)$/gm, '<li>$1</li>');
|
|
5913
|
+
// Paragraphs (lines not in other elements)
|
|
5914
|
+
const lines = html.split(/\\n\\n+/);
|
|
5915
|
+
html = lines.map(line => {
|
|
5916
|
+
line = line.trim();
|
|
5917
|
+
if (!line) return '';
|
|
5918
|
+
if (/^<(h[1-6]|ul|ol|pre|blockquote|hr|p|div)/.test(line)) return line;
|
|
5919
|
+
return '<p>' + line + '</p>';
|
|
5920
|
+
}).join('\\n');
|
|
5921
|
+
return html;
|
|
5922
|
+
}
|
|
5923
|
+
function marked(md, opts) {
|
|
5924
|
+
return parseMarkdown(md);
|
|
5925
|
+
}
|
|
5926
|
+
marked.parse = parseMarkdown;
|
|
5927
|
+
marked.setOptions = function () { return marked; };
|
|
5928
|
+
marked.use = function () { return marked; };
|
|
5929
|
+
marked.Renderer = function () {};
|
|
5930
|
+
marked.parseInline = function (md) {
|
|
5931
|
+
let html = String(md || '');
|
|
5932
|
+
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
5933
|
+
html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
5934
|
+
html = html.replace(/\\\`([^\\\`]+)\\\`/g, '<code>$1</code>');
|
|
5935
|
+
html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
|
|
5936
|
+
return html;
|
|
5937
|
+
};
|
|
5938
|
+
module.exports = marked;
|
|
5939
|
+
module.exports.marked = marked;
|
|
5940
|
+
module.exports.default = marked;
|
|
5846
5941
|
`;
|
|
5847
5942
|
|
|
5848
5943
|
// axios — minimal fetch-based replacement
|
|
@@ -5920,6 +6015,8 @@ module.exports.default = proxy;
|
|
|
5920
6015
|
'multer.js': multerShim,
|
|
5921
6016
|
'axios.js': axiosShim,
|
|
5922
6017
|
'express.js': expressShim,
|
|
6018
|
+
'marked.js': markedShim,
|
|
6019
|
+
'markdown-it.js': markedShim,
|
|
5923
6020
|
'noop.js': noopShim,
|
|
5924
6021
|
};
|
|
5925
6022
|
for (const [name, content] of Object.entries(shimFiles)) {
|
package/src/services/llm.mjs
CHANGED
|
@@ -563,6 +563,44 @@ export async function callGrok(apiKey, model, systemPrompt, userMessage, stream
|
|
|
563
563
|
return data.choices?.[0]?.message?.content || '';
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
/**
|
|
567
|
+
* OpenRouter — aggregator that exposes 100+ models (Claude, GPT, Gemini,
|
|
568
|
+
* Mistral, Llama, Qwen, DeepSeek, etc.) via a single OpenAI-compatible API.
|
|
569
|
+
* Endpoint: https://openrouter.ai/api/v1/chat/completions
|
|
570
|
+
* Model names: "anthropic/claude-sonnet-4.5", "openai/gpt-4o", "google/gemini-2.5-pro"...
|
|
571
|
+
*/
|
|
572
|
+
export async function callOpenRouter(apiKey, model, systemPrompt, userMessage, stream = false, opts = {}) {
|
|
573
|
+
const body = {
|
|
574
|
+
model: model || 'anthropic/claude-sonnet-4.5',
|
|
575
|
+
max_tokens: opts.max_tokens || 8192,
|
|
576
|
+
messages: [
|
|
577
|
+
{ role: 'system', content: systemPrompt },
|
|
578
|
+
..._openaiHistory(opts),
|
|
579
|
+
{ role: 'user', content: userMessage },
|
|
580
|
+
],
|
|
581
|
+
stream,
|
|
582
|
+
};
|
|
583
|
+
if (opts.temperature !== undefined) body.temperature = opts.temperature;
|
|
584
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers: {
|
|
587
|
+
'Content-Type': 'application/json',
|
|
588
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
589
|
+
// OpenRouter recommends these for proper attribution + ranking
|
|
590
|
+
'HTTP-Referer': 'https://nothumanallowed.com',
|
|
591
|
+
'X-Title': 'NotHumanAllowed CLI',
|
|
592
|
+
},
|
|
593
|
+
body: JSON.stringify(body),
|
|
594
|
+
});
|
|
595
|
+
if (!res.ok) {
|
|
596
|
+
const err = await res.text();
|
|
597
|
+
throw new Error(`OpenRouter ${res.status}: ${err}`);
|
|
598
|
+
}
|
|
599
|
+
if (stream) return streamSSE(res, 'openai');
|
|
600
|
+
const data = await res.json();
|
|
601
|
+
return data.choices?.[0]?.message?.content || '';
|
|
602
|
+
}
|
|
603
|
+
|
|
566
604
|
export async function callMistral(apiKey, model, systemPrompt, userMessage, stream = false, opts = {}) {
|
|
567
605
|
const body = {
|
|
568
606
|
model: model || 'mistral-large-latest',
|
|
@@ -753,6 +791,7 @@ const PROVIDERS = {
|
|
|
753
791
|
grok: callGrok,
|
|
754
792
|
mistral: callMistral,
|
|
755
793
|
cohere: callCohere,
|
|
794
|
+
openrouter: callOpenRouter,
|
|
756
795
|
};
|
|
757
796
|
|
|
758
797
|
export function getProviderCall(provider) {
|
|
@@ -788,6 +827,12 @@ export async function callLLMWithTools(config, systemPrompt, messages, tools, on
|
|
|
788
827
|
return _callOpenAIWithTools(apiKey, model, systemPrompt, messages, tools, onText, onToolCall, opts);
|
|
789
828
|
}
|
|
790
829
|
|
|
830
|
+
// OpenRouter — uses OpenAI-compatible function calling schema. We reuse the
|
|
831
|
+
// OpenAI tool-call implementation but point to OpenRouter's endpoint.
|
|
832
|
+
if (provider === 'openrouter') {
|
|
833
|
+
return _callOpenAIWithTools(apiKey, model || 'anthropic/claude-sonnet-4.5', systemPrompt, messages, tools, onText, onToolCall, { ...opts, baseUrl: 'https://openrouter.ai/api/v1', referer: 'https://nothumanallowed.com', xTitle: 'NotHumanAllowed CLI' });
|
|
834
|
+
}
|
|
835
|
+
|
|
791
836
|
// Other providers: fallback to text-based tool calling (old system)
|
|
792
837
|
// The caller should handle this by checking the return value
|
|
793
838
|
return null;
|
|
@@ -972,12 +1017,17 @@ async function _callOpenAIWithTools(apiKey, model, systemPrompt, messages, tools
|
|
|
972
1017
|
stream: true,
|
|
973
1018
|
};
|
|
974
1019
|
|
|
975
|
-
|
|
1020
|
+
// Support baseUrl override so OpenRouter (OpenAI-compatible) can reuse this loop.
|
|
1021
|
+
const endpoint = (opts.baseUrl || 'https://api.openai.com/v1') + '/chat/completions';
|
|
1022
|
+
const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` };
|
|
1023
|
+
if (opts.referer) headers['HTTP-Referer'] = opts.referer;
|
|
1024
|
+
if (opts.xTitle) headers['X-Title'] = opts.xTitle;
|
|
1025
|
+
const res = await fetch(endpoint, {
|
|
976
1026
|
method: 'POST',
|
|
977
|
-
headers
|
|
1027
|
+
headers,
|
|
978
1028
|
body: JSON.stringify(body),
|
|
979
1029
|
});
|
|
980
|
-
if (!res.ok) { const err = await res.text(); throw new Error(
|
|
1030
|
+
if (!res.ok) { const err = await res.text(); throw new Error(`${opts.baseUrl ? 'OpenRouter' : 'OpenAI'} ${res.status}: ${err}`); }
|
|
981
1031
|
|
|
982
1032
|
const reader = res.body.getReader();
|
|
983
1033
|
const dec = new TextDecoder();
|
|
@@ -1053,6 +1103,7 @@ export function getApiKey(config, provider) {
|
|
|
1053
1103
|
grok: config.llm.grokKey || config.llm.apiKey,
|
|
1054
1104
|
mistral: config.llm.mistralKey || config.llm.apiKey,
|
|
1055
1105
|
cohere: config.llm.cohereKey || config.llm.apiKey,
|
|
1106
|
+
openrouter: config.llm.openrouterKey || config.llm.openrouter_key || config.llm.apiKey,
|
|
1056
1107
|
};
|
|
1057
1108
|
return keyMap[provider] || config.llm.apiKey;
|
|
1058
1109
|
}
|