nothumanallowed 14.1.57 → 14.1.59

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.57",
3
+ "version": "14.1.59",
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.57';
8
+ export const VERSION = '14.1.59';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -96,7 +96,92 @@ async function persistGmailMessageToDb(config, msg) {
96
96
  }
97
97
 
98
98
  export function register(router) {
99
- // ── Gmail ──────────────────────────────────────────────────────────────
99
+ // ── Gmail DB-first routes ──────────────────────────────────────────────
100
+
101
+ // GET /api/gmail/messages — reads from local SQLite DB (fast, offline)
102
+ router.get('/api/gmail/messages', async (req, res) => {
103
+ try {
104
+ const { SQLITE_AVAILABLE, listMessages } = await import('../../services/email-db.mjs');
105
+ if (!SQLITE_AVAILABLE) return sendJSON(res, 200, { messages: [], total: 0, dbUnavailable: true });
106
+
107
+ const url = new URL(req.url, 'http://localhost');
108
+ const folder = url.searchParams.get('folder') || 'INBOX';
109
+ const limit = parseInt(url.searchParams.get('limit') || '50');
110
+ const offset = parseInt(url.searchParams.get('offset') || '0');
111
+ const search = url.searchParams.get('search') || '';
112
+
113
+ const result = listMessages('google', null, limit, offset, search, folder);
114
+ sendJSON(res, 200, { messages: result.messages ?? [], total: result.total ?? 0, source: 'db' });
115
+ } catch (e) { sendJSON(res, 200, { messages: [], total: 0, error: e.message }); }
116
+ });
117
+
118
+ // POST /api/gmail/sync — fetches from Gmail API and saves to local DB
119
+ router.post('/api/gmail/sync', async (req, res) => {
120
+ try {
121
+ const body = await parseBody(req);
122
+ const config = loadConfig();
123
+
124
+ if (!config.google?.clientId && !config.google?.tokens?.access_token) {
125
+ return sendJSON(res, 200, { synced: 0, authRequired: true });
126
+ }
127
+
128
+ const folder = (body.folder || 'INBOX').toUpperCase();
129
+ const limit = body.limit || 50;
130
+
131
+ const { getAllEmails } = await import('../../services/google-gmail.mjs');
132
+ const emails = await getAllEmails(config, folder, limit);
133
+
134
+ // getAllEmails already calls cacheMessages internally which saves to SQLite DB
135
+ // Also persist via persistGmailMessageToDb for body_html
136
+ for (const msg of emails) {
137
+ await persistGmailMessageToDb(config, msg).catch(() => {});
138
+ }
139
+
140
+ sendJSON(res, 200, { synced: emails.length, folder });
141
+ } catch (e) {
142
+ sendJSON(res, 200, { synced: 0, error: e.message });
143
+ }
144
+ });
145
+
146
+ // GET /api/gmail/message — read single message from DB, fallback to Gmail API
147
+ router.get('/api/gmail/message', async (req, res) => {
148
+ try {
149
+ const url = new URL(req.url, 'http://localhost');
150
+ const id = url.searchParams.get('id');
151
+ if (!id) return sendError(res, 400, 'id required');
152
+
153
+ // Try DB first
154
+ const { getMessage: dbGetMessage } = await import('../../services/email-db.mjs');
155
+ const dbMsg = dbGetMessage(id);
156
+ if (dbMsg && (dbMsg.body_text || dbMsg.body_html)) {
157
+ return sendJSON(res, 200, { message: dbMsg, source: 'db' });
158
+ }
159
+
160
+ // Fallback to Gmail API
161
+ const config = loadConfig();
162
+ const msg = await getMessage(config, id);
163
+ if (msg) {
164
+ persistGmailMessageToDb(config, msg).catch(() => {});
165
+ return sendJSON(res, 200, {
166
+ message: {
167
+ id: msg.id,
168
+ subject: msg.subject,
169
+ from_address: msg.from ? (msg.from.match(/<(.+?)>$/)?.[1] || msg.from) : '',
170
+ from_name: msg.from ? (msg.from.match(/^(.+?)\s*<.+>$/)?.[1]?.replace(/['"]/g, '') || '') : '',
171
+ to_addresses: msg.to || '',
172
+ internal_date: msg.date || '',
173
+ body_text: msg.body || '',
174
+ body_html: msg.bodyHtml || '',
175
+ body_preview: msg.snippet || '',
176
+ },
177
+ source: 'gmail_api',
178
+ });
179
+ }
180
+ sendJSON(res, 200, { message: null });
181
+ } catch (e) { sendError(res, 500, e.message); }
182
+ });
183
+
184
+ // ── Gmail legacy ──────────────────────────────────────────────────────
100
185
 
101
186
  router.get('/api/emails', async (req, res) => {
102
187
  try {
@@ -325,12 +410,12 @@ export function register(router) {
325
410
  const { listMessages } = await import('../../services/email-db.mjs');
326
411
  const url = new URL(req.url, 'http://localhost');
327
412
  const accountId = url.searchParams.get('accountId');
328
- const folder = url.searchParams.get('folder') || 'INBOX';
413
+ const labelId = url.searchParams.get('labelId') || null;
414
+ const folder = url.searchParams.get('folder') || null;
329
415
  const limit = parseInt(url.searchParams.get('limit') || '50');
330
416
  const offset = parseInt(url.searchParams.get('offset') || '0');
331
417
  const search = url.searchParams.get('search') || '';
332
- // listMessages(accountId, labelId, limit, offset, search)
333
- const result = listMessages(accountId, null, limit, offset, search);
418
+ const result = listMessages(accountId, labelId, limit, offset, search, folder);
334
419
  sendJSON(res, 200, { messages: result.messages ?? result, total: result.total ?? 0 });
335
420
  } catch (e) { sendError(res, 500, e.message); }
336
421
  });
@@ -565,13 +565,13 @@ export function getMessageLabels(messageId) {
565
565
 
566
566
  // ── MESSAGE QUERIES ────────────────────────────────────────────────────────
567
567
 
568
- export function listMessages(accountId, labelId, limit, offset, search) {
568
+ export function listMessages(accountId, labelId, limit, offset, search, folder) {
569
569
  const db = getDb();
570
570
  let where = 'm.account_id = ? AND m.permanently_deleted = 0';
571
571
  const params = [accountId];
572
572
 
573
573
  if (labelId) {
574
- // Also include messages saved with matching imap_folder_path in case label link was missing
574
+ // Filter by label — also include messages saved with matching imap_folder_path
575
575
  const lbl = db.prepare('SELECT system_type FROM email_labels WHERE id = ?').get(labelId);
576
576
  const folderFallback = lbl?.system_type ? lbl.system_type.charAt(0).toUpperCase() + lbl.system_type.slice(1) : null;
577
577
  if (folderFallback) {
@@ -581,12 +581,20 @@ export function listMessages(accountId, labelId, limit, offset, search) {
581
581
  where += ' AND EXISTS (SELECT 1 FROM email_message_labels j WHERE j.message_id = m.id AND j.label_id = ?)';
582
582
  params.push(labelId);
583
583
  }
584
+ } else if (folder) {
585
+ // Filter by folder path (used by Gmail — messages stored with imap_folder_path like INBOX, SENT, etc.)
586
+ where += ' AND m.imap_folder_path = ?';
587
+ params.push(folder.toUpperCase());
584
588
  }
585
589
  if (search) {
586
- where += ' AND (m.subject LIKE ? OR m.from_address LIKE ? OR m.from_name LIKE ? OR m.body_preview LIKE ?)';
590
+ where += ' AND (m.subject LIKE ? OR m.from_address LIKE ? OR m.from_name LIKE ? OR m.body_preview LIKE ? OR m.body_text LIKE ?)';
587
591
  const q = `%${search}%`;
588
- params.push(q, q, q, q);
592
+ params.push(q, q, q, q, q);
589
593
  }
594
+
595
+ const countWhere = where;
596
+ const countParams = [...params];
597
+
590
598
  params.push(limit || 50, offset || 0);
591
599
 
592
600
  const rows = db.prepare(`
@@ -601,7 +609,7 @@ export function listMessages(accountId, labelId, limit, offset, search) {
601
609
  LIMIT ? OFFSET ?
602
610
  `).all(...params);
603
611
 
604
- const total = db.prepare(`SELECT COUNT(*) as cnt FROM email_messages m WHERE ${where.replace('LIMIT ? OFFSET ?', '')}`).get(...params.slice(0, -2));
612
+ const total = db.prepare(`SELECT COUNT(*) as cnt FROM email_messages m WHERE ${countWhere}`).get(...countParams);
605
613
 
606
614
  return { messages: rows, total: total?.cnt || 0 };
607
615
  }
@@ -353,17 +353,34 @@ function parseMessage(raw) {
353
353
  }
354
354
 
355
355
  let body = '';
356
+ let bodyHtml = '';
357
+
358
+ // Recursive function to find parts in nested multipart structures
359
+ function findParts(parts) {
360
+ if (!parts) return;
361
+ for (const p of parts) {
362
+ if (p.mimeType === 'text/plain' && p.body?.data && !body) {
363
+ body = Buffer.from(p.body.data, 'base64url').toString('utf-8');
364
+ } else if (p.mimeType === 'text/html' && p.body?.data && !bodyHtml) {
365
+ bodyHtml = Buffer.from(p.body.data, 'base64url').toString('utf-8');
366
+ } else if (p.parts) {
367
+ findParts(p.parts);
368
+ }
369
+ }
370
+ }
371
+
356
372
  if (raw.payload?.body?.data) {
357
- body = Buffer.from(raw.payload.body.data, 'base64url').toString('utf-8');
373
+ // Single-part message
374
+ const content = Buffer.from(raw.payload.body.data, 'base64url').toString('utf-8');
375
+ if (raw.payload.mimeType === 'text/html') bodyHtml = content;
376
+ else body = content;
358
377
  } else if (raw.payload?.parts) {
359
- const textPart = raw.payload.parts.find(p => p.mimeType === 'text/plain');
360
- if (textPart?.body?.data) {
361
- body = Buffer.from(textPart.body.data, 'base64url').toString('utf-8');
362
- }
378
+ findParts(raw.payload.parts);
363
379
  }
364
380
 
365
381
  // Extract URLs from body
366
- const urls = (body.match(/https?:\/\/[^\s<>"']+/g) || []).slice(0, 10);
382
+ const textContent = body || bodyHtml.replace(/<[^>]+>/g, '');
383
+ const urls = (textContent.match(/https?:\/\/[^\s<>"']+/g) || []).slice(0, 10);
367
384
 
368
385
  return {
369
386
  id: raw.id,
@@ -373,7 +390,8 @@ function parseMessage(raw) {
373
390
  subject: headers.subject || '(no subject)',
374
391
  date: headers.date || '',
375
392
  snippet: raw.snippet || '',
376
- body: body.slice(0, 5000), // cap for LLM context
393
+ body: body.slice(0, 5000),
394
+ bodyHtml,
377
395
  urls,
378
396
  labels: raw.labelIds || [],
379
397
  isUnread: (raw.labelIds || []).includes('UNREAD'),
@@ -446,9 +464,9 @@ async function cacheMessages(messages, folder = 'INBOX') {
446
464
  const insertStmt = db.prepare(`
447
465
  INSERT OR REPLACE INTO email_messages
448
466
  (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,
467
+ subject, from_address, from_name, to_addresses, body_text, body_html, body_preview,
450
468
  internal_date, has_attachments, source)
451
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
469
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
452
470
  `);
453
471
  const stateStmt = db.prepare(`
454
472
  INSERT OR REPLACE INTO email_message_state (message_id, is_read, is_starred)
@@ -457,15 +475,15 @@ async function cacheMessages(messages, folder = 'INBOX') {
457
475
 
458
476
  const insertAll = db.transaction((msgs) => {
459
477
  for (const msg of msgs) {
460
- const msgId = msg.id; // Use Gmail message ID as primary key (stable, unique)
478
+ const msgId = msg.id;
461
479
  insertStmt.run(
462
480
  msgId, googleAccount.id, folderId, folderKey,
463
- Date.now() + Math.random() * 1000 | 0, // unique uid
481
+ Date.now() + Math.random() * 1000 | 0,
464
482
  msg.id, msg.threadId || msg.id,
465
483
  msg.subject || '(no subject)',
466
484
  extractEmail(msg.from), extractName(msg.from),
467
485
  msg.to || '',
468
- msg.body || '', msg.snippet || '',
486
+ msg.body || '', msg.bodyHtml || '', msg.snippet || '',
469
487
  new Date(msg.date || Date.now()).toISOString(),
470
488
  0, 'gmail_api'
471
489
  );