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 +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/webcraft.mjs +65 -32
- package/src/services/llm.mjs +53 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
4340
|
-
-
|
|
4341
|
-
-
|
|
4342
|
-
-
|
|
4343
|
-
-
|
|
4344
|
-
|
|
4345
|
-
|
|
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
|
-
`
|
|
4348
|
-
`
|
|
4349
|
-
`
|
|
4350
|
-
`
|
|
4351
|
-
`Output the
|
|
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
|
-
`-
|
|
4354
|
-
`-
|
|
4355
|
-
|
|
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
|
|
4380
|
-
const
|
|
4381
|
-
|
|
4382
|
-
|
|
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 (
|
|
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 (
|
|
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 <
|
|
4424
|
-
if (emit) emit({ type: 'warn', msg: `CSS extend produced suspicious output (${body.length} bytes
|
|
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
|
-
//
|
|
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,
|
|
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 (
|
|
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:
|
|
4476
|
+
sizeAfter: combined.length,
|
|
4477
|
+
appendedBytes: body.length,
|
|
4445
4478
|
coverageBefore: analysis.coverage,
|
|
4446
4479
|
coverageAfter: after.coverage,
|
|
4447
4480
|
missingBefore: analysis.missing.length,
|
package/src/services/llm.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|