nothumanallowed 9.3.15 → 9.3.17

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": "9.3.15",
3
+ "version": "9.3.17",
4
4
  "description": "NotHumanAllowed — 38 AI agents + 58 tools + browser automation + web search. Streaming chat, headless Chrome CDP, multi-conversation, export. Gmail, Calendar, Drive, GitHub, Notion, Slack. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -233,6 +233,26 @@ export async function cmdUI(args) {
233
233
 
234
234
  // ── API Routes ────────────────────────────────────────────────────
235
235
 
236
+ // GET /api/screenshots/:filename — serve saved screenshots from disk
237
+ if (method === 'GET' && pathname.startsWith('/api/screenshots/')) {
238
+ const ssName = pathname.split('/').pop();
239
+ if (!ssName || ssName.includes('..') || !ssName.endsWith('.jpg')) {
240
+ sendJSON(res, 404, { error: 'not found' });
241
+ logRequest(method, pathname, 404, Date.now() - start);
242
+ return;
243
+ }
244
+ const ssPath = path.join(NHA_DIR, 'screenshots', ssName);
245
+ if (!fs.existsSync(ssPath)) {
246
+ sendJSON(res, 404, { error: 'screenshot not found' });
247
+ logRequest(method, pathname, 404, Date.now() - start);
248
+ return;
249
+ }
250
+ res.writeHead(200, { 'Content-Type': 'image/jpeg', 'Cache-Control': 'public, max-age=86400' });
251
+ res.end(fs.readFileSync(ssPath));
252
+ logRequest(method, pathname, 200, Date.now() - start);
253
+ return;
254
+ }
255
+
236
256
  // GET /api/status
237
257
  if (method === 'GET' && pathname === '/api/status') {
238
258
  sendJSON(res, 200, {
@@ -1001,7 +1021,7 @@ export async function cmdUI(args) {
1001
1021
  if (toolResults.length > 0) {
1002
1022
  // Second LLM call with real tool results — forces the LLM to use actual data
1003
1023
  const toolContext = toolResults.map(t => `[${t.action} result]: ${t.result.slice(0, 3000)}`).join('\n\n');
1004
- 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 based ONLY on the REAL data above. Do NOT invent or fabricate any information. Present the actual results clearly.`;
1024
+ 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. Do NOT output any JSON blocks just natural text.`;
1005
1025
  try {
1006
1026
  fullResponse = await callLLM(config, enrichedSystemPrompt, followUp);
1007
1027
  } catch {
@@ -1219,8 +1239,17 @@ export async function cmdUI(args) {
1219
1239
  quality: 75,
1220
1240
  });
1221
1241
  if (!ssResult.error) {
1222
- sendSSE('screenshot', { base64: ssResult.base64, format: 'jpeg' });
1223
- toolResults.push({ action, result: `Screenshot captured (${Math.round(ssResult.size / 1024)}KB)` });
1242
+ // Save screenshot to disk for persistence across sessions
1243
+ const ssDir = path.join(NHA_DIR, 'screenshots');
1244
+ fs.mkdirSync(ssDir, { recursive: true });
1245
+ const ssFilename = `ss-${Date.now()}.jpg`;
1246
+ fs.writeFileSync(path.join(ssDir, ssFilename), Buffer.from(ssResult.base64, 'base64'));
1247
+
1248
+ sendSSE('screenshot', { base64: ssResult.base64, format: 'jpeg', filename: ssFilename });
1249
+ toolResults.push({ action, result: `Screenshot captured (${Math.round(ssResult.size / 1024)}KB) [file: ${ssFilename}]` });
1250
+ // Store screenshot ref for persistence
1251
+ if (!res._screenshotFiles) res._screenshotFiles = [];
1252
+ res._screenshotFiles.push(ssFilename);
1224
1253
  sendSSE('tool', { action, status: 'done', result: 'Screenshot captured' });
1225
1254
  } else {
1226
1255
  toolResults.push({ action, result: `Error: ${ssResult.message}` });
@@ -1234,14 +1263,20 @@ export async function cmdUI(args) {
1234
1263
  toolResults.push({ action, result: resultStr });
1235
1264
  sendSSE('tool', { action, status: 'done', result: typeof resultStr === 'string' ? resultStr.slice(0, 500) : '' });
1236
1265
 
1237
- // If the tool produced a screenshot (web_search with screenshot=true), send it via SSE
1266
+ // If the tool produced a screenshot (web_search with screenshot=true), send it via SSE and save to disk
1238
1267
  if (resultStr.includes('[Screenshot of results captured')) {
1239
1268
  try {
1240
1269
  const be = await import('../services/browser-engine.mjs');
1241
1270
  if (be.isBrowserRunning()) {
1242
1271
  const ssResult = await be.browserScreenshot({ fullPage: false, format: 'jpeg', quality: 75 });
1243
1272
  if (!ssResult.error) {
1244
- sendSSE('screenshot', { base64: ssResult.base64, format: 'png' });
1273
+ const ssDir = path.join(NHA_DIR, 'screenshots');
1274
+ fs.mkdirSync(ssDir, { recursive: true });
1275
+ const ssFilename = `ss-${Date.now()}.jpg`;
1276
+ fs.writeFileSync(path.join(ssDir, ssFilename), Buffer.from(ssResult.base64, 'base64'));
1277
+ sendSSE('screenshot', { base64: ssResult.base64, format: 'jpeg', filename: ssFilename });
1278
+ if (!res._screenshotFiles) res._screenshotFiles = [];
1279
+ res._screenshotFiles.push(ssFilename);
1245
1280
  }
1246
1281
  }
1247
1282
  } catch { /* screenshot send failed */ }
@@ -1256,23 +1291,31 @@ export async function cmdUI(args) {
1256
1291
  let finalResponse = fullResponse;
1257
1292
  if (toolResults.length > 0) {
1258
1293
  const toolContext = toolResults.map(t => `[${t.action} result]: ${t.result.slice(0, 3000)}`).join('\n\n');
1259
- const followUp = `The user asked: "${msg}"\n\nI executed these tools and got REAL results:\n\n${toolContext}\n\nNow respond to the user based ONLY on the REAL data above. Present the actual results clearly.`;
1294
+ 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. Do NOT output any JSON blocks — just natural text.`;
1260
1295
  sendSSE('tool_synthesis', {});
1261
1296
  try {
1262
1297
  finalResponse = await callLLMStream(config, enrichedPrompt, followUp, (chunk) => {
1263
1298
  sendSSE('token', { content: chunk });
1264
1299
  });
1300
+ // Strip any JSON blocks the LLM might have emitted anyway
1301
+ finalResponse = finalResponse.replace(/```json[\s\S]*?```/g, '').trim();
1265
1302
  } catch {
1266
1303
  finalResponse = toolResults.map(t => `${t.action}: ${t.result}`).join('\n\n');
1267
1304
  }
1268
1305
  }
1269
1306
 
1270
- // Persist to conversation
1307
+ // Persist to conversation (append screenshot references so they survive reload)
1271
1308
  if (convId) {
1272
1309
  try {
1310
+ let persistedResponse = finalResponse;
1311
+ const ssFiles = res._screenshotFiles || [];
1312
+ if (ssFiles.length > 0) {
1313
+ const ssRefs = ssFiles.map(f => `\n![Screenshot](/api/screenshots/${f})`).join('');
1314
+ persistedResponse = finalResponse + ssRefs;
1315
+ }
1273
1316
  const conv = loadConversation(convId);
1274
1317
  if (conv) {
1275
- addMessages(conv, msg, finalResponse);
1318
+ addMessages(conv, msg, persistedResponse);
1276
1319
  }
1277
1320
  } catch {}
1278
1321
  }
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 = '9.3.15';
8
+ export const VERSION = '9.3.17';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -405,7 +405,8 @@ function renderMessages(){
405
405
  var h='';chatHistory.forEach(function(m){
406
406
  var raw=m.content||'';
407
407
  var imgs=[];var idx=0;
408
- var safe=raw.replace(/!\\[([^\\]]*)\\]\\((data:image\\/[a-z]+;base64,[A-Za-z0-9+\\/=]+)\\)/g,function(_,alt,src){var ph='__IMG'+idx+'__';imgs.push({ph:ph,alt:alt,src:src});idx++;return ph;});
408
+ // Match both base64 inline images and /api/screenshots/ URLs
409
+ var safe=raw.replace(/!\\[([^\\]]*)\\]\\(((?:data:image\\/[a-z]+;base64,[A-Za-z0-9+\\/=]+|\\/api\\/screenshots\\/[a-zA-Z0-9._-]+))\\)/g,function(_,alt,src){var ph='__IMG'+idx+'__';imgs.push({ph:ph,alt:alt,src:src});idx++;return ph;});
409
410
  var content=esc(safe);
410
411
  for(var i=0;i<imgs.length;i++){content=content.replace(imgs[i].ph,'<img class="screenshot-preview" alt="'+esc(imgs[i].alt)+'" src="'+imgs[i].src+'">');}
411
412
  h+='<div class="msg msg--'+esc(m.role)+'"><div class="msg__label">'+esc(m.role==='user'?'You':'NHA')+'</div><div class="msg__bubble">'+content+'</div></div>';
@@ -539,8 +540,10 @@ function sendChat(){
539
540
  }
540
541
  if(currentEvent==='screenshot'&&data.base64){
541
542
  if(!chatHistory[streamIdx]._screenshots)chatHistory[streamIdx]._screenshots=[];
542
- chatHistory[streamIdx]._screenshots.push('data:image/'+(data.format||'png')+';base64,'+data.base64);
543
- chatHistory[streamIdx].content+='\\n![Screenshot](data:image/'+(data.format||'png')+';base64,'+data.base64+')\\n';
543
+ // Use file URL if available (persists across sessions), fall back to base64
544
+ var ssUrl=data.filename?'/api/screenshots/'+data.filename:'data:image/'+(data.format||'jpeg')+';base64,'+data.base64;
545
+ chatHistory[streamIdx]._screenshots.push(ssUrl);
546
+ chatHistory[streamIdx].content+='\\n![Screenshot]('+ssUrl+')\\n';
544
547
  renderMessages();
545
548
  }
546
549
  if(currentEvent==='tool_synthesis'){chatHistory[streamIdx].content='';renderMessages();}