nothumanallowed 14.1.60 → 14.1.62
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 +1 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/config.mjs +17 -0
- package/src/server/routes/email.mjs +2 -1
- package/src/services/email-db.mjs +24 -5
- package/src/services/email-imap.mjs +6 -4
- package/src/services/google-gmail.mjs +3 -2
- package/src/ui-dist/assets/{index-1TuZveGD.js → index-CWI3H1gn.js} +2 -2
- package/src/ui-dist/index.html +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "14.1.
|
|
3
|
+
"version": "14.1.62",
|
|
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 = '14.1.
|
|
8
|
+
export const VERSION = '14.1.62';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -98,9 +98,26 @@ export function register(router) {
|
|
|
98
98
|
success: true,
|
|
99
99
|
message: newVersion ? `Updated to v${newVersion}` : 'Update completed',
|
|
100
100
|
newVersion,
|
|
101
|
+
restart: true,
|
|
101
102
|
stdout: stdout.trim(),
|
|
102
103
|
stderr: stderr.trim()
|
|
103
104
|
});
|
|
105
|
+
|
|
106
|
+
// Self-restart: spawn new process with same args, then exit
|
|
107
|
+
// Delay to allow HTTP response to flush
|
|
108
|
+
setTimeout(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const { spawn } = await import('child_process');
|
|
111
|
+
const args = process.argv.slice(1);
|
|
112
|
+
const child = spawn(process.argv[0], args, {
|
|
113
|
+
detached: true,
|
|
114
|
+
stdio: 'ignore',
|
|
115
|
+
env: process.env,
|
|
116
|
+
});
|
|
117
|
+
child.unref();
|
|
118
|
+
} catch { /* ignore spawn errors */ }
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}, 1500);
|
|
104
121
|
} catch (e) {
|
|
105
122
|
const isPermission = e.message?.includes('EACCES') || e.message?.includes('permission denied');
|
|
106
123
|
sendJSON(res, 200, {
|
|
@@ -205,6 +205,7 @@ export function register(router) {
|
|
|
205
205
|
|
|
206
206
|
const url = new URL(req.url, 'http://localhost');
|
|
207
207
|
const folder = url.searchParams.get('folder') || 'inbox';
|
|
208
|
+
const searchQuery = url.searchParams.get('search') || '';
|
|
208
209
|
const limit = parseInt(url.searchParams.get('pageSize') || url.searchParams.get('limit') || '200');
|
|
209
210
|
const offset = parseInt(url.searchParams.get('page') || url.searchParams.get('offset') || '0') * (url.searchParams.get('page') ? limit : 1);
|
|
210
211
|
try {
|
|
@@ -219,7 +220,7 @@ export function register(router) {
|
|
|
219
220
|
: folder.toUpperCase() === 'STARRED' ? 'STARRED'
|
|
220
221
|
: folder.toUpperCase() === 'IMPORTANT' ? 'IMPORTANT'
|
|
221
222
|
: folder;
|
|
222
|
-
const emails = await getAllEmails(config, gmailFolder, limit + offset);
|
|
223
|
+
const emails = await getAllEmails(config, gmailFolder, limit + offset, searchQuery);
|
|
223
224
|
sendJSON(res, 200, { emails: emails.slice(offset, offset + limit), total: emails.length });
|
|
224
225
|
|
|
225
226
|
// Persist fetched emails to local SQLite DB (fire-and-forget, non-blocking)
|
|
@@ -571,13 +571,32 @@ export function listMessages(accountId, labelId, limit, offset, search, folder)
|
|
|
571
571
|
const params = [accountId];
|
|
572
572
|
|
|
573
573
|
if (labelId) {
|
|
574
|
-
// Filter by label —
|
|
574
|
+
// Filter by label — look up the actual folder path from email_folders for this account
|
|
575
575
|
const lbl = db.prepare('SELECT system_type FROM email_labels WHERE id = ?').get(labelId);
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
576
|
+
if (lbl?.system_type) {
|
|
577
|
+
// Find the actual folder(s) in email_folders that match this system_type (folder_type)
|
|
578
|
+
const folders = db.prepare('SELECT path FROM email_folders WHERE account_id = ? AND folder_type = ?').all(accountId, lbl.system_type);
|
|
579
|
+
if (folders.length > 0) {
|
|
580
|
+
// Filter by actual folder paths OR by message_labels join OR by folder_id
|
|
581
|
+
const placeholders = folders.map(() => '?').join(',');
|
|
582
|
+
where += ` AND (m.imap_folder_path IN (${placeholders}) OR EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?))`;
|
|
583
|
+
params.push(...folders.map(f => f.path), labelId);
|
|
584
|
+
} else {
|
|
585
|
+
// No folder found with correct folder_type — also try matching by name pattern
|
|
586
|
+
// Also look for folders whose name/path contains the system_type keyword (handles legacy 'custom' type)
|
|
587
|
+
const foldersByName = db.prepare('SELECT path FROM email_folders WHERE account_id = ? AND (LOWER(path) LIKE ? OR LOWER(name) LIKE ?)').all(accountId, `%${lbl.system_type}%`, `%${lbl.system_type}%`);
|
|
588
|
+
if (foldersByName.length > 0) {
|
|
589
|
+
const placeholders = foldersByName.map(() => '?').join(',');
|
|
590
|
+
where += ` AND (m.imap_folder_path IN (${placeholders}) OR EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?))`;
|
|
591
|
+
params.push(...foldersByName.map(f => f.path), labelId);
|
|
592
|
+
} else {
|
|
593
|
+
// Last resort — LIKE match on imap_folder_path
|
|
594
|
+
where += ' AND (EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?) OR LOWER(m.imap_folder_path) LIKE ?)';
|
|
595
|
+
params.push(labelId, `%${lbl.system_type}%`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
580
598
|
} else {
|
|
599
|
+
// Custom label (no system_type) — only use message_labels join
|
|
581
600
|
where += ' AND EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?)';
|
|
582
601
|
params.push(labelId);
|
|
583
602
|
}
|
|
@@ -148,10 +148,12 @@ function stripQuotedReplies(text) {
|
|
|
148
148
|
return text.split('\n').filter(l => !l.startsWith('>')).join('\n').trim();
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
export async function syncFolder(accountId, folderPath, fullResync, limitMessages = 0) {
|
|
151
|
+
export async function syncFolder(accountId, folderPath, fullResync, limitMessages = 0, folderType = null) {
|
|
152
152
|
const client = await getImapClient(accountId);
|
|
153
153
|
const db = getDb();
|
|
154
154
|
const folderMeta = getFolder(accountId, folderPath);
|
|
155
|
+
// Use passed folderType or existing metadata (fixes first-sync creating folders as 'custom')
|
|
156
|
+
const resolvedFolderType = folderType || resolvedFolderType;
|
|
155
157
|
|
|
156
158
|
// Open mailbox to get uidValidity BEFORE getMailboxLock
|
|
157
159
|
const lock = await client.getMailboxLock(folderPath);
|
|
@@ -271,7 +273,7 @@ export async function syncFolder(accountId, folderPath, fullResync, limitMessage
|
|
|
271
273
|
const hash = contentHash(msgId || '', fromAddr || '', hdr.internalDate);
|
|
272
274
|
|
|
273
275
|
const folderRec = upsertFolder(accountId, folderPath, folderPath,
|
|
274
|
-
|
|
276
|
+
resolvedFolderType, currentUidValidity, newLastUid);
|
|
275
277
|
|
|
276
278
|
const msgDbId = insertMessage({
|
|
277
279
|
account_id: accountId,
|
|
@@ -313,7 +315,7 @@ export async function syncFolder(accountId, folderPath, fullResync, limitMessage
|
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
upsertFolder(accountId, folderPath, folderPath,
|
|
316
|
-
|
|
318
|
+
resolvedFolderType, currentUidValidity, newLastUid);
|
|
317
319
|
|
|
318
320
|
updateLabelCounts(accountId);
|
|
319
321
|
return { synced, lastUid: newLastUid, total: totalMessages };
|
|
@@ -339,7 +341,7 @@ export async function syncAccount(accountId, opts = {}) {
|
|
|
339
341
|
for (const f of toSync) {
|
|
340
342
|
try {
|
|
341
343
|
const limit = opts.full ? 0 : FIRST_SYNC_LIMIT;
|
|
342
|
-
const result = await syncFolder(accountId, f.path, false, limit);
|
|
344
|
+
const result = await syncFolder(accountId, f.path, false, limit, f.folderType);
|
|
343
345
|
totalSynced += result.synced;
|
|
344
346
|
console.log(`[email:sync] ${f.path}: ${result.synced} new messages (total on server: ${result.total})`);
|
|
345
347
|
} catch (err) {
|
|
@@ -99,7 +99,7 @@ export async function getUnreadImportant(config, maxResults = 30) {
|
|
|
99
99
|
* Get ALL emails from a folder (read + unread) for local-first architecture.
|
|
100
100
|
* This replaces getUnreadImportant to ensure ALL emails are downloaded locally.
|
|
101
101
|
*/
|
|
102
|
-
export async function getAllEmails(config, folder = 'INBOX', maxResults = 200) {
|
|
102
|
+
export async function getAllEmails(config, folder = 'INBOX', maxResults = 200, search = '') {
|
|
103
103
|
// Map folder names to Gmail queries
|
|
104
104
|
const folderQueries = {
|
|
105
105
|
'INBOX': 'in:inbox',
|
|
@@ -111,7 +111,8 @@ export async function getAllEmails(config, folder = 'INBOX', maxResults = 200) {
|
|
|
111
111
|
'IMPORTANT': 'is:important'
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
let query = folderQueries[folder.toUpperCase()] || `in:${folder.toLowerCase()}`;
|
|
115
|
+
if (search) query += ` ${search}`;
|
|
115
116
|
const messageRefs = await listMessages(config, query, maxResults);
|
|
116
117
|
const messages = [];
|
|
117
118
|
|