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 +1 -1
- package/src/commands/ui.mjs +51 -8
- package/src/constants.mjs +1 -1
- package/src/services/web-ui.mjs +6 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "9.3.
|
|
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": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
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
|
|
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`).join('');
|
|
1314
|
+
persistedResponse = finalResponse + ssRefs;
|
|
1315
|
+
}
|
|
1273
1316
|
const conv = loadConversation(convId);
|
|
1274
1317
|
if (conv) {
|
|
1275
|
-
addMessages(conv, msg,
|
|
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.
|
|
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
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
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\\n';
|
|
544
547
|
renderMessages();
|
|
545
548
|
}
|
|
546
549
|
if(currentEvent==='tool_synthesis'){chatHistory[streamIdx].content='';renderMessages();}
|