nothumanallowed 12.6.4 → 12.6.6

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": "12.6.4",
3
+ "version": "12.6.6",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools. 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": {
@@ -335,15 +335,40 @@ export async function cmdUI(args) {
335
335
 
336
336
  // POST /api/google/auth — trigger Google OAuth flow from web UI
337
337
  if (method === 'POST' && pathname === '/api/google/auth') {
338
+ // Check if Google credentials are configured first
339
+ const clientId = config.google?.clientId || '';
340
+ const clientSecret = config.google?.clientSecret || '';
341
+ if (!clientId) {
342
+ sendJSON(res, 200, {
343
+ ok: false,
344
+ needsSetup: true,
345
+ message: 'Google OAuth not configured yet.\n\n' +
346
+ 'To connect Google services, you need OAuth credentials:\n\n' +
347
+ '1. Go to https://console.cloud.google.com/apis/credentials\n' +
348
+ '2. Create an OAuth 2.0 Client ID (Desktop app type)\n' +
349
+ '3. Enable Gmail API, Calendar API, Drive API, People API, Tasks API\n' +
350
+ '4. Add authorized redirect URIs: http://127.0.0.1:19847/callback through http://127.0.0.1:19851/callback\n' +
351
+ '5. In the NHA terminal, run:\n' +
352
+ ' nha config set google-client-id YOUR_CLIENT_ID\n' +
353
+ ' nha config set google-client-secret YOUR_CLIENT_SECRET\n' +
354
+ '6. Then click "Connect Google" again.',
355
+ });
356
+ logRequest(method, pathname, 200, Date.now() - start);
357
+ return;
358
+ }
338
359
  try {
339
360
  const { runAuthFlow } = await import('../services/google-oauth.mjs');
340
- // Run auth flow in background — opens browser
341
- runAuthFlow(config).then(success => {
342
- if (success) config._googleConnected = true;
343
- }).catch(() => {});
344
- sendJSON(res, 200, { ok: true, message: 'OAuth flow started. Check the browser window that opened.' });
361
+ const success = await runAuthFlow(config);
362
+ if (success) {
363
+ config._googleConnected = true;
364
+ const freshConfig = await loadConfig();
365
+ Object.assign(config, freshConfig);
366
+ sendJSON(res, 200, { ok: true, message: 'Google connected successfully! You can now use email, calendar, contacts, and Drive.' });
367
+ } else {
368
+ sendJSON(res, 200, { ok: false, message: 'Google OAuth failed. The browser window should have opened at accounts.google.com. If it didn\'t, try running "nha google" from the terminal.' });
369
+ }
345
370
  } catch (e) {
346
- sendJSON(res, 500, { error: e.message });
371
+ sendJSON(res, 500, { error: `Google OAuth error: ${e.message}. Try running "nha google" from the terminal.` });
347
372
  }
348
373
  logRequest(method, pathname, 200, Date.now() - start);
349
374
  return;
@@ -1257,12 +1282,14 @@ export async function cmdUI(args) {
1257
1282
  if (sLines.length > 0) parts.push(`[CONTEXT — ${sLines.length} earlier exchanges]\n${sLines.join('\n')}\n[END CONTEXT]`);
1258
1283
  }
1259
1284
  for (const turn of requestHistory.slice(-RECENT)) {
1260
- parts.push(`${turn.role === 'user' ? '[User]' : '[Assistant]'} ${turn.content.slice(0, 2000)}`);
1285
+ // Use llmContent (file context) when available, otherwise display content
1286
+ const turnContent = turn.llmContent || turn.content;
1287
+ parts.push(`${turn.role === 'user' ? '[User]' : '[Assistant]'} ${turnContent.slice(0, 4000)}`);
1261
1288
  }
1262
1289
  parts.push(`[User] ${body.message}`);
1263
1290
  let userMessage = parts.join('\n\n');
1264
1291
 
1265
- // Inject episodic memory context into the system prompt
1292
+ // Inject episodic memory + cross-conversation memory into the system prompt
1266
1293
  const basePrompt = effectiveSystemPrompt || chatSystemPrompt;
1267
1294
  let enrichedSystemPrompt = basePrompt;
1268
1295
  try {
@@ -1270,6 +1297,37 @@ export async function cmdUI(args) {
1270
1297
  if (memCtx) enrichedSystemPrompt = basePrompt + memCtx;
1271
1298
  } catch { /* memory unavailable */ }
1272
1299
 
1300
+ // Cross-conversation memory — summaries of recent conversations
1301
+ try {
1302
+ const allConvs = listConversations();
1303
+ if (allConvs.length > 1) {
1304
+ const summaries = [];
1305
+ let totalChars = 0;
1306
+ for (const c of allConvs) {
1307
+ if (c.id === (body.conversationId || activeConvId)) continue;
1308
+ if (summaries.length >= 8 || totalChars > 2000) break;
1309
+ if (!c.messages || c.messages.length === 0) continue;
1310
+ const firstUser = c.messages.find(m => m.role === 'user');
1311
+ const lastAssistant = [...c.messages].reverse().find(m => m.role === 'assistant');
1312
+ if (!firstUser) continue;
1313
+ const date = c.updatedAt?.split('T')[0] || '?';
1314
+ const title = c.title !== 'New Chat' ? c.title : firstUser.content.slice(0, 60);
1315
+ let s = `• [${date}] "${title}" (${c.messages.length} msgs)`;
1316
+ s += `\n User: ${firstUser.content.replace(/\s+/g, ' ').slice(0, 120)}`;
1317
+ if (lastAssistant) {
1318
+ const preview = lastAssistant.content.replace(/<think>[\s\S]*?<\/think>/g, '').replace(/\s+/g, ' ').slice(0, 150);
1319
+ s += `\n Result: ${preview}`;
1320
+ }
1321
+ if (totalChars + s.length > 2000) break;
1322
+ summaries.push(s);
1323
+ totalChars += s.length;
1324
+ }
1325
+ if (summaries.length > 0) {
1326
+ enrichedSystemPrompt += `\n\n--- CONVERSATION MEMORY ---\nYou remember these past conversations:\n\n${summaries.join('\n\n')}\n\nUse this to maintain continuity. Never say "I don't have access to previous conversations".\n--- END MEMORY ---`;
1327
+ }
1328
+ }
1329
+ } catch { /* non-critical */ }
1330
+
1273
1331
  // Handle image attachment — vision API
1274
1332
  if (body.imageBase64 && body.imageMimeType) {
1275
1333
  try {
@@ -1362,13 +1420,33 @@ export async function cmdUI(args) {
1362
1420
  const pdfPrompt = body.message || `Read and analyze this PDF document "${body.pdfName}". Extract all text content, summarize key information.`;
1363
1421
  let pdfResponse = '';
1364
1422
 
1365
- if (provider === 'nha') {
1366
- // NHA Free tier: extract text from PDF, then send to Liara chat
1367
- // Decode PDF base64 and extract text content
1423
+ // Step 1: Extract text — try server (pdftotext) first, then local fallback
1424
+ let pdfText = '';
1425
+ try {
1426
+ const extractRes = await fetch('https://nothumanallowed.com/api/v1/tools/extract-pdf', {
1427
+ method: 'POST',
1428
+ headers: { 'Content-Type': 'application/json', 'X-NHA-Client': 'desktop' },
1429
+ body: JSON.stringify({ base64: body.pdfBase64 }),
1430
+ signal: AbortSignal.timeout(30000),
1431
+ });
1432
+ if (extractRes.ok) {
1433
+ const d = await extractRes.json();
1434
+ pdfText = d.text || '';
1435
+ }
1436
+ } catch { /* server unreachable */ }
1437
+ // Local fallback if server extraction failed
1438
+ if (pdfText.length < 20) {
1368
1439
  const pdfBuffer = Buffer.from(body.pdfBase64, 'base64');
1369
- const pdfText = extractTextFromPdf(pdfBuffer);
1440
+ pdfText = extractTextFromPdf(pdfBuffer);
1441
+ }
1442
+
1443
+ // Save extracted text as llmContent so it persists across turns
1444
+ const pdfLlmContent = pdfText.length > 20
1445
+ ? `[PDF: ${body.pdfName}]\n\n${pdfText.slice(0, 12000)}\n\n---\n\nUser question: ${pdfPrompt}`
1446
+ : '';
1447
+
1448
+ if (provider === 'nha') {
1370
1449
  if (!pdfText || pdfText.length < 10) {
1371
- // Fallback: send first page as image to vision model
1372
1450
  const r = await fetch('https://nothumanallowed.com/api/v1/liara/vision', {
1373
1451
  method: 'POST',
1374
1452
  headers: { 'Content-Type': 'application/json' },
@@ -1381,7 +1459,6 @@ export async function cmdUI(args) {
1381
1459
  pdfResponse = 'Could not read this PDF. Try a text-based PDF or use Claude/Gemini for scanned documents.';
1382
1460
  }
1383
1461
  } else {
1384
- // Send extracted text to Liara chat
1385
1462
  const truncatedText = pdfText.slice(0, 12000);
1386
1463
  const r = await fetch('https://nothumanallowed.com/api/v1/liara/chat', {
1387
1464
  method: 'POST',
@@ -1439,7 +1516,8 @@ export async function cmdUI(args) {
1439
1516
  pdfResponse = `PDF reading requires Anthropic (Claude) or Gemini. Your provider "${provider}" does not support native PDF documents.`;
1440
1517
  }
1441
1518
 
1442
- sendJSON(res, 200, { response: pdfResponse });
1519
+ // Return llmContent so frontend can persist the PDF text across turns
1520
+ sendJSON(res, 200, { response: pdfResponse, llmContent: pdfLlmContent || undefined });
1443
1521
  logRequest(method, pathname, 200, Date.now() - start);
1444
1522
  return;
1445
1523
  } catch (e) {
@@ -1449,12 +1527,14 @@ export async function cmdUI(args) {
1449
1527
  }
1450
1528
  }
1451
1529
 
1452
- // Handle text file attachment
1530
+ // Handle text file attachment — include file content as llmContent for persistence
1531
+ let fileLlmContent = '';
1453
1532
  if (body.fileContent && body.fileName) {
1454
1533
  const filePrompt = body.message
1455
1534
  ? `User asks about file "${body.fileName}": ${body.message}\n\nFile content:\n${body.fileContent.slice(0, 8000)}`
1456
1535
  : `Analyze this file "${body.fileName}":\n\n${body.fileContent.slice(0, 8000)}`;
1457
1536
  userMessage = filePrompt;
1537
+ fileLlmContent = filePrompt;
1458
1538
  }
1459
1539
 
1460
1540
  try {
@@ -1551,7 +1631,7 @@ export async function cmdUI(args) {
1551
1631
  } catch { /* non-critical */ }
1552
1632
  try { extractMemory('chat', body.message, fullResponse); } catch { /* non-critical */ }
1553
1633
 
1554
- sendJSON(res, 200, { response: fullResponse, toolResults, actions });
1634
+ sendJSON(res, 200, { response: fullResponse, toolResults, actions, ...(fileLlmContent ? { llmContent: fileLlmContent } : {}) });
1555
1635
  } catch (e) {
1556
1636
  sendJSON(res, 200, { response: null, error: e.message });
1557
1637
  }
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 = '12.6.4';
8
+ export const VERSION = '12.6.6';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -44,9 +44,14 @@ function generatePKCE() {
44
44
  function openBrowser(url) {
45
45
  const platform = os.platform();
46
46
  try {
47
- if (platform === 'darwin') execSync(`open "${url}"`);
48
- else if (platform === 'win32') execSync(`start "" "${url}"`);
49
- else execSync(`xdg-open "${url}"`);
47
+ if (platform === 'darwin') {
48
+ execSync(`open "${url}"`);
49
+ } else if (platform === 'win32') {
50
+ // Use rundll32 — more reliable than 'start' in batch/npm contexts
51
+ execSync(`rundll32 url.dll,FileProtocolHandler "${url}"`);
52
+ } else {
53
+ execSync(`xdg-open "${url}"`);
54
+ }
50
55
  } catch {
51
56
  warn('Could not open browser automatically.');
52
57
  info(`Open this URL manually:\n\n ${url}\n`);
@@ -925,6 +925,11 @@ function sendChat(){
925
925
  clearChatAttach();
926
926
  apiPost('/api/chat',payload).then(function(r){
927
927
  chatHistory.pop();
928
+ // Save llmContent in the user message so file context persists across turns
929
+ if(r&&r.llmContent){
930
+ var lastUser=chatHistory[chatHistory.length-1];
931
+ if(lastUser&&lastUser.role==='user')lastUser.llmContent=r.llmContent;
932
+ }
928
933
  if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
929
934
  else if(r&&r.error){chatHistory.push({role:'assistant',content:'Error: '+r.error})}
930
935
  else{chatHistory.push({role:'assistant',content:'Error: no response'})}
@@ -943,7 +948,7 @@ function sendChat(){
943
948
  chatHistory.push({role:'assistant',content:''});
944
949
  renderMessages();
945
950
  var streamIdx=chatHistory.length-1;
946
- var allHistory=chatHistory.slice(0,-1).map(function(m){return{role:m.role,content:(m.content||'').replace(/!\\[Screenshot\\]\\(data:image\\/[^)]+\\)/g,'[Screenshot taken]')};});
951
+ var allHistory=chatHistory.slice(0,-1).map(function(m){var h={role:m.role,content:(m.content||'').replace(/!\\[Screenshot\\]\\(data:image\\/[^)]+\\)/g,'[Screenshot taken]')};if(m.llmContent)h.llmContent=m.llmContent;return h;});
947
952
  var payload={message:msg,history:allHistory,conversationId:activeConvId,isRetry:isRetry};
948
953
 
949
954
  fetch(API+'/api/chat/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),signal:chatAbortController.signal}).then(function(response){