nothumanallowed 9.2.4 → 9.3.1

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.
@@ -7,6 +7,8 @@
7
7
  * Zero external dependencies — pure Node.js 22.
8
8
  */
9
9
 
10
+ import os from 'os';
11
+
10
12
  import {
11
13
  listMessages,
12
14
  getMessage,
@@ -276,6 +278,55 @@ TOOLS:
276
278
  Returns: title, excerpt, and body text (max 8000 chars). Only fetches text/html/json/xml.
277
279
  Use this when the user provides a specific URL to read, summarize, or analyze.
278
280
 
281
+ --- BROWSER AUTOMATION ---
282
+
283
+ 49. browser_open(url: string, waitForLoad?: boolean)
284
+ Open a URL in a headless Chrome browser. Launches Chrome automatically on first use.
285
+ SSRF-protected (blocks private IPs, localhost). Renders JavaScript, SPAs, and dynamic pages.
286
+ Use this when you need to interact with a page (click, type, screenshot) or when fetch_url fails on JS-rendered content.
287
+
288
+ 50. browser_screenshot(fullPage?: boolean, format?: "png"|"jpeg"|"webp", quality?: number, saveTo?: string)
289
+ Capture a screenshot of the current browser page.
290
+ fullPage=true captures the entire scrollable page. saveTo saves to a file path (e.g. "~/screenshot.png").
291
+ Returns base64-encoded image data. Use after browser_open to see what's on screen.
292
+
293
+ 51. browser_click(selector?: string, x?: number, y?: number)
294
+ Click an element on the page by CSS selector or x/y coordinates.
295
+ Examples: selector="#submit-btn", selector="a.nav-link", selector="button:nth-of-type(2)"
296
+ Use coordinates for precise clicking when selector doesn't work.
297
+
298
+ 52. browser_type(text: string, selector?: string, clear?: boolean, delay?: number)
299
+ Type text into an input field. If selector is provided, clicks the element first to focus it.
300
+ clear=true clears existing content before typing. delay=50 types with 50ms delay between keys.
301
+
302
+ 53. browser_extract(selector?: string, mode?: "text"|"html"|"value"|"attribute", attribute?: string, all?: boolean)
303
+ Extract content from the page. Default: extract all text from body.
304
+ selector="h1" extracts the first h1 text. all=true extracts from ALL matching elements.
305
+ mode="value" gets input values. mode="attribute" with attribute="href" gets link URLs.
306
+ mode="html" gets raw HTML of the element.
307
+
308
+ 54. browser_js(code: string)
309
+ Execute arbitrary JavaScript in the browser page context and return the result.
310
+ The code runs inside the page — you have access to document, window, fetch, etc.
311
+ Example: "document.querySelectorAll('.item').length" returns the count of items.
312
+ Use for complex interactions, form filling, or data extraction that other tools can't handle.
313
+
314
+ 55. browser_wait(selector: string, timeout?: number, visible?: boolean)
315
+ Wait for an element to appear on the page. Default timeout: 10 seconds.
316
+ visible=true (default) waits for the element to be visible, not just in DOM.
317
+ Use after clicking or navigating to wait for dynamic content to load.
318
+
319
+ 56. browser_scroll(direction?: "up"|"down"|"top"|"bottom", amount?: number)
320
+ Scroll the page. direction="down" (default) scrolls down 500px. "top"/"bottom" go to extremes.
321
+ amount=1000 scrolls 1000px instead of default 500.
322
+
323
+ 57. browser_key(key: string, ctrl?: boolean, shift?: boolean, alt?: boolean)
324
+ Press a keyboard key. Keys: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Space.
325
+ ctrl=true holds Ctrl (or Cmd on Mac). Example: key="Enter" to submit a form.
326
+
327
+ 58. browser_close()
328
+ Close the browser. Frees resources. Browser auto-closes when NHA exits.
329
+
279
330
  RULES:
280
331
  - For search/read operations, execute immediately and present results conversationally.
281
332
  - For write/send/delete operations (gmail_send, gmail_reply, gmail_delete, calendar_create, calendar_move, calendar_update, contact_delete, task_done, notify_remind), DESCRIBE what you're about to do and include the JSON block so the system can ask the user for confirmation.
@@ -1035,6 +1086,152 @@ export async function executeTool(action, params, config) {
1035
1086
  return `Birthday set for ${contact.name}: ${monthName} ${day}. It will appear in the Birthdays tab.`;
1036
1087
  }
1037
1088
 
1089
+ // ── Browser Automation ────────────────────────────────────────────
1090
+ case 'browser_open': {
1091
+ const be = await import('./browser-engine.mjs');
1092
+ const url = params.url;
1093
+ if (!url) return 'A URL is required.';
1094
+
1095
+ const result = await be.browserOpen(url, {
1096
+ waitForLoad: params.waitForLoad !== false,
1097
+ });
1098
+ if (result.error) return `Browser error: ${result.message}`;
1099
+
1100
+ return `Page loaded: "${result.title}"\nURL: ${result.url}`;
1101
+ }
1102
+
1103
+ case 'browser_screenshot': {
1104
+ const be = await import('./browser-engine.mjs');
1105
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1106
+
1107
+ const saveTo = params.saveTo
1108
+ ? params.saveTo.replace(/^~/, os.homedir())
1109
+ : null;
1110
+
1111
+ const result = await be.browserScreenshot({
1112
+ fullPage: params.fullPage || false,
1113
+ format: params.format || 'png',
1114
+ quality: params.quality,
1115
+ saveTo,
1116
+ });
1117
+ if (result.error) return `Screenshot error: ${result.message}`;
1118
+
1119
+ if (result.savedTo) {
1120
+ return `Screenshot saved to: ${result.savedTo} (${Math.round(result.size / 1024)}KB base64)`;
1121
+ }
1122
+
1123
+ // Return base64 for LLM vision analysis
1124
+ return `Screenshot captured (${Math.round(result.size / 1024)}KB base64 PNG).\n[Base64 data available — use browser_js or browser_extract to analyze page content instead]`;
1125
+ }
1126
+
1127
+ case 'browser_click': {
1128
+ const be = await import('./browser-engine.mjs');
1129
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1130
+
1131
+ const result = await be.browserClick({
1132
+ selector: params.selector,
1133
+ x: params.x,
1134
+ y: params.y,
1135
+ });
1136
+ if (result.error) return `Click error: ${result.message}`;
1137
+
1138
+ return `Clicked: ${result.selector} at (${result.x}, ${result.y})`;
1139
+ }
1140
+
1141
+ case 'browser_type': {
1142
+ const be = await import('./browser-engine.mjs');
1143
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1144
+
1145
+ if (!params.text) return 'Text is required.';
1146
+
1147
+ const result = await be.browserType({
1148
+ text: params.text,
1149
+ selector: params.selector,
1150
+ clear: params.clear || false,
1151
+ delay: params.delay || 0,
1152
+ });
1153
+ if (result.error) return `Type error: ${result.message}`;
1154
+
1155
+ return `Typed ${result.length} chars into ${result.selector}`;
1156
+ }
1157
+
1158
+ case 'browser_extract': {
1159
+ const be = await import('./browser-engine.mjs');
1160
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1161
+
1162
+ const result = await be.browserExtract({
1163
+ selector: params.selector || 'body',
1164
+ mode: params.mode || 'text',
1165
+ attribute: params.attribute,
1166
+ all: params.all || false,
1167
+ });
1168
+ if (result.error) return `Extract error: ${result.message}`;
1169
+
1170
+ return `[${result.selector}] (${result.length} chars):\n${result.content}`;
1171
+ }
1172
+
1173
+ case 'browser_js': {
1174
+ const be = await import('./browser-engine.mjs');
1175
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1176
+
1177
+ if (!params.code) return 'JavaScript code is required.';
1178
+
1179
+ const result = await be.browserEval(params.code);
1180
+ if (result.error) return `JS error: ${result.message}`;
1181
+
1182
+ return `[${result.type}] ${result.result}`;
1183
+ }
1184
+
1185
+ case 'browser_wait': {
1186
+ const be = await import('./browser-engine.mjs');
1187
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1188
+
1189
+ if (!params.selector) return 'A CSS selector is required.';
1190
+
1191
+ const result = await be.browserWaitFor(params.selector, {
1192
+ timeout: params.timeout || 10000,
1193
+ visible: params.visible !== false,
1194
+ });
1195
+ if (result.error) return `Wait failed: ${result.message}`;
1196
+
1197
+ return `Element found: ${result.selector} (${result.elapsed}ms)`;
1198
+ }
1199
+
1200
+ case 'browser_scroll': {
1201
+ const be = await import('./browser-engine.mjs');
1202
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1203
+
1204
+ const result = await be.browserScroll({
1205
+ direction: params.direction || 'down',
1206
+ amount: params.amount || 500,
1207
+ });
1208
+ if (result.error) return `Scroll error: ${result.message}`;
1209
+
1210
+ return `Scrolled ${result.direction}`;
1211
+ }
1212
+
1213
+ case 'browser_key': {
1214
+ const be = await import('./browser-engine.mjs');
1215
+ if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
1216
+
1217
+ if (!params.key) return 'A key name is required (e.g. Enter, Tab, Escape).';
1218
+
1219
+ const result = await be.browserKeyPress(params.key, {
1220
+ ctrl: params.ctrl,
1221
+ shift: params.shift,
1222
+ alt: params.alt,
1223
+ });
1224
+ if (result.error) return `Key error: ${result.message}`;
1225
+
1226
+ return `Pressed: ${result.key}`;
1227
+ }
1228
+
1229
+ case 'browser_close': {
1230
+ const be = await import('./browser-engine.mjs');
1231
+ const result = await be.browserClose();
1232
+ return result.message;
1233
+ }
1234
+
1038
1235
  // ── Web Search & Fetch ──────────────────────────────────────────────
1039
1236
  case 'web_search': {
1040
1237
  const wt = await import('./web-tools.mjs');
@@ -95,6 +95,11 @@ input:focus,textarea:focus{border-color:var(--green3)}
95
95
  .msg--assistant .msg__bubble{background:var(--greendim);border:1px solid var(--green3);border-radius:8px 8px 8px 2px;padding:10px 14px;max-width:85%;color:var(--text);white-space:pre-wrap;word-wrap:break-word}
96
96
  .msg__label{font-size:10px;color:var(--dim);margin-bottom:2px}
97
97
  .msg--thinking{color:var(--dim);font-style:italic}
98
+ .tool-indicator{display:inline-block;padding:2px 8px;margin:2px 0;border-radius:4px;font-size:11px;background:var(--bg3);border:1px solid var(--border)}
99
+ .tool-indicator--browser{border-color:#9c27b0;color:#ce93d8}
100
+ .tool-indicator--web{border-color:var(--cyan);color:var(--cyan)}
101
+ .tool-indicator--email{border-color:var(--green3);color:var(--green)}
102
+ .screenshot-preview{max-width:100%;border-radius:var(--r);margin:8px 0;border:1px solid var(--border)}
98
103
  .chat__bar{display:flex;gap:8px;padding:10px 0 0 0;border-top:1px solid var(--border);flex-shrink:0}
99
104
  .chat__input{flex:1;resize:none;min-height:40px;max-height:100px;padding:10px 14px}
100
105
  .chat__send{background:var(--green3);color:var(--bg);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
@@ -372,11 +377,16 @@ function exportConvMd(){if(!activeConvId)return;window.open(API+'/api/conversati
372
377
  function renderMessages(){
373
378
  var el=document.getElementById('chatMessages');if(!el)return;
374
379
  if(chatHistory.length===0){
375
- el.innerHTML='<div class="chat__empty"><div class="chat__empty-title">NHA Chat</div><div>Personal Operations Assistant — Streaming + Web Search</div><div class="chat__empty-hint">Try: Show my unread emails / Search the web for React 19 / What is on my calendar?</div></div>';
380
+ el.innerHTML='<div class="chat__empty"><div class="chat__empty-title">NHA Chat</div><div>Personal Operations Assistant — Streaming + Web Search + Browser</div><div class="chat__empty-hint">Try: Show my unread emails / Search the web for React 19 / Open google.com and take a screenshot</div></div>';
376
381
  return;
377
382
  }
378
383
  var h='';chatHistory.forEach(function(m){
379
- h+='<div class="msg msg--'+esc(m.role)+'"><div class="msg__label">'+esc(m.role==='user'?'You':'NHA')+'</div><div class="msg__bubble">'+esc(m.content)+'</div></div>';
384
+ var raw=m.content||'';
385
+ var imgs=[];var idx=0;
386
+ 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;});
387
+ var content=esc(safe);
388
+ 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+'">');}
389
+ 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>';
380
390
  });
381
391
  el.innerHTML=h;el.scrollTop=el.scrollHeight;
382
392
  }
@@ -491,12 +501,21 @@ function sendChat(){
491
501
  if(el){var msgs=el.querySelectorAll('.msg');var last=msgs[msgs.length-1];if(last){var bub=last.querySelector('.msg__bubble');if(bub)bub.textContent=chatHistory[streamIdx].content;}el.scrollTop=el.scrollHeight;}
492
502
  }
493
503
  if(currentEvent==='tool'){
494
- var indicator=data.status==='executing'?'['+data.action+' executing...]':'['+data.action+': '+data.status+']';
504
+ var toolLabels={browser_open:'Opening page',browser_screenshot:'Taking screenshot',browser_click:'Clicking element',browser_type:'Typing text',browser_extract:'Extracting content',browser_js:'Running JavaScript',browser_wait:'Waiting for element',browser_scroll:'Scrolling page',browser_key:'Pressing key',browser_close:'Closing browser',web_search:'Searching the web',fetch_url:'Fetching URL',gmail_list:'Searching emails',gmail_read:'Reading email',gmail_send:'Sending email',calendar_today:'Loading calendar',calendar_create:'Creating event'};
505
+ var label=toolLabels[data.action]||data.action;
506
+ var indicator=data.status==='executing'?'\\u23f3 '+label+'...':'\\u2705 '+label;
507
+ if(data.status==='error')indicator='\\u274c '+label+' failed';
495
508
  chatHistory[streamIdx].content+=indicator+'\\n';
496
509
  renderMessages();
497
510
  }
511
+ if(currentEvent==='screenshot'&&data.base64){
512
+ if(!chatHistory[streamIdx]._screenshots)chatHistory[streamIdx]._screenshots=[];
513
+ chatHistory[streamIdx]._screenshots.push('data:image/'+(data.format||'png')+';base64,'+data.base64);
514
+ chatHistory[streamIdx].content+='\\n![Screenshot](data:image/'+(data.format||'png')+';base64,'+data.base64+')\\n';
515
+ renderMessages();
516
+ }
498
517
  if(currentEvent==='tool_synthesis'){chatHistory[streamIdx].content='';renderMessages();}
499
- if(currentEvent==='done'){chatStreaming=false;if(data.content)chatHistory[streamIdx].content=data.content;renderMessages();loadConvList();}
518
+ if(currentEvent==='done'){chatStreaming=false;if(data.content)chatHistory[streamIdx].content=data.content;var ss=chatHistory[streamIdx]._screenshots;if(ss&&ss.length>0){for(var si=0;si<ss.length;si++){chatHistory[streamIdx].content+='\\n![Screenshot]('+ss[si]+')\\n';}}renderMessages();loadConvList();}
500
519
  if(currentEvent==='error'){chatStreaming=false;chatHistory[streamIdx].content='Error: '+(data.message||'Unknown');renderMessages();}
501
520
  }catch(e){}
502
521
  }