nothumanallowed 15.1.50 → 15.1.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": "15.1.50",
3
+ "version": "15.1.51",
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": {
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 = '15.1.50';
8
+ export const VERSION = '15.1.51';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -425,6 +425,69 @@ export function register(router) {
425
425
  }
426
426
  }
427
427
 
428
+ // ── Auto-chain: search → read → attachment_read ──────────────────────
429
+ // When the user wants to read an attachment of a specific email, the
430
+ // model often emits the first search but then gets stuck saying "Let
431
+ // me read the full email" without actually emitting the follow-up
432
+ // tool. We chain them server-side: parse the imap_search results,
433
+ // pick the best subject match, fetch it with imap_read, then read
434
+ // the first attachment if wantsReadAttachment is on.
435
+ if (wantsReadAttachment) {
436
+ try {
437
+ // Collect candidate messages from all imap_search results we already
438
+ // executed in this turn. Format of imap_search output:
439
+ // [id_xxx] [UNREAD] From: name | Subject | date\n preview...
440
+ const candidates = new Map(); // id → { id, subject }
441
+ for (const tr of toolResults) {
442
+ if (tr.action !== 'imap_search' || typeof tr.result !== 'string') continue;
443
+ const lineRe = /\[([a-zA-Z0-9_-]{6,})\][^\n]*?\|\s*([^|]{2,200})\s*\|/g;
444
+ let m;
445
+ while ((m = lineRe.exec(tr.result)) !== null) {
446
+ if (!candidates.has(m[1])) candidates.set(m[1], { id: m[1], subject: m[2].trim() });
447
+ }
448
+ }
449
+ if (candidates.size > 0) {
450
+ // Rank by token-overlap with the identifiers we extracted from
451
+ // the user message.
452
+ const norm = (s) => String(s || '').toLowerCase()
453
+ .normalize('NFD').replace(/[̀-ͯ]/g, '')
454
+ .replace(/[^a-z0-9\s./_-]/g, ' ')
455
+ .split(/\s+/).filter(t => t.length > 2);
456
+ const userTokens = norm(msg);
457
+ const ranked = [...candidates.values()].map(c => {
458
+ const subjTokens = new Set(norm(c.subject));
459
+ const score = userTokens.filter(t => subjTokens.has(t)).length;
460
+ return { ...c, score };
461
+ }).sort((a, b) => b.score - a.score);
462
+ const best = ranked[0];
463
+ if (best && best.score >= 1) {
464
+ sse('tool', { action: 'imap_read', status: 'executing' });
465
+ try {
466
+ const readResult = await executeTool('imap_read', { messageId: best.id }, config);
467
+ toolResults.push({ action: 'imap_read', result: readResult });
468
+ sse('tool', { action: 'imap_read', status: 'done', result: String(readResult).slice(0, 500) });
469
+ // If the read result lists at least one attachment, fetch it.
470
+ const attMatch = String(readResult).match(/ATTACHMENTS \(\d+\)/);
471
+ if (attMatch) {
472
+ sse('tool', { action: 'imap_attachment_read', status: 'executing' });
473
+ try {
474
+ const attResult = await executeTool('imap_attachment_read', { messageId: best.id, index: 1 }, config);
475
+ toolResults.push({ action: 'imap_attachment_read', result: attResult });
476
+ sse('tool', { action: 'imap_attachment_read', status: 'done', result: String(attResult).slice(0, 500) });
477
+ } catch (e) {
478
+ toolResults.push({ action: 'imap_attachment_read', result: `Error: ${e.message}` });
479
+ sse('tool', { action: 'imap_attachment_read', status: 'error', error: e.message });
480
+ }
481
+ }
482
+ } catch (e) {
483
+ toolResults.push({ action: 'imap_read', result: `Error: ${e.message}` });
484
+ sse('tool', { action: 'imap_read', status: 'error', error: e.message });
485
+ }
486
+ }
487
+ }
488
+ } catch { /* chain best-effort — fall through to synthesis */ }
489
+ }
490
+
428
491
  // Capture the pre-synthesis prose (what the model said in Round 1).
429
492
  // We will combine this with the synthesis output so the user keeps
430
493
  // BOTH — currently the UI was overwriting Round 1 with Round 2 alone,
@@ -458,8 +521,8 @@ export function register(router) {
458
521
  }
459
522
  };
460
523
  const toolContext = toolResults.map(t => `[${t.action} result]:\n${cleanResult(t.action, t.result)}`).join('\n\n---\n\n');
461
- const synthesisPrompt = `${enrichedPrompt}\n\n## DATA FROM TOOLS:\n${toolContext}\n\n## STRICT OUTPUT RULES:\n- Write ONLY plain prose or markdown (headers, bullets, bold)\n- NEVER use \`\`\`json, \`\`\`data, or any fenced code block containing data\n- NEVER output raw JSON, arrays, or objects\n- Format numbers/prices as plain text (e.g. "Bitcoin: $103,000")\n- Be concise and human-readable`;
462
- const synthesisMsg = `${effectiveMsg}\n\nAnswer using ONLY the data above. Plain text/markdown only — zero JSON, zero code blocks.`;
524
+ const synthesisPrompt = `${enrichedPrompt}\n\n## DATA FROM TOOLS (already executed — do NOT plan to call them again):\n${toolContext}\n\n## STRICT OUTPUT RULES:\n- The tools above HAVE ALREADY RUN. Their output is the ground truth.\n- NEVER say "Let me read…", "I'll search…", "I will fetch…", "leggerò", "cercherò", "ti dirò" — those tools are already done.\n- Answer the user's question DIRECTLY using the data above. If a specific detail (delivery date, item, total, etc.) is in the data, quote it verbatim.\n- If the data does not contain the answer, state plainly what is missing — do not announce intent to look further.\n- Write ONLY plain prose or markdown (headers, bullets, bold). NEVER use \`\`\`json or any fenced code block.\n- Format numbers/prices as plain text. Be concise and human-readable.`;
525
+ const synthesisMsg = `${effectiveMsg}\n\nThe tools have already been executed and their results are in the system prompt. Answer the question DIRECTLY using that data. Do NOT announce further actions ("Let me…", "I'll…", "leggerò", "cercherò"). Plain prose/markdown — zero JSON, zero code blocks.`;
463
526
  sse('tool_synthesis', {});
464
527
  // Keep the pre-synthesis prose around. If the synthesis call returns
465
528
  // empty (provider error, content filter, model bailed), we fall back