nothumanallowed 13.2.29 → 13.2.30

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": "13.2.29",
3
+ "version": "13.2.30",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2544,56 +2544,85 @@ export async function cmdUI(args) {
2544
2544
  if (!task) { sendJSON(res, 400, { error: 'task required' }); logRequest(method, pathname, 400, Date.now() - start); return; }
2545
2545
 
2546
2546
  const plannerLang = (() => { const LANG_MAP2 = {en:'English',it:'Italian',es:'Spanish',fr:'French',de:'German',pt:'Portuguese',zh:'Chinese',ja:'Japanese',ar:'Arabic',hi:'Hindi',ru:'Russian',nl:'Dutch',pl:'Polish',tr:'Turkish',ko:'Korean'}; const lc = (config?.language||'it').slice(0,2); return LANG_MAP2[lc]||'Italian'; })();
2547
- const planPrompt = `You are a workflow planner. Task: "${task}"
2548
- Language: ${plannerLang}. All prompts must be in ${plannerLang}.
2549
2547
 
2550
- Output ONLY valid JSON (no markdown, no explanation, no thinking):
2551
- {"steps":[{"icon":"EMOJI","agent":"AGENT_NAME","label":"SHORT LABEL","prompt":"INSTRUCTION IN ${plannerLang}"}]}
2552
-
2553
- Rules:
2554
- - 2-5 steps max
2555
- - Use WebSearchAgent to fetch live web data
2556
- - Use EmailAgent/CalendarAgent/GitHubAgent/SlackAgent for personal live data
2557
- - Use HERALD for news briefings, ORACLE for business analysis, NAVI for data analysis, ATHENA for tech evaluation, CASSANDRA for risk, MERCURY for finance, HERALD for trends, QUILL for summaries
2558
- - Use CanvasAgent LAST only if user explicitly asks for HTML/visual/dashboard
2559
- - icon must be a real emoji character
2548
+ // ── Fast keyword-based planning (no LLM call needed for common patterns) ──────
2549
+ // Detect task intent from keywords — avoids sending the full task through SENTINEL
2550
+ const taskLow = task.toLowerCase();
2551
+ const hasEmail = /email|mail|inbox|posta/i.test(taskLow);
2552
+ const hasCalendar = /calendar|agenda|calendari|eventi|schedule/i.test(taskLow);
2553
+ const hasSearch = /cerca|search|notizie|news|ultime|latest|web|internet/i.test(taskLow);
2554
+ const hasCanvas = /html|dashboard|visua|report|grafico|chart/i.test(taskLow);
2555
+ const hasGitHub = /github|git|issue|pr|pull request/i.test(taskLow);
2556
+ const hasSlack = /slack|channel|messag/i.test(taskLow);
2557
+ const hasNotion = /notion|note|page/i.test(taskLow);
2558
+ const hasBriefing = /briefing|analisi|analizza|summary|sommario|riassunto|riepiloga/i.test(taskLow);
2559
+ const hasFinance = /finance|mercato|market|stock|trading|finanz/i.test(taskLow);
2560
+ const hasSecurity = /security|sicurezza|vulnerabilit|audit|pentest/i.test(taskLow);
2561
+
2562
+ // Extract a clean short search query from the task (to avoid SENTINEL flagging long task strings)
2563
+ const extractSearchQuery = (t) => {
2564
+ // Try to find an explicit search topic after keywords like "cerca", "search", "notizie su"
2565
+ const m = t.match(/(?:cerca|search|find|ricerca|notizie su|news about|latest on|aggiornamenti su|ultime su)\s+(.{5,80}?)(?:\s+(?:e |and |per |for |poi |then )|[,\n]|$)/i);
2566
+ if (m) return m[1].trim();
2567
+ // Strip leading instruction prefix (everything before the first colon)
2568
+ const stripped = t.replace(/^[^:]+:\s*/,'').split(/[,\n]/)[0].slice(0,100).trim();
2569
+ return stripped || t.slice(0,80).trim();
2570
+ };
2571
+ const searchQuery = extractSearchQuery(task);
2572
+
2573
+ // Build plan directly from keywords — reliable, fast, no SENTINEL risk
2574
+ const buildKeywordPlan = () => {
2575
+ const steps = [];
2576
+ if (hasEmail) steps.push({icon:'\u{1F4E7}',agent:'EmailAgent',label:plannerLang==='Italian'?'Controlla email':'Check emails',prompt:'Read the latest unread emails and identify urgent items, deadlines, and required actions'});
2577
+ if (hasCalendar) steps.push({icon:'\u{1F4C5}',agent:'CalendarAgent',label:plannerLang==='Italian'?'Rivedi calendario':'Review calendar',prompt:'Check today\'s events and identify any scheduling conflicts or important meetings'});
2578
+ if (hasGitHub) steps.push({icon:'\u{1F4BB}',agent:'GitHubAgent',label:'GitHub',prompt:'Read open issues and pull requests, identify what needs attention'});
2579
+ if (hasSlack) steps.push({icon:'\u{1F4AC}',agent:'SlackAgent',label:'Slack',prompt:'Check recent Slack messages and identify important conversations'});
2580
+ if (hasNotion) steps.push({icon:'\u{1F4DD}',agent:'NotionAgent',label:'Notion',prompt:'Search Notion for relevant pages and notes'});
2581
+ if (hasSearch || (!hasEmail && !hasCalendar && !hasGitHub && !hasSlack)) {
2582
+ steps.push({icon:'\u{1F50D}',agent:'WebSearchAgent',label:plannerLang==='Italian'?'Ricerca web':'Web search',prompt:searchQuery});
2583
+ }
2584
+ if (hasSecurity) {
2585
+ steps.push({icon:'\u{1F6E1}',agent:'cassandra',label:plannerLang==='Italian'?'Analisi rischi':'Risk analysis',prompt:'Analyze the data and identify security risks and recommendations'});
2586
+ } else if (hasFinance) {
2587
+ steps.push({icon:'\u{1F4B0}',agent:'mercury',label:plannerLang==='Italian'?'Analisi finanziaria':'Financial analysis',prompt:'Analyze the financial data and market information'});
2588
+ } else if (hasBriefing || steps.length > 0) {
2589
+ steps.push({icon:'\u{1F4F0}',agent:'HERALD',label:plannerLang==='Italian'?'Analisi e briefing':'Analysis & briefing',prompt:'Based on ALL the data collected by the previous steps, write a complete executive briefing with priorities, findings, and strategic recommendations. Do NOT invent data — only use what was provided.'});
2590
+ }
2591
+ if (hasCanvas) steps.push({icon:'\u{1F4CA}',agent:'CanvasAgent',label:plannerLang==='Italian'?'Dashboard HTML':'HTML Dashboard',prompt:'Create a professional HTML dashboard report with the briefing data'});
2592
+ return steps;
2593
+ };
2560
2594
 
2561
- Example output:
2562
- {"steps":[{"icon":"🔍","agent":"WebSearchAgent","label":"Cerca notizie","prompt":"Cerca le ultime notizie su intelligenza artificiale oggi"},{"icon":"📰","agent":"HERALD","label":"Analisi notizie","prompt":"Analizza le notizie trovate e crea un briefing esecutivo"},{"icon":"📊","agent":"CanvasAgent","label":"Dashboard HTML","prompt":"Crea una dashboard HTML visuale con i risultati"}]}`;
2595
+ // Use keyword plan directly if it covers the task well enough
2596
+ // Only fall back to LLM planning for genuinely ambiguous tasks
2597
+ const keywordSteps = buildKeywordPlan();
2598
+ const taskIsComplex = !hasEmail && !hasCalendar && !hasSearch && !hasGitHub && !hasSlack && !hasBriefing && keywordSteps.length <= 1;
2563
2599
 
2564
2600
  try {
2565
- // Force thinking OFF for planner — we need deterministic JSON, not reasoning chains
2566
- const planConfig = Object.assign({}, config, { thinking: 'off' });
2567
- const planRaw = await callLLM(planConfig, 'You are a JSON workflow planner. Output ONLY valid JSON. No thinking, no explanation, no markdown.', planPrompt, { max_tokens: 1500 });
2568
- process.stderr.write('[STUDIO PLAN RAW] ' + planRaw.slice(0, 400) + '\n');
2569
2601
  let steps;
2570
- try {
2571
- // Strip ALL <think>...</think> blocks (greedy handles nested/multiple)
2572
- let clean = planRaw;
2573
- let prev = '';
2574
- while (prev !== clean) { prev = clean; clean = clean.replace(/<think>[\s\S]*?<\/think>/g, ''); }
2575
- clean = clean.trim();
2576
- // Strip markdown fences
2577
- clean = clean.replace(/^```[\w]*\r?\n?/,'').replace(/\r?\n?```$/,'').trim();
2578
- // Extract first complete JSON object
2579
- const jsonMatch = clean.match(/\{[\s\S]*\}/);
2580
- const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : clean);
2581
- steps = parsed.steps;
2582
- } catch (parseErr) {
2583
- process.stderr.write('[STUDIO PLAN PARSE ERR] ' + parseErr.message + '\n');
2584
- // Fallback: build a sensible default plan from the task keywords
2585
- const hasEmail = /email|mail/i.test(task);
2586
- const hasCalendar = /calendar|agenda|calendari/i.test(task);
2587
- const hasSearch = /cerca|search|notizie|news/i.test(task);
2588
- const hasCanvas = /html|dashboard|visua|report/i.test(task);
2589
- // Extract a short search query from the task (take key noun phrases, max 80 chars)
2590
- const searchQueryFallback = task.replace(/^[^:]+:\s*/,'').split(/[,\n]/)[0].slice(0,80).trim() || task.slice(0,80);
2591
- steps = [];
2592
- if (hasEmail) steps.push({icon:'\u{1F4E7}',agent:'EmailAgent',label:'Controlla email',prompt:'Leggi le ultime email non lette e identifica elementi urgenti'});
2593
- if (hasCalendar) steps.push({icon:'\u{1F4C5}',agent:'CalendarAgent',label:'Rivedi calendario',prompt:'Controlla gli eventi di oggi e identifica eventuali conflitti'});
2594
- if (hasSearch || steps.length === 0) steps.push({icon:'\u{1F50D}',agent:'WebSearchAgent',label:'Ricerca web',prompt:searchQueryFallback});
2595
- steps.push({icon:'\u{1F4F0}',agent:'HERALD',label:'Analisi e briefing',prompt:'Analizza tutti i dati forniti e scrivi un briefing esecutivo dettagliato con priorit\u00e0 e raccomandazioni'});
2596
- if (hasCanvas) steps.push({icon:'\u{1F4CA}',agent:'CanvasAgent',label:'Dashboard HTML',prompt:'Crea una dashboard HTML visuale con i dati del briefing'});
2602
+ if (!taskIsComplex) {
2603
+ // Use keyword plan directlyno LLM, no SENTINEL risk
2604
+ process.stderr.write('[STUDIO PLAN KEYWORD] steps=' + keywordSteps.length + '\n');
2605
+ steps = keywordSteps;
2606
+ } else {
2607
+ // Task is ambiguous — use LLM planner with sanitized short description
2608
+ const shortTask = task.slice(0, 200).replace(/[`'"]/g, ' ');
2609
+ const plannerLangStr = plannerLang;
2610
+ const planPrompt = `Workflow planner. Goal: ${shortTask}\nLanguage: ${plannerLangStr}.\nOutput ONLY JSON:\n{"steps":[{"icon":"EMOJI","agent":"AGENT_NAME","label":"LABEL","prompt":"INSTRUCTION"}]}\nAgents: WebSearchAgent, EmailAgent, CalendarAgent, HERALD, ORACLE, ATHENA, CASSANDRA, MERCURY, QUILL, CanvasAgent (last, only if visual needed). 2-5 steps.`;
2611
+ const planConfig = Object.assign({}, config, { thinking: 'off' });
2612
+ const planRaw = await callLLM(planConfig, 'Output ONLY valid JSON. No explanation.', planPrompt, { max_tokens: 800 });
2613
+ process.stderr.write('[STUDIO PLAN LLM RAW] ' + planRaw.slice(0, 400) + '\n');
2614
+ try {
2615
+ let clean = planRaw;
2616
+ let prev = '';
2617
+ while (prev !== clean) { prev = clean; clean = clean.replace(/<think>[\s\S]*?<\/think>/g, ''); }
2618
+ clean = clean.trim().replace(/^```[\w]*\r?\n?/,'').replace(/\r?\n?```$/,'').trim();
2619
+ const jsonMatch = clean.match(/\{[\s\S]*\}/);
2620
+ const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : clean);
2621
+ steps = parsed.steps;
2622
+ } catch (parseErr) {
2623
+ process.stderr.write('[STUDIO PLAN PARSE ERR] ' + parseErr.message + '\n');
2624
+ steps = keywordSteps;
2625
+ }
2597
2626
  }
2598
2627
  if (!Array.isArray(steps) || !steps.length) {
2599
2628
  sendJSON(res, 500, { error: 'Empty workflow plan' });
@@ -2742,94 +2771,42 @@ Example output:
2742
2771
  // Tool-data agents: fetch real live data and use buildSystemPrompt (tool calls allowed)
2743
2772
  const isLiveDataAgent = ['CalendarAgent','EmailAgent','GitHubAgent','NotionAgent','SlackAgent','DriveAgent','BrowserAgent','WebSearchAgent','ResearchAgent'].includes(agent);
2744
2773
 
2745
- const canvasSystemPrompt = `You are an expert HTML report designer. Output ONLY a complete HTML document in ${language}. Start immediately with <!DOCTYPE html>. No markdown, no explanation.
2746
-
2747
- FORMATTING RULES use ALL of these in the report:
2748
- - <strong> for bold key terms and important values
2749
- - <em> for emphasis and technical terms
2750
- - <u> for critical warnings or deadlines
2751
- - <ul><li> for bullet lists, <ol><li> for numbered/priority lists
2752
- - <a href="URL" target="_blank" style="color:#22d3ee;text-decoration:underline"> for ALL source URLsALWAYS make URLs clickable hyperlinks, never plain text
2753
- - Bar charts: use CSS width% on colored divs to visualize percentages/scores
2754
- - Use <span class="badge-high">, <span class="badge-med">, <span class="badge-low"> for priority labels
2755
-
2756
- CONTENT RULES:
2757
- - ALL text must be in ${language}
2758
- - Include ALL real content from the data: titles, descriptions, URLs, key findings
2759
- - Do NOT summarize include specific details, names, numbers, dates from the source data
2760
- - For each news item/source: show title as <strong>, URL as clickable <a href>, and a real summary paragraph
2761
- - Use <blockquote> for direct quotes or key excerpts
2762
-
2763
- USE EXACTLY THIS CSS (do not change it, only add content):
2764
- <!DOCTYPE html>
2765
- <html lang="${language.slice(0,2).toLowerCase()}">
2766
- <head>
2767
- <meta charset="UTF-8">
2768
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
2769
- <title>NHA Studio Report</title>
2770
- <style>
2771
- *{margin:0;padding:0;box-sizing:border-box}
2772
- body{font-family:'Inter',system-ui,sans-serif;background:#0d0d14;color:#f0f0f5;min-height:100vh;padding:20px;font-size:14px;line-height:1.65}
2773
- a{color:#22d3ee;text-decoration:underline}a:hover{color:#6366f1}
2774
- strong{color:#f0f0f5;font-weight:700}
2775
- em{color:#a5b4fc;font-style:italic}
2776
- u{text-decoration-color:#ef4444}
2777
- blockquote{border-left:3px solid #6366f1;padding:8px 16px;margin:10px 0;background:#15151f;border-radius:0 8px 8px 0;color:#8b8b9e;font-style:italic}
2778
- .header{background:linear-gradient(135deg,#4f46e5 0%,#06b6d4 100%);border-radius:16px;padding:28px 36px;margin-bottom:20px}
2779
- .header h1{font-size:26px;font-weight:800;color:#fff;margin-bottom:6px}
2780
- .header p{font-size:13px;color:rgba(255,255,255,0.85);margin:0}
2781
- .meta{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}
2782
- .meta span{background:rgba(255,255,255,0.18);border-radius:20px;padding:3px 12px;font-size:11px;color:#fff;font-weight:500}
2783
- .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin-bottom:20px}
2784
- .card{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:18px}
2785
- .card-label{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#6366f1;font-weight:700;margin-bottom:8px}
2786
- .card h3{font-size:18px;font-weight:700;color:#f0f0f5;margin-bottom:4px}
2787
- .card p{font-size:12px;color:#8b8b9e;margin:0}
2788
- .section{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:22px;margin-bottom:16px}
2789
- .section-title{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#22d3ee;font-weight:700;margin-bottom:16px}
2790
- .section h3{font-size:15px;font-weight:600;color:#f0f0f5;margin-bottom:6px;margin-top:14px}
2791
- .section p{font-size:13px;color:#8b8b9e;line-height:1.7;margin-bottom:10px}
2792
- ul{list-style:none;padding:0;margin:8px 0}
2793
- ul li{padding:4px 0 4px 18px;position:relative;font-size:13px;color:#8b8b9e}
2794
- ul li::before{content:'›';position:absolute;left:0;color:#6366f1;font-weight:700}
2795
- ol{padding-left:20px;margin:8px 0}
2796
- ol li{padding:4px 0;font-size:13px;color:#8b8b9e;line-height:1.6}
2797
- .priority-list{display:flex;flex-direction:column;gap:8px}
2798
- .priority-item{display:flex;align-items:flex-start;gap:12px;padding:12px;background:#1c1c28;border-radius:8px}
2799
- .priority-num{width:26px;height:26px;border-radius:50%;background:#6366f1;color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
2800
- .priority-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:3px}
2801
- .priority-item p{font-size:12px;color:#8b8b9e;line-height:1.5;margin:0}
2802
- .source-item{padding:14px;background:#1c1c28;border-radius:8px;margin-bottom:10px;border-left:3px solid #6366f1}
2803
- .source-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:4px}
2804
- .source-item p{font-size:12px;color:#8b8b9e;line-height:1.6;margin:4px 0}
2805
- .source-item a{font-size:11px}
2806
- .bar-row{margin-bottom:10px}
2807
- .bar-label{font-size:12px;color:#8b8b9e;margin-bottom:4px;display:flex;justify-content:space-between}
2808
- .bar-track{background:#1c1c28;border-radius:4px;height:8px;overflow:hidden}
2809
- .bar-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#6366f1,#22d3ee)}
2810
- .badge-high{display:inline-block;background:#7f1d1d;color:#ef4444;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
2811
- .badge-med{display:inline-block;background:#713f12;color:#f59e0b;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
2812
- .badge-low{display:inline-block;background:#14532d;color:#34d399;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
2813
- .badge-info{display:inline-block;background:#1e1b4b;color:#a5b4fc;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
2814
- .divider{border:none;border-top:1px solid #2a2a38;margin:16px 0}
2815
- .footer{text-align:center;padding:18px;font-size:11px;color:#4a4a5e;margin-top:8px}
2816
- </style>
2817
- </head>
2818
- <body>
2819
-
2820
- [HEADER — gradient card with h1 title, subtitle p, and .meta spans for date/stats]
2821
- [GRID — 3-4 .card stat boxes with real numbers from the data]
2822
- [SECTIONS — use .section with .section-title, then real content with h3, p, ul/ol, .source-item for URLs, .priority-list for ranked items, .bar-row for visual charts]
2823
- [For every URL in the data: wrap in <a href="..." target="_blank"> as a clickable hyperlink]
2824
- [FOOTER — "NHA Studio · ${today}"]
2825
-
2826
- Output ONLY the full HTML. Replace all bracketed instructions above with real HTML content.`;
2774
+ // ── Canvas HTML template built server-side, guaranteed CSS ─────
2775
+ // The LLM outputs ONLY the <body> inner HTML (no <html>, no <style>)
2776
+ // Server wraps it in the full template. This prevents the model from ignoring CSS.
2777
+ const NHA_CSS = `*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Inter',system-ui,sans-serif;background:#0d0d14;color:#f0f0f5;min-height:100vh;padding:24px;font-size:14px;line-height:1.65}a{color:#22d3ee;text-decoration:underline}a:hover{color:#818cf8}strong{color:#f0f0f5;font-weight:700}em{color:#a5b4fc;font-style:italic}u{text-decoration-color:#ef4444;text-underline-offset:2px}blockquote{border-left:3px solid #6366f1;padding:10px 16px;margin:12px 0;background:#15151f;border-radius:0 8px 8px 0;color:#8b8b9e;font-style:italic}.header{background:linear-gradient(135deg,#4f46e5 0%,#06b6d4 100%);border-radius:16px;padding:28px 36px;margin-bottom:20px}.header h1{font-size:24px;font-weight:800;color:#fff;margin-bottom:6px}.header p{font-size:13px;color:rgba(255,255,255,.85);margin:0}.meta{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}.meta span{background:rgba(255,255,255,.18);border-radius:20px;padding:3px 12px;font-size:11px;color:#fff;font-weight:500}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-bottom:20px}.card{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:18px}.card-label{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#6366f1;font-weight:700;margin-bottom:8px}.card h3{font-size:20px;font-weight:700;color:#f0f0f5;margin-bottom:4px}.card p{font-size:12px;color:#8b8b9e;margin:0}.section{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:22px;margin-bottom:16px}.section-title{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#22d3ee;font-weight:700;margin-bottom:16px}.section h3{font-size:15px;font-weight:600;color:#f0f0f5;margin-bottom:6px;margin-top:14px}.section p{font-size:13px;color:#8b8b9e;line-height:1.7;margin-bottom:10px}ul{list-style:none;padding:0;margin:8px 0}ul li{padding:4px 0 4px 18px;position:relative;font-size:13px;color:#8b8b9e}ul li::before{content:'›';position:absolute;left:0;color:#6366f1;font-weight:700}ol{padding-left:20px;margin:8px 0}ol li{padding:4px 0;font-size:13px;color:#8b8b9e;line-height:1.6}.priority-list{display:flex;flex-direction:column;gap:8px}.priority-item{display:flex;align-items:flex-start;gap:12px;padding:12px;background:#1c1c28;border-radius:8px}.priority-num{width:26px;height:26px;border-radius:50%;background:#6366f1;color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}.priority-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:3px}.priority-item p{font-size:12px;color:#8b8b9e;line-height:1.5;margin:0}.source-item{padding:14px;background:#1c1c28;border-radius:8px;margin-bottom:10px;border-left:3px solid #6366f1}.source-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:4px}.source-item p{font-size:12px;color:#8b8b9e;line-height:1.6;margin:4px 0}.source-item a{font-size:11px}.bar-row{margin-bottom:10px}.bar-label{font-size:12px;color:#8b8b9e;margin-bottom:4px;display:flex;justify-content:space-between}.bar-track{background:#1c1c28;border-radius:4px;height:8px;overflow:hidden}.bar-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#6366f1,#22d3ee)}.badge-high{display:inline-block;background:#7f1d1d;color:#ef4444;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-med{display:inline-block;background:#713f12;color:#f59e0b;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-low{display:inline-block;background:#14532d;color:#34d399;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-info{display:inline-block;background:#1e1b4b;color:#a5b4fc;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.divider{border:none;border-top:1px solid #2a2a38;margin:16px 0}.footer{text-align:center;padding:18px;font-size:11px;color:#4a4a5e;margin-top:8px}`;
2778
+
2779
+ const wrapInNHATemplate = (bodyHtml, title) => `<!DOCTYPE html><html lang="${langCode}"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${(title||'NHA Report').replace(/</g,'&lt;')}</title><style>${NHA_CSS}</style></head><body>${bodyHtml}</body></html>`;
2780
+
2781
+ const canvasSystemPrompt = `You are a professional HTML content generator. Output ONLY the HTML content that goes INSIDE the <body> tag. Do NOT output <!DOCTYPE>, <html>, <head>, <style>, or any wrapper tags the CSS and document structure are already provided by the system.
2782
+
2783
+ AVAILABLE CSS CLASSES (use these, they are pre-defined):
2784
+ - .header > h1, p, .meta > span (gradient header banner)
2785
+ - .grid > .card > .card-label, h3, p (stat grid)
2786
+ - .section > .section-title, h3, p (content sections)
2787
+ - .source-item > h4, p, a (news/source items with left accent)
2788
+ - .priority-list > .priority-item > .priority-num, h4, p (ranked list)
2789
+ - .bar-row > .bar-label, .bar-track > .bar-fill[style="width:X%"] (bar charts)
2790
+ - .badge-high .badge-med .badge-low .badge-info (colored badges)
2791
+ - ul > li, ol > li (bullet/numbered lists with custom styling)
2792
+ - blockquote (quoted excerpts)
2793
+ - .divider (horizontal rule)
2794
+ - .footer (footer)
2795
+ - <strong> <em> <u> (inline formatting)
2796
+ - <a href="URL" target="_blank"> (clickable links — use for ALL URLs in the data)
2797
+
2798
+ RULES:
2799
+ - Language: ${language}. ALL text must be in ${language}.
2800
+ - Use REAL data from the input — do NOT invent or fabricate
2801
+ - URLs from the data: ALWAYS wrap in <a href="URL" target="_blank">clickable text</a>
2802
+ - Use .priority-list for action items, .source-item for each email/news source, .bar-row for any percentage data
2803
+ - Output must start with <div class="header"> and end with <div class="footer">`;
2827
2804
 
2828
2805
  let sysPrompt, userMsg;
2829
2806
 
2830
2807
  if (isCanvasAgent) {
2831
2808
  sysPrompt = canvasSystemPrompt;
2832
- userMsg = `Generate a beautiful HTML dashboard report for this content. Start immediately with <!DOCTYPE html>:\n\n${context.slice(0, 10000)}`;
2809
+ userMsg = `Create a professional dashboard report for this data. Output ONLY the inner HTML body content (starting with <div class="header">):\n\n${context.slice(0, 10000)}`;
2833
2810
  } else if (isLiveDataAgent) {
2834
2811
  // These agents fetched real data — use a focused prompt (no tool definitions to avoid JSON output)
2835
2812
  const agentInstruction = `You are ${agent}, a specialist AI agent inside NHA Studio. Today is ${today}. Respond entirely in ${language}.
@@ -2898,50 +2875,37 @@ ${context ? `## OUTPUT FROM PREVIOUS AGENTS:\n${context.slice(0, 6000)}\n` : ''}
2898
2875
  }
2899
2876
 
2900
2877
  if (isCanvasAgent) {
2901
- let html = fullOutput.trim();
2902
- // Strip thinking tags if not already filtered
2903
- html = html.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
2904
- // Extract from markdown code block
2905
- const mdMatch = html.match(/```html?\s*([\s\S]*?)```/i);
2906
- if (mdMatch) html = mdMatch[1].trim();
2907
- // Find <!DOCTYPE or <html start if there's preamble text
2908
- const doctypeIdx = html.indexOf('<!DOCTYPE');
2909
- const htmlTagIdx = html.indexOf('<html');
2910
- const startIdx = doctypeIdx >= 0 ? doctypeIdx : (htmlTagIdx >= 0 ? htmlTagIdx : -1);
2911
- if (startIdx > 0) html = html.slice(startIdx);
2912
- // Fallback: build clean HTML from the context directly (no LLM needed)
2913
- if (!html.trim() || !html.includes('<')) {
2914
- // Try splitting on markdown headings first, then numbered items, then double newlines
2915
- let sections = context.split(/\n#{1,3} /).filter(s => s.trim());
2916
- if (sections.length <= 1) sections = context.split(/\n(?=\*\*\d+[\.\)])|(?=^\d+[\.\)])/).filter(s => s.trim());
2917
- if (sections.length <= 1) sections = context.split(/\n{2,}/).filter(s => s.trim());
2918
- const reportTitle = (task.slice(0, 80) || 'NHA Studio Report').replace(/</g,'&lt;').replace(/>/g,'&gt;');
2919
- const cardsHtml = sections.map(s => {
2920
- const clean = s.replace(/\*\*/g, '').replace(/\*/g, '').trim();
2921
- const lines = clean.split('\n').filter(Boolean);
2922
- const titleLine = lines[0] || '';
2923
- const bodyLines = lines.slice(1).join('\n').trim();
2924
- return `<div class="card">${titleLine ? `<h2>${titleLine.replace(/</g,'&lt;')}</h2>` : ''}<p>${(bodyLines || titleLine).replace(/\n/g, '</p><p>').replace(/</g,'&lt;')}</p></div>`;
2925
- }).join('');
2926
- const safeContext = context.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'</p><p>');
2927
- html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Report</title><style>
2928
- *{box-sizing:border-box;margin:0;padding:0}
2929
- body{font-family:system-ui,-apple-system,sans-serif;background:#f8fafc;color:#1e293b;padding:0}
2930
- .header{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;padding:48px 40px;margin-bottom:32px}
2931
- .header h1{font-size:1.8em;font-weight:700;margin-bottom:8px}
2932
- .header p{opacity:.85;font-size:1em}
2933
- .content{max-width:900px;margin:0 auto;padding:0 32px 48px}
2934
- .card{background:#fff;border-radius:12px;padding:28px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.05);border:1px solid #e2e8f0}
2935
- .card h2{color:#6366f1;font-size:1.05em;font-weight:700;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid #f1f5f9}
2936
- .card p{color:#475569;line-height:1.75;margin-bottom:8px}
2937
- .card p:last-child{margin-bottom:0}
2938
- </style></head><body>
2939
- <div class="header"><h1>${reportTitle}</h1><p>Report generated by NHA Studio</p></div>
2940
- <div class="content">${cardsHtml || '<div class="card"><p>' + safeContext + '</p></div>'}</div>
2941
- </body></html>`;
2878
+ let bodyHtml = fullOutput.trim();
2879
+ // Strip thinking tags
2880
+ bodyHtml = bodyHtml.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
2881
+ // Strip markdown code fences
2882
+ const mdMatch = bodyHtml.match(/```html?\s*([\s\S]*?)```/i);
2883
+ if (mdMatch) bodyHtml = mdMatch[1].trim();
2884
+ // If model returned full HTML despite instructions, extract body content
2885
+ const bodyTagMatch = bodyHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
2886
+ if (bodyTagMatch) bodyHtml = bodyTagMatch[1].trim();
2887
+ // If model returned <!DOCTYPE (full doc), extract everything after <body> open tag
2888
+ else if (bodyHtml.includes('<!DOCTYPE') || bodyHtml.includes('<html')) {
2889
+ const bodyStart = bodyHtml.search(/<body[^>]*>/i);
2890
+ if (bodyStart >= 0) bodyHtml = bodyHtml.slice(bodyStart).replace(/<body[^>]*>/i, '').replace(/<\/body>[\s\S]*/i, '').trim();
2891
+ }
2892
+ // Fallback: if LLM output is empty or has no HTML tags, build body from context using markdown→HTML conversion
2893
+ if (!bodyHtml || !bodyHtml.includes('<')) {
2894
+ const reportTitle = task.slice(0, 80).replace(/</g,'&lt;');
2895
+ const sections = context.split(/\n#{1,3} |(?=\n\n)/).filter(s => s.trim()).slice(0, 12);
2896
+ bodyHtml = `<div class="header"><h1>${reportTitle}</h1><p>NHA Studio Report \u00b7 ${today}</p><div class="meta"><span>${today}</span></div></div>` +
2897
+ sections.map(s => {
2898
+ const lines = s.replace(/\*\*/g,'').replace(/\*/g,'').trim().split('\n').filter(Boolean);
2899
+ const title = lines[0] || '';
2900
+ const body = lines.slice(1).map(l => `<p>${l.replace(/</g,'&lt;')}</p>`).join('');
2901
+ return `<div class="section"><div class="section-title">${title.replace(/</g,'&lt;')}</div>${body}</div>`;
2902
+ }).join('') +
2903
+ `<div class="footer">NHA Studio \u00b7 ${today}</div>`;
2942
2904
  }
2905
+ // Always wrap in the guaranteed NHA dark CSS template
2906
+ const finalHtml = wrapInNHATemplate(bodyHtml, task.slice(0, 60));
2943
2907
  sendToken('\n\n[Report generato]');
2944
- sendEvent({ canvas: html });
2908
+ sendEvent({ canvas: finalHtml });
2945
2909
  }
2946
2910
 
2947
2911
  // Estimate token usage (aprox: 1 token ≈ 4 chars)
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 = '13.2.29';
8
+ export const VERSION = '13.2.30';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -3218,7 +3218,14 @@ function runStudioStep(idx, node, task, context, stepDef, signal) {
3218
3218
  if (ev.token) {
3219
3219
  // Tool status tokens (start with '[') shown in dim color, LLM output streamed normally
3220
3220
  var isStatus = ev.token.charAt(0) === '[' && ev.token.indexOf(']') > 0 && ev.token.length < 80;
3221
- if (!isStatus) output += ev.token;
3221
+ if (!isStatus) {
3222
+ var tok = ev.token;
3223
+ // Insert space between tokens if missing (Qwen3/Liara emits tokens without separators)
3224
+ if (output && tok && !/[\s\n]$/.test(output) && !/^[\s\n.,;:!?)\]}\u2019'"]/.test(tok)) {
3225
+ tok = ' ' + tok;
3226
+ }
3227
+ output += tok;
3228
+ }
3222
3229
  // Update live log entry
3223
3230
  var entries = document.querySelectorAll('.studio-log-entry');
3224
3231
  var last = entries[entries.length - 1];