nothumanallowed 13.5.127 → 13.5.129

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.5.127",
3
+ "version": "13.5.129",
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": {
@@ -2111,9 +2111,33 @@ export async function cmdUI(args) {
2111
2111
  clean = clean.replace(/\[Screenshot[^\]]*\]/g, '').replace(/!\[.*?\]\(data:image[^)]+\)/g, '').slice(0, 3000);
2112
2112
  return `[${t.action} result]: ${clean.trim() || 'Done.'}`;
2113
2113
  }).join('\n\n');
2114
- const followUp = `The user asked: "${body.message}"\n\nI executed these tools and got REAL results:\n\n${toolContext}\n\nNow respond conversationally based ONLY on the REAL data above. Do NOT output any JSON blocks, base64, or image markdown — just natural text.`;
2114
+ const followUp = `The user asked: "${body.message}"\n\nI executed these tools and got REAL results:\n\n${toolContext}\n\nNow respond to the user conversationally based ONLY on the REAL data above. If the user's request has multiple steps and the first step is done, execute the next step using a JSON tool block. Do NOT embed base64 data or image markdown — just natural text.`;
2115
2115
  try {
2116
2116
  fullResponse = await callLLM(config, enrichedSystemPrompt, followUp);
2117
+
2118
+ // Round 2: execute any tool calls emitted in the synthesis response
2119
+ const { textParts: synthText2, actions: synthActions2 } = parseActions(fullResponse);
2120
+ if (synthActions2.length > 0) {
2121
+ const round2Results = [];
2122
+ for (const { action: a2, params: p2 } of synthActions2) {
2123
+ try {
2124
+ const r2 = await executeTool(a2, p2, config);
2125
+ round2Results.push({ action: a2, result: typeof r2 === 'object' ? JSON.stringify(r2) : String(r2) });
2126
+ } catch (e2) {
2127
+ round2Results.push({ action: a2, result: `Error: ${e2.message}` });
2128
+ }
2129
+ }
2130
+ const round2Context = round2Results.map(t => `[${t.action} result]: ${t.result.slice(0, 2000)}`).join('\n\n');
2131
+ try {
2132
+ const r2Summary = await callLLM(config, enrichedSystemPrompt, `The user asked: "${body.message}"\n\n${toolContext}\n\nRound 2 tool results:\n\n${round2Context}\n\nGive the user a final natural-language summary of everything. Do NOT output JSON blocks.`);
2133
+ fullResponse = synthText2.join('\n').replace(/```json[\s\S]*?```/g, '').trim() + (synthText2.join('').trim() ? '\n\n' : '') + r2Summary.trim();
2134
+ } catch {
2135
+ fullResponse = synthText2.join('\n').replace(/```json[\s\S]*?```/g, '').trim() + '\n\n' + round2Results.map(t => `${t.action}: ${t.result}`).join('\n');
2136
+ }
2137
+ } else {
2138
+ fullResponse = fullResponse.replace(/```json[\s\S]*?```/g, '').trim();
2139
+ }
2140
+
2117
2141
  // Prepend preserved markers so the UI can render canvas/screenshots
2118
2142
  if (preservedMarkers) fullResponse = preservedMarkers + fullResponse;
2119
2143
  } catch {
@@ -2615,17 +2639,50 @@ export async function cmdUI(args) {
2615
2639
  sendSSE('canvas', { markers: preservedMarkers });
2616
2640
  }
2617
2641
 
2618
- const followUp = `The user asked: "${msg}"\n\nI executed these tools and got REAL results:\n\n${toolContext}\n\nNow respond to the user conversationally based ONLY on the REAL data above. Present the results clearly. Do NOT output any JSON blocks, any base64 data, or any image markdown — just natural text. If a screenshot was taken, just mention "Screenshot captured" without embedding it.`;
2642
+ const followUp = `The user asked: "${msg}"\n\nI executed these tools and got REAL results:\n\n${toolContext}\n\nNow respond to the user conversationally based ONLY on the REAL data above. Present the results clearly. If the user's request has multiple steps and the first step is done, execute the next step using a JSON tool block. Do NOT embed base64 data or image markdown — just natural text. If a screenshot was taken, just mention "Screenshot captured" without embedding it.`;
2619
2643
  sendSSE('tool_synthesis', {});
2620
2644
  try {
2621
2645
  finalResponse = await callLLMStream(config, enrichedPrompt, followUp, (chunk) => {
2622
2646
  sendSSE('token', { content: chunk });
2623
2647
  });
2624
2648
  finalResponse = finalResponse
2625
- .replace(/```json[\s\S]*?```/g, '')
2626
2649
  .replace(/!\[.*?\]\(data:image\/[^)]+\)/g, '')
2627
2650
  .replace(/data:image\/[a-z]+;base64,[A-Za-z0-9+/=]{100,}/g, '[image]')
2628
2651
  .trim();
2652
+
2653
+ // Round 2: execute any tool calls emitted in the synthesis response
2654
+ const { textParts: synthText, actions: synthActions } = parseActions(finalResponse);
2655
+ if (synthActions.length > 0) {
2656
+ const round2Results = [];
2657
+ for (const { action: a2, params: p2 } of synthActions) {
2658
+ sendSSE('tool', { action: a2, status: 'executing' });
2659
+ try {
2660
+ const r2 = await executeTool(a2, p2, config);
2661
+ const r2str = typeof r2 === 'object' ? JSON.stringify(r2) : String(r2);
2662
+ round2Results.push({ action: a2, result: r2str });
2663
+ sendSSE('tool', { action: a2, status: 'done', result: r2str.slice(0, 200) });
2664
+ } catch (e2) {
2665
+ round2Results.push({ action: a2, result: `Error: ${e2.message}` });
2666
+ sendSSE('tool', { action: a2, status: 'error', error: e2.message });
2667
+ }
2668
+ }
2669
+ const round2Context = round2Results.map(t => `[${t.action} result]: ${t.result.slice(0, 2000)}`).join('\n\n');
2670
+ const round2Prompt = `${toolContext}\n\nRound 2 tool results:\n\n${round2Context}\n\nNow give the user a final natural-language summary of everything that was done. Do NOT output JSON blocks.`;
2671
+ sendSSE('tool_synthesis', {});
2672
+ try {
2673
+ finalResponse = synthText.join('\n').replace(/```json[\s\S]*?```/g, '').trim();
2674
+ const r2Summary = await callLLMStream(config, enrichedPrompt, `The user asked: "${msg}"\n\n${round2Prompt}`, (chunk) => {
2675
+ sendSSE('token', { content: chunk });
2676
+ });
2677
+ finalResponse = (finalResponse ? finalResponse + '\n\n' : '') + r2Summary.trim();
2678
+ } catch {
2679
+ finalResponse = synthText.join('\n').replace(/```json[\s\S]*?```/g, '').trim() + '\n\n' + round2Results.map(t => `${t.action}: ${t.result}`).join('\n');
2680
+ }
2681
+ } else {
2682
+ // No new tool calls — strip any leftover JSON blocks from display text
2683
+ finalResponse = finalResponse.replace(/```json[\s\S]*?```/g, '').trim();
2684
+ }
2685
+
2629
2686
  // Prepend preserved markers for persistence
2630
2687
  if (preservedMarkers) finalResponse = preservedMarkers + finalResponse;
2631
2688
  } catch {
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.5.127';
8
+ export const VERSION = '13.5.129';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -387,6 +387,18 @@ export function getSystemLabel(accountId, systemType) {
387
387
  return getDb().prepare('SELECT * FROM email_labels WHERE account_id = ? AND system_type = ?').get(accountId, systemType);
388
388
  }
389
389
 
390
+ /** Idempotent — seeds any missing system labels for a single account. */
391
+ export function ensureLabelsForAccount(accountId) {
392
+ const db = getDb();
393
+ const insert = db.prepare(`
394
+ INSERT OR IGNORE INTO email_labels (id, account_id, name, system_type, is_system, color, icon, sort_order, path)
395
+ VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?)
396
+ `);
397
+ for (const lbl of SYSTEM_LABELS) {
398
+ insert.run(randomUUID(), accountId, lbl.name, lbl.system_type, lbl.color, lbl.icon, lbl.sort_order, lbl.system_type);
399
+ }
400
+ }
401
+
390
402
  export function createLabel(accountId, name, color, parentId) {
391
403
  const db = getDb();
392
404
  const id = randomUUID();
@@ -503,8 +515,16 @@ export function listMessages(accountId, labelId, limit, offset, search) {
503
515
  const params = [accountId];
504
516
 
505
517
  if (labelId) {
506
- where += ' AND EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?)';
507
- params.push(labelId);
518
+ // Also include messages saved with matching imap_folder_path in case label link was missing
519
+ const lbl = db.prepare('SELECT system_type FROM email_labels WHERE id = ?').get(labelId);
520
+ const folderFallback = lbl?.system_type ? lbl.system_type.charAt(0).toUpperCase() + lbl.system_type.slice(1) : null;
521
+ if (folderFallback) {
522
+ where += ' AND (EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?) OR m.imap_folder_path = ?)';
523
+ params.push(labelId, folderFallback);
524
+ } else {
525
+ where += ' AND EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?)';
526
+ params.push(labelId);
527
+ }
508
528
  }
509
529
  if (search) {
510
530
  where += ' AND (m.subject LIKE ? OR m.from_address LIKE ? OR m.from_name LIKE ? OR m.body_preview LIKE ?)';
@@ -9,7 +9,7 @@ import { createTransport } from 'nodemailer';
9
9
  import { randomUUID } from 'crypto';
10
10
  import {
11
11
  getAccountCredentials, insertMessage, addMessageToLabel, getSystemLabel,
12
- insertAttachments, getDb,
12
+ ensureLabelsForAccount, insertAttachments, getDb,
13
13
  } from './email-db.mjs';
14
14
  import { createHash } from 'crypto';
15
15
 
@@ -97,6 +97,8 @@ export async function sendEmail(accountId, opts) {
97
97
  // ── Save to local DB as "sent" ────────────────────────────────────────
98
98
  const now = new Date().toISOString();
99
99
  const tid = threadId(opts.inReplyTo || msgId);
100
+ // Ensure system labels exist for this account (first send may precede first sync)
101
+ ensureLabelsForAccount(accountId);
100
102
  const sentLabel = getSystemLabel(accountId, 'sent');
101
103
 
102
104
  const dbId = insertMessage({
@@ -988,15 +988,19 @@ export async function executeTool(action, params, config) {
988
988
  case 'imap_send': {
989
989
  if (!params.accountId || !params.to || !params.subject) return 'accountId, to, subject required.';
990
990
  const { sendEmail: imapSendEmail } = await import('./email-smtp.mjs');
991
- const result = await imapSendEmail(params.accountId, {
992
- to: params.to,
993
- cc: params.cc || null,
994
- subject: params.subject,
995
- bodyHtml: params.bodyHtml || params.body || '',
996
- bodyText: params.bodyText || null,
997
- inReplyTo: params.inReplyTo || null,
998
- });
999
- return `Email sent successfully. Message-ID: ${result.messageId}`;
991
+ try {
992
+ const result = await imapSendEmail(params.accountId, {
993
+ to: params.to,
994
+ cc: params.cc || null,
995
+ subject: params.subject,
996
+ bodyHtml: params.bodyHtml || params.body || '',
997
+ bodyText: params.bodyText || null,
998
+ inReplyTo: params.inReplyTo || null,
999
+ });
1000
+ return `✅ Email sent and saved to Sent folder. Message-ID: ${result.messageId}`;
1001
+ } catch (e) {
1002
+ return `❌ SEND FAILED — the email was NOT delivered. Error: ${e.message}. Tell the user the send failed and show the exact error.`;
1003
+ }
1000
1004
  }
1001
1005
 
1002
1006
  case 'imap_mark_read': {
@@ -1016,15 +1020,19 @@ export async function executeTool(action, params, config) {
1016
1020
  let refs = [];
1017
1021
  try { refs = JSON.parse(orig.references_list || '[]'); } catch {}
1018
1022
  if (orig.message_id) refs.push(orig.message_id);
1019
- const result = await imapSendReply(params.accountId, {
1020
- to: orig.from_address,
1021
- cc: params.cc || null,
1022
- subject: replySubject,
1023
- bodyHtml: params.bodyHtml,
1024
- inReplyTo: orig.message_id || null,
1025
- references: refs,
1026
- });
1027
- return `Reply sent to ${orig.from_address}. Message-ID: ${result.messageId}`;
1023
+ try {
1024
+ const result = await imapSendReply(params.accountId, {
1025
+ to: orig.from_address,
1026
+ cc: params.cc || null,
1027
+ subject: replySubject,
1028
+ bodyHtml: params.bodyHtml,
1029
+ inReplyTo: orig.message_id || null,
1030
+ references: refs,
1031
+ });
1032
+ return `✅ Reply sent to ${orig.from_address} and saved to Sent folder. Message-ID: ${result.messageId}`;
1033
+ } catch (e) {
1034
+ return `❌ SEND FAILED — reply was NOT delivered. Error: ${e.message}. Tell the user the send failed and show the exact error.`;
1035
+ }
1028
1036
  }
1029
1037
 
1030
1038
  case 'imap_thread': {
@@ -1089,8 +1097,12 @@ export async function executeTool(action, params, config) {
1089
1097
  const applyVars = (str, vars) => Object.entries(vars).reduce((s, [k, v]) => s.split('[' + k + ']').join(v || ''), str);
1090
1098
  const subject = applyVars(tpl.subject, params.vars);
1091
1099
  const html = applyVars(tpl.html, params.vars);
1092
- const result = await imapSendTpl(params.accountId, { to: params.to, subject, bodyHtml: html });
1093
- return `Template email "${params.templateId}" sent to ${params.to}. Message-ID: ${result.messageId}`;
1100
+ try {
1101
+ const result = await imapSendTpl(params.accountId, { to: params.to, subject, bodyHtml: html });
1102
+ return `✅ Template email "${params.templateId}" sent to ${params.to} and saved to Sent folder. Message-ID: ${result.messageId}`;
1103
+ } catch (e) {
1104
+ return `❌ SEND FAILED — email was NOT delivered. Error: ${e.message}. Tell the user the send failed and show the exact error.`;
1105
+ }
1094
1106
  }
1095
1107
 
1096
1108
  case 'imap_bulk_send': {
@@ -1639,7 +1639,11 @@ function emailSend() {
1639
1639
  }).then(function(r) {
1640
1640
  if (r.ok) {
1641
1641
  if (status) { status.textContent = 'Sent!'; status.style.color = 'var(--green)'; }
1642
- setTimeout(emailCloseCompose, 800);
1642
+ setTimeout(function() {
1643
+ emailCloseCompose();
1644
+ // Reload current label to show the sent message if we are on Sent
1645
+ emailLoadMessages();
1646
+ }, 800);
1643
1647
  } else {
1644
1648
  if (status) { status.textContent = r.error || 'Error'; status.style.color = 'var(--red)'; }
1645
1649
  }