nothumanallowed 13.2.95 → 13.2.97

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.95",
3
+ "version": "13.2.97",
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": {
@@ -3280,116 +3280,146 @@ ${context ? `## OUTPUT FROM PREVIOUS AGENTS (use only what is RELEVANT to the wo
3280
3280
  }
3281
3281
 
3282
3282
  // ── Stream LLM response ───────────────────────────────────────
3283
+ // Specialist (non-canvas, non-live-data) agents use Structured Generation:
3284
+ // Phase 1 — Outline: one call that returns ONLY section headings as a numbered list
3285
+ // Phase 2 — Section fill: one call per section, with full context of sections already written
3286
+ // Live-data + Canvas agents use the classic single-stream approach.
3283
3287
  let fullOutput = '';
3284
- let inThinkBlock = false;
3285
- let thinkBuf = '';
3286
- sendToken(isCanvasAgent ? 'Generating visual report...' : '');
3287
- const llmTimeout = isCanvasAgent ? 180000 : 120000;
3288
- // Canvas: no thinking (needs full tokens for HTML). Specialists: cap thinking to 2048 to leave room for answer.
3289
- const stepLlmOpts = isCanvasAgent
3290
- ? { max_tokens: 12288, thinking: 'off' }
3291
- : { max_tokens: 8192, thinking_budget: 2048 };
3292
- try {
3288
+ let fullOutputClean = '';
3289
+
3290
+ // ── Helper: strip <think> tags from a string ─────────────────
3291
+ const stripThink = (s) => s.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
3292
+
3293
+ // ── Helper: stream a single LLM call, strip think tags, return full text ─
3294
+ const streamCall = async (sysPr, usrMsg, opts, timeout, onToken) => {
3295
+ let raw = '';
3296
+ let inThink = false;
3297
+ let tBuf = '';
3293
3298
  await withTimeout(
3294
- callLLMStream(config, sysPrompt, userMsg,
3295
- (token) => {
3296
- fullOutput += token;
3297
- if (!isCanvasAgent) {
3298
- // Buffer and strip <think>...</think> blocks before sending to client
3299
- thinkBuf += token;
3300
- // Process thinkBuf: emit only content outside think tags
3301
- let out = '';
3302
- let buf = thinkBuf;
3303
- while (buf.length > 0) {
3304
- if (inThinkBlock) {
3305
- const closeIdx = buf.indexOf('</think>');
3306
- if (closeIdx >= 0) {
3307
- buf = buf.slice(closeIdx + 8);
3308
- inThinkBlock = false;
3309
- } else {
3310
- buf = '';
3311
- }
3312
- } else {
3313
- const openIdx = buf.indexOf('<think>');
3314
- if (openIdx >= 0) {
3315
- out += buf.slice(0, openIdx);
3316
- buf = buf.slice(openIdx + 7);
3317
- inThinkBlock = true;
3318
- } else {
3319
- out += buf;
3320
- buf = '';
3321
- }
3322
- }
3299
+ callLLMStream(config, sysPr, usrMsg, (tok) => {
3300
+ raw += tok;
3301
+ if (onToken) {
3302
+ tBuf += tok;
3303
+ let out = '';
3304
+ let buf = tBuf;
3305
+ while (buf.length > 0) {
3306
+ if (inThink) {
3307
+ const ci = buf.indexOf('</think>');
3308
+ if (ci >= 0) { buf = buf.slice(ci + 8); inThink = false; }
3309
+ else { buf = ''; }
3310
+ } else {
3311
+ const oi = buf.indexOf('<think>');
3312
+ if (oi >= 0) { out += buf.slice(0, oi); buf = buf.slice(oi + 7); inThink = true; }
3313
+ else { out += buf; buf = ''; }
3323
3314
  }
3324
- thinkBuf = '';
3325
- // Strip JSON tool calls from specialist agent output
3326
- const stripped = out.replace(/\{[\s\S]*?"action"[\s\S]*?\}/g, '').trim();
3327
- if (stripped) sendToken(stripped);
3328
3315
  }
3329
- },
3330
- stepLlmOpts,
3331
- ),
3332
- llmTimeout
3316
+ tBuf = '';
3317
+ const stripped = out.replace(/\{[\s\S]*?"action"[\s\S]*?\}/g, '').trim();
3318
+ if (stripped) onToken(stripped);
3319
+ }
3320
+ }, opts),
3321
+ timeout
3333
3322
  );
3334
- } catch (e) {
3335
- if (!isCanvasAgent) sendToken(`[Error: ${e.message}]`);
3336
- }
3337
-
3338
- // Strip think tags from fullOutput before emptiness check
3339
- let fullOutputClean = fullOutput.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
3340
-
3341
- // ── Fill empty sections ──────────────────────────────────────────
3342
- // Detect headings with no body: heading immediately followed by another heading or end
3343
- // e.g. "## 6. Piano d'Azione\n\n## 7. ..." section 6 has no content
3344
- const emptySectionRegex = /^(#{1,4}\s+[^\n]+)\n+(?=#{1,4}\s|$)/gm;
3345
- const emptySections = [];
3346
- let emMatch;
3347
- while ((emMatch = emptySectionRegex.exec(fullOutputClean)) !== null) {
3348
- emptySections.push(emMatch[1].trim());
3349
- }
3350
-
3351
- if (!isCanvasAgent && emptySections.length > 0) {
3352
- // Fill each empty section with a targeted LLM call
3353
- sendToken('\n\n[Completando sezioni mancanti...] ');
3354
- // Pass raw data AND the partial report already written so the model writes in context
3355
- const dataForFill = (toolData || context || '').slice(0, 4000);
3356
- const fillConfig = Object.assign({}, config, { thinking: 'off' });
3357
- for (const emptyHeading of emptySections) {
3358
- sendToken('\n' + emptyHeading + '\n');
3359
- let filledContent = '';
3360
- // Snapshot of the report written so far (trimmed to last 3000 chars for recency)
3361
- const reportSoFar = fullOutputClean.slice(-3000);
3323
+ return stripThink(raw);
3324
+ };
3325
+
3326
+ const useStructuredGen = !isCanvasAgent && !isLiveDataAgent;
3327
+
3328
+ if (isCanvasAgent) {
3329
+ // ── Canvas: single stream, HTML output ────────────────────
3330
+ sendToken('Generating visual report...');
3331
+ try {
3332
+ fullOutput = await streamCall(sysPrompt, userMsg, { max_tokens: 12288, thinking: 'off' }, 180000, null);
3333
+ } catch (e) { /* canvas errors surfaced below */ }
3334
+ fullOutputClean = fullOutput;
3335
+
3336
+ } else if (useStructuredGen) {
3337
+ // ── Structured Generation ─────────────────────────────────
3338
+ // Phase 1: ask for ONLY the section outline (headings list), fast and cheap
3339
+ const structConfig = Object.assign({}, config, { thinking: 'off' });
3340
+ const outlineSys = `You are ${agent}, a specialist AI agent. Today is ${today}. Respond in ${language}.
3341
+ Your task: plan the sections of a complete structured report for this goal: ${task}
3342
+ Instruction: ${stepPrompt}
3343
+
3344
+ Output ONLY a numbered list of section headings — one per line, no body text, no explanation.
3345
+ Use ## prefix for each heading. Maximum 8 sections. Example:
3346
+ ## 1. Executive Summary
3347
+ ## 2. Key Findings
3348
+ ## 3. Analysis`;
3349
+ let outlineRaw = '';
3350
+ try {
3351
+ outlineRaw = await withTimeout(
3352
+ new Promise((res) => {
3353
+ let acc = '';
3354
+ callLLMStream(structConfig, outlineSys, 'List the section headings only.', (tok) => { acc += tok; }, { max_tokens: 300, thinking: 'off' }).then(() => res(acc)).catch(() => res(acc));
3355
+ }),
3356
+ 20000
3357
+ );
3358
+ } catch {}
3359
+
3360
+ // Parse headings: lines starting with ## (or plain numbered lines as fallback)
3361
+ const headingLines = stripThink(outlineRaw)
3362
+ .split('\n')
3363
+ .map(l => l.trim())
3364
+ .filter(l => l.match(/^#{1,4}\s+\S/) || l.match(/^\d+[\.\)]\s+\S/))
3365
+ .map(l => l.match(/^#{1,4}\s+/) ? l : '## ' + l.replace(/^\d+[\.\)]\s+/, ''))
3366
+ .slice(0, 8);
3367
+
3368
+ if (headingLines.length === 0) {
3369
+ // Outline failed — fall back to single stream
3370
+ sendToken('');
3362
3371
  try {
3363
- await withTimeout(
3364
- callLLMStream(
3365
- fillConfig,
3366
- `You are ${agent}. You wrote a report and accidentally left one section empty. Fill in ONLY the missing section — do NOT repeat the heading, do NOT add other sections. Be specific, consistent with the rest of the report, and thorough. Write in ${language}.\n\nSource data:\n${dataForFill}\n\nOverall task: ${task}\n\nReport written so far (for context and style consistency):\n${reportSoFar}`,
3367
- `Write ONLY the body content for this section (heading already printed, do not repeat it):\n${emptyHeading}`,
3368
- (tok) => { filledContent += tok; sendToken(tok); },
3369
- { max_tokens: 1500, thinking: 'off' }
3370
- ),
3371
- 45000
3372
- );
3373
- } catch {}
3374
- if (filledContent.trim()) {
3375
- // Inject the filled content into fullOutputClean right after the heading
3376
- const escapedH = emptyHeading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3377
- fullOutputClean = fullOutputClean.replace(
3378
- new RegExp('(' + escapedH + ')\\n+(#{1,4}\\s)', 'g'),
3379
- '$1\n\n' + filledContent.trim() + '\n\n$2'
3380
- );
3381
- // Also handle trailing empty heading (last section)
3382
- fullOutputClean = fullOutputClean.replace(
3383
- new RegExp('(' + escapedH + ')\\s*$', 'g'),
3384
- '$1\n\n' + filledContent.trim()
3385
- );
3372
+ fullOutput = await streamCall(sysPrompt, userMsg, { max_tokens: 8192, thinking_budget: 2048 }, 120000, sendToken);
3373
+ } catch (e) { sendToken(`[Error: ${e.message}]`); }
3374
+ fullOutputClean = stripThink(fullOutput);
3375
+ } else {
3376
+ // Phase 2: one call per section, each sees the sections already written
3377
+ const dataBlock = [
3378
+ attachmentText ? `## ATTACHED FILE:\n${attachmentText}` : '',
3379
+ toolData ? `## LIVE DATA:\n${toolData}` : '',
3380
+ context ? `## PREVIOUS AGENTS OUTPUT:\n${context}` : '',
3381
+ ].filter(Boolean).join('\n\n');
3382
+ const sectionParts = [];
3383
+ for (let si = 0; si < headingLines.length; si++) {
3384
+ const heading = headingLines[si];
3385
+ const writtenSoFar = sectionParts.join('\n\n');
3386
+ const secSys = `You are ${agent}, a specialist AI agent inside NHA Studio. Today is ${today}. Respond entirely in ${language}.
3387
+
3388
+ ## OVERALL WORKFLOW GOAL:
3389
+ ${task}
3390
+
3391
+ CRITICAL RULES:
3392
+ - Write ONLY the body content for the section heading given — do NOT repeat the heading
3393
+ - Do NOT open new headings or sub-sections — write flowing prose + bullet points
3394
+ - Do NOT invent data — use ONLY what is in the DATA block below
3395
+ - Be thorough, specific, and complete — this is one section of an executive report
3396
+
3397
+ ${dataBlock}
3398
+ ${writtenSoFar ? `## REPORT WRITTEN SO FAR (for consistency):\n${writtenSoFar.slice(-3000)}` : ''}`;
3399
+ const secUser = `Write the complete body content for this section (do NOT repeat the heading):\n${heading}`;
3400
+ sendToken('\n\n' + heading + '\n');
3401
+ let secContent = '';
3402
+ try {
3403
+ secContent = await streamCall(secSys, secUser, { max_tokens: 2000, thinking: 'off' }, 60000, sendToken);
3404
+ } catch {}
3405
+ if (secContent.trim()) {
3406
+ sectionParts.push(heading + '\n\n' + secContent.trim());
3407
+ fullOutput += '\n\n' + heading + '\n\n' + secContent.trim();
3408
+ }
3386
3409
  }
3410
+ fullOutputClean = fullOutput.trim();
3387
3411
  }
3412
+
3388
3413
  } else {
3389
- // Remove empty sections if we can't fill them (fallback: no data available)
3390
- fullOutputClean = fullOutputClean.replace(/^(#{1,4}\s+[^\n]+)\n+(?=#{1,4}\s|$)/gm, '');
3414
+ // ── Live-data agents: single stream ───────────────────────
3415
+ sendToken('');
3416
+ try {
3417
+ fullOutput = await streamCall(sysPrompt, userMsg, { max_tokens: 8192, thinking_budget: 2048 }, 120000, sendToken);
3418
+ } catch (e) { sendToken(`[Error: ${e.message}]`); }
3419
+ fullOutputClean = stripThink(fullOutput);
3391
3420
  }
3392
- // Strip trailing lone headings at end of output (truly unfillable)
3421
+
3422
+ // Strip trailing lone headings at end of output (truly unfillable edge cases)
3393
3423
  fullOutputClean = fullOutputClean.replace(/(#{1,4}\s+[^\n]+)\s*$/g, '').trim();
3394
3424
 
3395
3425
  // Fallback: if LLM returned empty and we have tool data, send it directly
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.95';
8
+ export const VERSION = '13.2.97';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -677,11 +677,21 @@ function openCanvasPanel(){
677
677
  var cp = document.getElementById('canvasPanel');
678
678
  if (!cp) return;
679
679
  cp.classList.add('open');
680
- // If no canvas data, show a tip in the frame area
681
- if (!studioState.canvas) {
682
- var cf = document.getElementById('canvasFrame');
683
- if (cf) {
684
- var tip = \x27<!DOCTYPE html><html><body style="background:#0a0a0a;color:#6b7280;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;text-align:center;padding:20px"><div><div style="font-size:32px;margin-bottom:16px">&#9632;</div><div style="font-size:13px;line-height:1.6">Nessun Canvas in questo workflow.<br>Aggiungi <strong style=\x22color:#4ade80\x22>html</strong>, <strong style=\x22color:#4ade80\x22>dashboard</strong> o <strong style=\x22color:#4ade80\x22>visual</strong> al prompt,<br>oppure usa un task con 2+ agenti specialisti.</div></div></body></html>\x27;
680
+ var cf = document.getElementById('canvasFrame');
681
+ if (studioState.canvas) {
682
+ // Re-inject canvas HTML — survives panel close/reopen
683
+ if (cf) cf.srcdoc = studioState.canvas;
684
+ var ct = document.getElementById('canvasTitle');
685
+ if (ct && ct.textContent === 'Canvas') ct.textContent = 'Studio Report';
686
+ } else {
687
+ // Check allCanvasData for current conversation
688
+ var d = getConvCanvasData();
689
+ if (d.canvases.length > 0) {
690
+ canvasMode = 'canvas';
691
+ canvasIdx = d.canvases.length - 1;
692
+ renderCanvasPanel();
693
+ } else if (cf) {
694
+ var tip = '<!DOCTYPE html><html><body style="background:#0a0a0a;color:#6b7280;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;text-align:center;padding:20px"><div><div style="font-size:32px;margin-bottom:16px">&#9632;</div><div style="font-size:13px;line-height:1.6">Nessun Canvas in questo workflow.<br>Aggiungi <strong style="color:#4ade80">html</strong>, <strong style="color:#4ade80">dashboard</strong> o <strong style="color:#4ade80">visual</strong> al prompt,<br>oppure usa un task con 2+ agenti specialisti.</div></div></body></html>';
685
695
  cf.srcdoc = tip;
686
696
  }
687
697
  }
@@ -3700,52 +3710,81 @@ function downloadStudioPDF() {
3700
3710
  '.section-title{color:#4f46e5!important}}</style>';
3701
3711
  var pdfHtml = html.replace('</head>', printCss + '</head>');
3702
3712
 
3703
- // Build hidden iframe at 800px (A4 equivalent at 96dpi)
3713
+ // Inject light-mode overrides for PDF dark backgrounds become unreadable on paper
3714
+ var lightOverride = '<style>' +
3715
+ 'body{background:#ffffff!important;color:#1a1a2e!important;padding:28px!important;max-width:760px!important;margin:0 auto!important}' +
3716
+ '.header{background:linear-gradient(135deg,#4f46e5 0%,#06b6d4 100%)!important;-webkit-print-color-adjust:exact!important;color-adjust:exact!important}' +
3717
+ '.header h1,.header p,.meta span{color:#fff!important}' +
3718
+ '.card,.section{background:#f4f6fb!important;border:1px solid #dde1ee!important}' +
3719
+ '.card h3,.section h3,.section-title,.card-label{color:#4f46e5!important}' +
3720
+ '.card p,.section p,ul li,ol li{color:#374151!important}' +
3721
+ '.priority-item{background:#eef0f8!important}' +
3722
+ '.priority-item h4{color:#1a1a2e!important}' +
3723
+ '.priority-item p{color:#374151!important}' +
3724
+ '.source-item{background:#eef0f8!important;border-left-color:#4f46e5!important}' +
3725
+ '.source-item h4{color:#1a1a2e!important}' +
3726
+ '.source-item p{color:#374151!important}' +
3727
+ '.bar-track{background:#e0e4ef!important}' +
3728
+ '.footer{color:#9ca3af!important}' +
3729
+ 'a{color:#4f46e5!important}' +
3730
+ '.section,.card,.priority-item,.source-item,.bar-row{break-inside:avoid;page-break-inside:avoid}' +
3731
+ 'h1,h2,h3,h4{break-after:avoid;page-break-after:avoid}' +
3732
+ '.header{break-after:avoid;page-break-after:avoid}' +
3733
+ '</style>';
3734
+ var pdfHtml2 = html.replace('</head>', lightOverride + '</head>');
3735
+
3736
+ // Build hidden iframe at 794px (A4 width at 96dpi = 210mm)
3704
3737
  var iframe = document.createElement('iframe');
3705
- iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:800px;height:1px;border:none;visibility:hidden';
3738
+ iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:794px;height:1px;border:none;visibility:hidden';
3706
3739
  document.body.appendChild(iframe);
3707
3740
  var ifrDoc = iframe.contentDocument || iframe.contentWindow.document;
3708
- ifrDoc.open(); ifrDoc.write(pdfHtml); ifrDoc.close();
3741
+ ifrDoc.open(); ifrDoc.write(pdfHtml2); ifrDoc.close();
3709
3742
  iframe.onload = function() {
3710
- var body = ifrDoc.body;
3711
- var totalH = Math.max(body.scrollHeight, body.offsetHeight, ifrDoc.documentElement.scrollHeight);
3743
+ var ifrBody = ifrDoc.body;
3744
+ var totalH = Math.max(ifrBody.scrollHeight, ifrBody.offsetHeight, ifrDoc.documentElement.scrollHeight);
3712
3745
  iframe.style.height = totalH + 'px';
3713
- // Render at 2.5x scale for crisp text
3714
- window.html2canvas(body, {
3715
- scale: 2.5, useCORS: true, allowTaint: true,
3716
- backgroundColor: '#0d0d14',
3717
- width: 800, windowWidth: 800,
3718
- scrollX: 0, scrollY: 0,
3719
- ignoreElements: function(el){ return el.tagName === 'SCRIPT'; }
3746
+ // Scale: 3x on HiDPI screens, minimum 2.5x for sharp text at A4
3747
+ var renderScale = Math.max(2.5, Math.min(3, window.devicePixelRatio * 1.5));
3748
+ window.html2canvas(ifrBody, {
3749
+ scale: renderScale,
3750
+ useCORS: true,
3751
+ allowTaint: true,
3752
+ backgroundColor: '#ffffff',
3753
+ width: 794,
3754
+ windowWidth: 794,
3755
+ scrollX: 0,
3756
+ scrollY: 0,
3757
+ logging: false,
3758
+ imageTimeout: 15000,
3759
+ ignoreElements: function(el){ return el.tagName === 'SCRIPT' || el.tagName === 'NOSCRIPT'; }
3720
3760
  }).then(function(canvas) {
3721
- var imgData = canvas.toDataURL('image/png'); // PNG for crisp text
3722
3761
  var pdf = new window.jspdf.jsPDF({orientation:'portrait', unit:'pt', format:'a4', compress:true});
3723
- var pageW = pdf.internal.pageSize.getWidth(); // 595pt
3724
- var pageH = pdf.internal.pageSize.getHeight(); // 842pt
3725
- var margin = 24; // pt
3762
+ var pageW = pdf.internal.pageSize.getWidth(); // 595.28pt
3763
+ var pageH = pdf.internal.pageSize.getHeight(); // 841.89pt
3764
+ var margin = 28; // pt — ~10mm margins
3726
3765
  var usableW = pageW - margin * 2;
3727
3766
  var usableH = pageH - margin * 2;
3728
- // Scale image to fit page width
3729
- var imgW = usableW;
3730
- var imgH = (canvas.height * usableW) / canvas.width;
3731
3767
 
3732
- // Smart paging: find safe cut points every ~usableH
3733
- // Strategy: render slice by slice, each slice fits one page
3734
- var sliceH = Math.floor(canvas.height * (usableH / imgH)); // canvas px per page
3768
+ // px per rendered page = canvas pixels that map to usableH in pt
3769
+ var pxPerPt = canvas.width / usableW;
3770
+ var sliceH = Math.floor(usableH * pxPerPt); // canvas px per A4 page
3771
+
3735
3772
  var yCanvas = 0;
3736
3773
  var pageNum = 0;
3737
3774
  while (yCanvas < canvas.height) {
3738
3775
  if (pageNum > 0) pdf.addPage();
3739
3776
  var remaining = canvas.height - yCanvas;
3740
3777
  var thisSlice = Math.min(sliceH, remaining);
3741
- // Create a temporary canvas for this slice
3742
3778
  var sliceCanvas = document.createElement('canvas');
3743
3779
  sliceCanvas.width = canvas.width;
3744
3780
  sliceCanvas.height = thisSlice;
3745
3781
  var ctx = sliceCanvas.getContext('2d');
3782
+ ctx.fillStyle = '#ffffff';
3783
+ ctx.fillRect(0, 0, sliceCanvas.width, sliceCanvas.height);
3746
3784
  ctx.drawImage(canvas, 0, yCanvas, canvas.width, thisSlice, 0, 0, canvas.width, thisSlice);
3747
3785
  var sliceData = sliceCanvas.toDataURL('image/png');
3748
- var sliceImgH = (thisSlice * usableW) / canvas.width;
3786
+ // Proportional height in pt: thisSlice / pxPerPt
3787
+ var sliceImgH = thisSlice / pxPerPt;
3749
3788
  pdf.addImage(sliceData, 'PNG', margin, margin, usableW, sliceImgH, '', 'FAST');
3750
3789
  yCanvas += thisSlice;
3751
3790
  pageNum++;