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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "16.0.49",
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": {
@@ -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.49';
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 (Anthropic, OpenAI)
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
- if (emit) emit({ type: 'status', msg: `CSS coverage ${(analysis.coverage * 100).toFixed(0)}% (${analysis.missing.length} selectors missing). Auto-extending ${targetRel} via LLM (timeout 60s)...` });
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. Otherwise a slow/stuck Liara
4336
- // leaves the user staring at "Auto-extending..." forever with no feedback.
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: 8192 }),
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
- if (emit) emit({ type: 'warn', msg: `CSS extend failed: ${(e.message || e).slice(0, 200)}. Sandbox continues with current CSS — open chat to extend manually.` });
4359
- return { extended: false, reason: 'llm_failed', error: e.message, partialBytes: body.length };
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
- if (/e404|notarget|not found in the npm registry/.test(msg)) {
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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)) {
@@ -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
- const res = await fetch('https://api.openai.com/v1/chat/completions', {
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: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
1027
+ headers,
978
1028
  body: JSON.stringify(body),
979
1029
  });
980
- if (!res.ok) { const err = await res.text(); throw new Error(`OpenAI ${res.status}: ${err}`); }
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
  }