nothumanallowed 9.2.3 → 9.3.0
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/README.md +96 -21
- package/bin/nha.mjs +4 -2
- package/package.json +2 -2
- package/src/cli.mjs +136 -1
- package/src/commands/chat.mjs +43 -1
- package/src/commands/ui.mjs +24 -0
- package/src/constants.mjs +1 -1
- package/src/services/browser-engine.mjs +1156 -0
- package/src/services/tool-executor.mjs +197 -0
- package/src/services/web-ui.mjs +20 -3
|
@@ -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');
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -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 /
|
|
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
|
-
|
|
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,10 +501,17 @@ 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
|
|
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
|
+
chatHistory[streamIdx].content+='\\n+';base64,'+data.base64+')\\n';
|
|
513
|
+
renderMessages();
|
|
514
|
+
}
|
|
498
515
|
if(currentEvent==='tool_synthesis'){chatHistory[streamIdx].content='';renderMessages();}
|
|
499
516
|
if(currentEvent==='done'){chatStreaming=false;if(data.content)chatHistory[streamIdx].content=data.content;renderMessages();loadConvList();}
|
|
500
517
|
if(currentEvent==='error'){chatStreaming=false;chatHistory[streamIdx].content='Error: '+(data.message||'Unknown');renderMessages();}
|