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.
|
|
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": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
507
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
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': {
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -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(
|
|
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
|
}
|