nothumanallowed 16.0.55 → 16.0.57

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.55",
3
+ "version": "16.0.57",
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.55';
8
+ export const VERSION = '16.0.57';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -4334,27 +4334,36 @@ export async function _autoExtendStylesIfNeeded(projectName, config, emit, opts)
4334
4334
  } catch {}
4335
4335
  }
4336
4336
 
4337
- const sys = `You are an expert frontend designer producing PRODUCTION-QUALITY CSS.
4337
+ // APPEND mode (16.0.57): output ONLY the new rules server appends to
4338
+ // existing file. Prevents monotonic regression (LLM truncating + losing
4339
+ // existing rules) and dramatically reduces output token cost.
4340
+ const sys = `You are an expert frontend designer. Generate ONLY new CSS rules to ADD to an existing stylesheet.
4338
4341
 
4339
- DESIGN REQUIREMENTS (NON-NEGOTIABLE):
4340
- - WCAG AA contrast: text-on-background ratio >= 4.5:1. NO washed-out pastels for text. Body text must be near-black on light bg, or near-white on dark bg.
4341
- - Use VIBRANT accent colors with sufficient saturation (HSL S >= 60%, L between 35-65% for accents).
4342
- - Cover EVERY selector listed including footer, header, nav, hero, sections, cards, buttons, forms, modals, tooltips.
4343
- - Use modern CSS: flex/grid layouts, custom properties for colors, smooth transitions (200-300ms), subtle shadows.
4344
-
4345
- OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no explanations, no preamble. The output will be written directly to disk.`;
4342
+ CRITICAL RULES:
4343
+ - Output ONLY the new CSS rules do NOT repeat any existing rules.
4344
+ - Do NOT output markdown fences, explanations, or comments about what you're doing.
4345
+ - The output will be APPENDED to an existing CSS file. Do not include @import, @charset, or any preamble.
4346
+ - Generate at least one rule for EVERY listed missing selector.
4347
+
4348
+ DESIGN REQUIREMENTS:
4349
+ - WCAG AA contrast: text-on-bg ratio >= 4.5:1. NO washed-out pastels for text.
4350
+ - Vibrant accent colors (HSL S >= 60%, L 35-65%).
4351
+ - Match the existing CSS's design language (look at the colors/spacing in the existing rules).
4352
+ - Include responsive breakpoints (768px, 480px) where layout matters.
4353
+ - Hover/focus/transition states for interactive elements.`;
4354
+ // Pick a sample of missing selectors that fits in token budget. We cap at
4355
+ // 200 to keep one pass reasonable; remaining are picked up by next pass.
4356
+ const passSelectors = analysis.missing.slice(0, 200);
4346
4357
  const user =
4347
- `Extend this CSS file: \`${targetRel}\`\n\n` +
4348
- `Current CSS (${target.size} bytes, ${analysis.cssRuleCount} rules):\n\`\`\`css\n${target.content.slice(0, 6000)}\n\`\`\`\n\n` +
4349
- `ALL missing selectors (${analysis.missing.length} cover every single one):\n${analysis.missing.join(', ')}\n\n` +
4350
- `HTML files for context:\n${htmlSample}\n\n` +
4351
- `Output the COMPLETE extended CSS file. Required additions:\n` +
4358
+ `Existing CSS file: \`${targetRel}\` (${target.size} bytes, ${analysis.cssRuleCount} rules).\n\n` +
4359
+ `Sample of existing rules (for design-language reference — DO NOT repeat in output):\n\`\`\`css\n${target.content.slice(0, 3000)}\n\`\`\`\n\n` +
4360
+ `HTML context (snippet, for layout reference):\n${htmlSample.slice(0, 2500)}\n\n` +
4361
+ `Generate NEW CSS rules that cover these ${passSelectors.length} currently-uncovered selectors (out of ${analysis.missing.length} total):\n${passSelectors.join(', ')}\n\n` +
4362
+ `Output ONLY the new rules. Required additions if not already in existing CSS:\n` +
4352
4363
  `- img { max-width: 100%; height: auto; object-fit: cover; display: block; }\n` +
4353
- `- Footer, header, nav with proper layout and visible styling (NOT transparent backgrounds with low-contrast text)\n` +
4354
- `- Rules for EVERY one of the ${analysis.missing.length} missing selectors above\n` +
4355
- `- Responsive breakpoints at 1024px, 768px, 480px (mobile-first)\n` +
4356
- `- Hover/focus/transition states for buttons, links, cards\n` +
4357
- `- Color contrast must pass WCAG AA: text-on-bg >= 4.5:1`;
4364
+ `- footer { padding: 32px 16px; background: ...; color: ...; } (or similar with visible contrast)\n` +
4365
+ `- Responsive @media queries at 768px and 480px for grid/flex sections.\n` +
4366
+ `Begin your output directly with the first CSS rule. No preamble.`;
4358
4367
 
4359
4368
  const provider = config?.llm?.provider || 'unknown';
4360
4369
  const model = config?.llm?.model || config?.llm?.[provider]?.model || 'default';
@@ -4374,23 +4383,30 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
4374
4383
  let aborted = false;
4375
4384
  const abortController = new AbortController();
4376
4385
 
4386
+ // Track byte velocity for an informative heartbeat: show b/s instead of
4387
+ // misleading "0s since last chunk" (which is almost always 0 when streaming).
4388
+ let prevBytes = 0;
4389
+ let prevHeartbeatAt = startedAt;
4377
4390
  const heartbeatInterval = setInterval(() => {
4378
4391
  if (!emit) return;
4379
- const elapsed = ((Date.now() - startedAt) / 1000).toFixed(0);
4380
- const sinceLast = ((Date.now() - lastChunkAt) / 1000).toFixed(0);
4381
- emit({ type: 'status', msg: `LLM extend: ${elapsed}s elapsed, ${body.length} bytes received (${sinceLast}s since last chunk)` });
4382
- if (!earlyWarningEmitted && body.length === 0 && Date.now() - startedAt > 15_000) {
4392
+ const now = Date.now();
4393
+ const elapsed = ((now - startedAt) / 1000).toFixed(0);
4394
+ const bytesPerSec = Math.round((body.length - prevBytes) / Math.max(1, (now - prevHeartbeatAt) / 1000));
4395
+ prevBytes = body.length;
4396
+ prevHeartbeatAt = now;
4397
+ emit({ type: 'status', msg: `LLM extend: ${elapsed}s elapsed, ${body.length} bytes received (${bytesPerSec} b/s)` });
4398
+ if (!earlyWarningEmitted && body.length === 0 && now - startedAt > 15_000) {
4383
4399
  earlyWarningEmitted = true;
4384
4400
  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.` });
4385
4401
  }
4386
4402
  // No-progress timeout: only if STUCK (zero chunks for 30s)
4387
- if (Date.now() - lastChunkAt > noProgressTimeoutMs && body.length > 0) {
4403
+ if (now - lastChunkAt > noProgressTimeoutMs && body.length > 0) {
4388
4404
  timedOut = true;
4389
4405
  aborted = true;
4390
4406
  abortController.abort();
4391
4407
  }
4392
4408
  // Absolute timeout
4393
- if (Date.now() - startedAt > absoluteTimeoutMs) {
4409
+ if (now - startedAt > absoluteTimeoutMs) {
4394
4410
  timedOut = true;
4395
4411
  aborted = true;
4396
4412
  abortController.abort();
@@ -4418,30 +4434,47 @@ OUTPUT: ONLY the complete extended CSS file content. No markdown fences, no expl
4418
4434
  }
4419
4435
  clearInterval(heartbeatInterval);
4420
4436
 
4421
- // Strip markdown fences
4437
+ // Strip markdown fences from the appended rules (LLM sometimes adds them)
4422
4438
  body = body.replace(/^```[a-zA-Z]*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
4423
- if (_looksLikeLLMError(body) || body.length < target.size * 0.5) {
4424
- if (emit) emit({ type: 'warn', msg: `CSS extend produced suspicious output (${body.length} bytes vs ${target.size} original) — keeping original.` });
4439
+ if (_looksLikeLLMError(body) || body.length < 200) {
4440
+ if (emit) emit({ type: 'warn', msg: `CSS extend produced suspicious output (${body.length} bytes of new rules) — keeping original.` });
4425
4441
  return { extended: false, reason: 'output_too_short_or_error' };
4426
4442
  }
4427
4443
 
4428
- // Backup + write
4444
+ // APPEND mode (16.0.57): keep ALL existing rules intact, add new ones at end.
4445
+ // Prevents monotonic regression where pass N replaces pass N-1's work with
4446
+ // a smaller file. Combined content = original + delimiter comment + new rules.
4447
+ const combined = target.content
4448
+ + '\n\n/* === nha-webcraft: auto-extended rules (' + new Date().toISOString() + ') === */\n'
4449
+ + body
4450
+ + '\n';
4451
+
4429
4452
  try {
4430
4453
  fs.writeFileSync(target.abs + '.before-extend-' + Date.now(), target.content, 'utf-8');
4431
- fs.writeFileSync(target.abs, body, 'utf-8');
4454
+ fs.writeFileSync(target.abs, combined, 'utf-8');
4432
4455
  } catch (e) {
4433
4456
  return { extended: false, reason: 'write_failed', error: e.message };
4434
4457
  }
4435
4458
 
4436
- // Re-analyze to confirm improvement
4459
+ // Re-analyze to confirm improvement. APPEND mode guarantees coverage
4460
+ // monotonically increases (or stays equal), never decreases.
4437
4461
  const after = _analyzeCssCoverage(dir);
4438
- if (emit) emit({ type: 'status', msg: `CSS extended: ${targetRel} ${target.size} → ${body.length} bytes. Coverage ${(analysis.coverage * 100).toFixed(0)}% → ${(after.coverage * 100).toFixed(0)}%.` });
4462
+ if (after.missing.length >= analysis.missing.length) {
4463
+ // No progress despite append — LLM produced rules that don't match selectors.
4464
+ // Roll back so the file doesn't bloat with useless rules.
4465
+ try { fs.writeFileSync(target.abs, target.content, 'utf-8'); } catch {}
4466
+ if (emit) emit({ type: 'warn', msg: `CSS extend rolled back: ${body.length} bytes of new rules added but no selectors covered (model output didn't match needed selectors).` });
4467
+ return { extended: false, reason: 'no_coverage_gain', missingBefore: analysis.missing.length, missingAfter: after.missing.length };
4468
+ }
4469
+
4470
+ if (emit) emit({ type: 'status', msg: `CSS extended: ${targetRel} ${target.size} → ${combined.length} bytes (appended ${body.length} bytes). Coverage ${(analysis.coverage * 100).toFixed(0)}% → ${(after.coverage * 100).toFixed(0)}%, ${analysis.missing.length} → ${after.missing.length} selectors missing.` });
4439
4471
 
4440
4472
  return {
4441
4473
  extended: true,
4442
4474
  file: targetRel,
4443
4475
  sizeBefore: target.size,
4444
- sizeAfter: body.length,
4476
+ sizeAfter: combined.length,
4477
+ appendedBytes: body.length,
4445
4478
  coverageBefore: analysis.coverage,
4446
4479
  coverageAfter: after.coverage,
4447
4480
  missingBefore: analysis.missing.length,
@@ -563,6 +563,41 @@ export async function callGrok(apiKey, model, systemPrompt, userMessage, stream
563
563
  return data.choices?.[0]?.message?.content || '';
564
564
  }
565
565
 
566
+ /**
567
+ * Auto-prefix a model name for OpenRouter. OpenRouter requires `vendor/model`
568
+ * format (e.g. `anthropic/claude-sonnet-4.5`). Agent cards in NHA often have
569
+ * bare model names like `claude-sonnet-4-20250514` or `gpt-4o`. Without the
570
+ * prefix, OpenRouter falls back to an OpenAI-like endpoint that returns a
571
+ * misleading "Incorrect API key provided" error (it's actually a model
572
+ * routing issue). We auto-prefix by inspecting the bare model name.
573
+ */
574
+ export function _autoPrefixOpenRouterModel(model) {
575
+ if (!model || typeof model !== 'string') return 'anthropic/claude-sonnet-4.5';
576
+ // Already in vendor/model format — leave alone
577
+ if (model.includes('/')) return model;
578
+ const lower = model.toLowerCase();
579
+ // Anthropic Claude family
580
+ if (lower.startsWith('claude')) return 'anthropic/' + model;
581
+ // OpenAI GPT family
582
+ if (lower.startsWith('gpt') || lower.startsWith('o1') || lower.startsWith('o3') || lower.startsWith('o4') || lower.startsWith('chatgpt') || lower === 'davinci' || lower === 'curie') return 'openai/' + model;
583
+ // Google Gemini family
584
+ if (lower.startsWith('gemini') || lower.startsWith('palm')) return 'google/' + model;
585
+ // Meta Llama family
586
+ if (lower.startsWith('llama') || lower.startsWith('codellama')) return 'meta-llama/' + model;
587
+ // DeepSeek
588
+ if (lower.startsWith('deepseek')) return 'deepseek/' + model;
589
+ // Mistral / Mixtral
590
+ if (lower.startsWith('mistral') || lower.startsWith('mixtral') || lower.startsWith('codestral')) return 'mistralai/' + model;
591
+ // Qwen
592
+ if (lower.startsWith('qwen')) return 'qwen/' + model;
593
+ // xAI Grok
594
+ if (lower.startsWith('grok')) return 'x-ai/' + model;
595
+ // Cohere
596
+ if (lower.startsWith('command') || lower.startsWith('cohere')) return 'cohere/' + model;
597
+ // Unknown vendor — default to anthropic prefix (most likely sandbox use case)
598
+ return 'anthropic/' + model;
599
+ }
600
+
566
601
  /**
567
602
  * OpenRouter — aggregator that exposes 100+ models (Claude, GPT, Gemini,
568
603
  * Mistral, Llama, Qwen, DeepSeek, etc.) via a single OpenAI-compatible API.
@@ -570,6 +605,7 @@ export async function callGrok(apiKey, model, systemPrompt, userMessage, stream
570
605
  * Model names: "anthropic/claude-sonnet-4.5", "openai/gpt-4o", "google/gemini-2.5-pro"...
571
606
  */
572
607
  export async function callOpenRouter(apiKey, model, systemPrompt, userMessage, stream = false, opts = {}) {
608
+ model = _autoPrefixOpenRouterModel(model);
573
609
  const body = {
574
610
  model: model || 'anthropic/claude-sonnet-4.5',
575
611
  max_tokens: opts.max_tokens || 8192,
@@ -594,7 +630,18 @@ export async function callOpenRouter(apiKey, model, systemPrompt, userMessage, s
594
630
  });
595
631
  if (!res.ok) {
596
632
  const err = await res.text();
597
- throw new Error(`OpenRouter ${res.status}: ${err}`);
633
+ // OpenRouter often passes through misleading "platform.openai.com" key errors
634
+ // when the real cause is (a) invalid OpenRouter key, (b) model name without
635
+ // vendor prefix, (c) model requires BYOK. Provide an actionable hint.
636
+ let hint = '';
637
+ if (res.status === 401) {
638
+ hint = '\n → 401 from OpenRouter usually means: (1) your sk-or-v1-... key is invalid/expired (regenerate at openrouter.ai/keys), or (2) the model name "' + model + '" needs a vendor prefix like "anthropic/' + model.replace(/^[a-z-]+\//, '') + '". This call auto-prefixed to "' + model + '".';
639
+ } else if (res.status === 404) {
640
+ hint = '\n → 404 from OpenRouter usually means the model "' + model + '" does not exist. See openrouter.ai/models for valid IDs.';
641
+ } else if (res.status === 402) {
642
+ hint = '\n → 402 from OpenRouter means insufficient credits. Top up at openrouter.ai/credits.';
643
+ }
644
+ throw new Error(`OpenRouter ${res.status}: ${err}${hint}`);
598
645
  }
599
646
  if (stream) return streamSSE(res, 'openai');
600
647
  const data = await res.json();
@@ -828,9 +875,12 @@ export async function callLLMWithTools(config, systemPrompt, messages, tools, on
828
875
  }
829
876
 
830
877
  // OpenRouter — uses OpenAI-compatible function calling schema. We reuse the
831
- // OpenAI tool-call implementation but point to OpenRouter's endpoint.
878
+ // OpenAI tool-call implementation but point to OpenRouter's endpoint and
879
+ // auto-prefix the model (anthropic/, openai/, etc) so bare names from agent
880
+ // cards work without manual edit.
832
881
  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' });
882
+ const orModel = _autoPrefixOpenRouterModel(model);
883
+ return _callOpenAIWithTools(apiKey, orModel, systemPrompt, messages, tools, onText, onToolCall, { ...opts, baseUrl: 'https://openrouter.ai/api/v1', referer: 'https://nothumanallowed.com', xTitle: 'NotHumanAllowed CLI' });
834
884
  }
835
885
 
836
886
  // Other providers: fallback to text-based tool calling (old system)