nothumanallowed 14.1.48 → 14.1.49

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": "14.1.48",
3
+ "version": "14.1.49",
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.48';
8
+ export const VERSION = '14.1.49';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -38,16 +38,17 @@ export function register(router) {
38
38
  const offset = parseInt(url.searchParams.get('page') || url.searchParams.get('offset') || '0') * (url.searchParams.get('page') ? limit : 1);
39
39
  try {
40
40
  // getAllEmails returns ALL messages (read + unread) for local-first architecture
41
- // This ensures email like Luca's are downloaded even if read in Gmail
42
- let emails;
43
- if (folder === 'inbox' || folder === 'INBOX') {
44
- const { getAllEmails } = await import('../../services/google-gmail.mjs');
45
- emails = await getAllEmails(config, 'INBOX', limit + offset);
46
- } else {
47
- const gmailQuery = folder === 'sent' ? 'in:sent' : folder === 'spam' ? 'in:spam' : folder === 'trash' ? 'in:trash' : `in:${folder}`;
48
- const refs = await listMessages(config, gmailQuery, limit + offset);
49
- emails = refs; // raw refs — full detail would require N individual getMessage calls
50
- }
41
+ // Works for ALL folders: INBOX, SENT, DRAFTS, SPAM, TRASH, STARRED, IMPORTANT
42
+ const { getAllEmails } = await import('../../services/google-gmail.mjs');
43
+ const gmailFolder = folder.toUpperCase() === 'INBOX' ? 'INBOX'
44
+ : folder.toUpperCase() === 'SENT' ? 'SENT'
45
+ : folder.toUpperCase() === 'DRAFTS' ? 'DRAFTS'
46
+ : folder.toUpperCase() === 'SPAM' ? 'SPAM'
47
+ : folder.toUpperCase() === 'TRASH' ? 'TRASH'
48
+ : folder.toUpperCase() === 'STARRED' ? 'STARRED'
49
+ : folder.toUpperCase() === 'IMPORTANT' ? 'IMPORTANT'
50
+ : folder;
51
+ const emails = await getAllEmails(config, gmailFolder, limit + offset);
51
52
  sendJSON(res, 200, { emails: emails.slice(offset, offset + limit), total: emails.length });
52
53
  } catch (providerErr) {
53
54
  if (providerErr.message?.includes('No mail provider') || providerErr.message?.includes('token')) {
@@ -123,7 +123,7 @@ export async function getAllEmails(config, folder = 'INBOX', maxResults = 200) {
123
123
  }
124
124
 
125
125
  // Cache messages locally - this ensures local-first access
126
- await cacheMessages(messages);
126
+ await cacheMessages(messages, folder);
127
127
  return messages;
128
128
  }
129
129
 
@@ -384,7 +384,7 @@ function parseMessage(raw) {
384
384
 
385
385
  // ── Local Cache ────────────────────────────────────────────────────────────
386
386
 
387
- async function cacheMessages(messages) {
387
+ async function cacheMessages(messages, folder = 'INBOX') {
388
388
  // Cache as JSON files (legacy ops system)
389
389
  fs.mkdirSync(INBOX_DIR, { recursive: true });
390
390
  for (const msg of messages) {
@@ -392,58 +392,89 @@ async function cacheMessages(messages) {
392
392
  fs.writeFileSync(file, JSON.stringify(msg, null, 2), { mode: 0o600 });
393
393
  }
394
394
 
395
- // ENTERPRISE LOCAL-FIRST: Also save to IMAP database for Web UI offline access
395
+ // ENTERPRISE LOCAL-FIRST: Also save to SQLite database for Web UI offline access
396
396
  try {
397
- if (!messages.length) return; // Nothing to cache
397
+ if (!messages.length) return;
398
+
399
+ const { SQLITE_AVAILABLE, getDb, ensureGoogleAccount, upsertFolder } = await import('./email-db.mjs');
400
+ if (!SQLITE_AVAILABLE) return;
398
401
 
399
- const { ensureGoogleAccount, upsertFolder, getSystemLabel, saveMessage } = await import('./email-db.mjs');
402
+ const db = getDb();
400
403
 
401
- // Extract email from first message for account setup
402
- const firstMsg = messages[0];
403
- const accountEmail = extractEmail(firstMsg.to) || 'gmail@account';
404
+ // Determine the user's own email from config (reliable source)
405
+ let accountEmail = 'gmail@account';
406
+ try {
407
+ const os = await import('os');
408
+ const cfgPath = path.join(os.default.homedir(), '.nha', 'config.json');
409
+ if (fs.existsSync(cfgPath)) {
410
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
411
+ accountEmail = cfg.google?.email || cfg.email || accountEmail;
412
+ }
413
+ } catch {}
414
+ // Fallback: extract from 'to' field of first inbox message
415
+ if (accountEmail === 'gmail@account' && messages[0]) {
416
+ accountEmail = extractEmail(messages[0].to) || accountEmail;
417
+ }
404
418
 
405
- // Ensure Google account exists in IMAP database
419
+ // Ensure Google account exists use transaction for atomicity
406
420
  const googleAccount = ensureGoogleAccount({
407
421
  id: 'google',
408
422
  display_name: 'Gmail',
409
423
  email_address: accountEmail,
410
424
  imap_host: 'gmail.googleapis.com',
411
425
  imap_port: 993,
412
- imap_secure: true,
413
426
  is_active: 1
414
427
  });
428
+ if (!googleAccount) return;
429
+
430
+ // Map folder to type and display name
431
+ const folderMap = {
432
+ 'INBOX': { name: 'Inbox', type: 'inbox' },
433
+ 'SENT': { name: 'Sent', type: 'sent' },
434
+ 'DRAFTS': { name: 'Drafts', type: 'drafts' },
435
+ 'SPAM': { name: 'Spam', type: 'spam' },
436
+ 'TRASH': { name: 'Trash', type: 'trash' },
437
+ 'STARRED': { name: 'Starred', type: 'starred' },
438
+ 'IMPORTANT': { name: 'Important', type: 'important' },
439
+ };
440
+ const folderKey = folder.toUpperCase();
441
+ const folderInfo = folderMap[folderKey] || { name: folder, type: 'custom' };
442
+ const folderId = upsertFolder(googleAccount.id, folderKey, folderInfo.name, folderInfo.type, 1, 0);
443
+ if (!folderId) return;
444
+
445
+ // Batch insert all messages in a single transaction (fast + atomic)
446
+ const insertStmt = db.prepare(`
447
+ INSERT OR REPLACE INTO email_messages
448
+ (id, account_id, folder_id, imap_folder_path, uid, message_id, thread_id,
449
+ subject, from_address, from_name, to_addresses, body_text, body_preview,
450
+ internal_date, has_attachments, source)
451
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
452
+ `);
453
+ const stateStmt = db.prepare(`
454
+ INSERT OR REPLACE INTO email_message_state (message_id, is_read, is_starred)
455
+ VALUES (?, ?, ?)
456
+ `);
457
+
458
+ const insertAll = db.transaction((msgs) => {
459
+ for (const msg of msgs) {
460
+ const msgId = msg.id; // Use Gmail message ID as primary key (stable, unique)
461
+ insertStmt.run(
462
+ msgId, googleAccount.id, folderId, folderKey,
463
+ Date.now() + Math.random() * 1000 | 0, // unique uid
464
+ msg.id, msg.threadId || msg.id,
465
+ msg.subject || '(no subject)',
466
+ extractEmail(msg.from), extractName(msg.from),
467
+ msg.to || '',
468
+ msg.body || '', msg.snippet || '',
469
+ new Date(msg.date || Date.now()).toISOString(),
470
+ 0, 'gmail_api'
471
+ );
472
+ stateStmt.run(msgId, msg.isUnread ? 0 : 1, msg.isImportant ? 1 : 0);
473
+ }
474
+ });
415
475
 
416
- if (!googleAccount) {
417
- throw new Error('Failed to create/get Google account');
418
- }
419
-
420
- // Ensure INBOX folder exists for Gmail account
421
- const inboxFolderId = upsertFolder(googleAccount.id, 'INBOX', 'Inbox', 'inbox', 1, 0);
422
-
423
- if (!inboxFolderId) {
424
- throw new Error('Failed to create/get INBOX folder');
425
- }
426
-
427
- // Save each message to IMAP database
428
- for (const msg of messages) {
429
- saveMessage(googleAccount.id, {
430
- folder_id: inboxFolderId,
431
- message_id: msg.id,
432
- thread_id: msg.threadId || msg.id,
433
- subject: msg.subject || '(no subject)',
434
- from_name: extractName(msg.from),
435
- from_address: extractEmail(msg.from),
436
- to_addresses: msg.to || '',
437
- body_text: msg.body || '',
438
- body_preview: msg.snippet || '',
439
- internal_date: new Date(msg.date || Date.now()).toISOString(),
440
- is_read: !msg.isUnread,
441
- is_starred: !!msg.isImportant,
442
- has_attachments: false
443
- });
444
- }
476
+ insertAll(messages);
445
477
  } catch (e) {
446
- // Fallback gracefully if IMAP DB is not available
447
478
  console.warn('[GMAIL CACHE] Failed to save to IMAP database:', e.message);
448
479
  }
449
480
  }