nothumanallowed 9.7.2 → 9.8.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.
- package/package.json +1 -1
- package/src/commands/ask.mjs +206 -18
- package/src/commands/chat.mjs +482 -64
- package/src/commands/ui.mjs +843 -89
- package/src/constants.mjs +1 -1
- package/src/services/browser-engine.mjs +1240 -0
- package/src/services/conversations.mjs +277 -0
- package/src/services/llm.mjs +120 -89
- package/src/services/tool-executor.mjs +384 -59
- package/src/services/web-tools.mjs +430 -0
- package/src/services/web-ui.mjs +422 -175
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* Zero external dependencies — pure Node.js 22.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
10
13
|
import {
|
|
11
14
|
listMessages,
|
|
12
15
|
getMessage,
|
|
@@ -62,8 +65,6 @@ export const DESTRUCTIVE_ACTIONS = new Set([
|
|
|
62
65
|
'notify_remind',
|
|
63
66
|
'slack_send',
|
|
64
67
|
'github_create_issue',
|
|
65
|
-
'cron_add',
|
|
66
|
-
'cron_remove',
|
|
67
68
|
]);
|
|
68
69
|
|
|
69
70
|
// ── Tool Definitions (for system prompt) ─────────────────────────────────────
|
|
@@ -74,14 +75,16 @@ export const DESTRUCTIVE_ACTIONS = new Set([
|
|
|
74
75
|
*/
|
|
75
76
|
export const TOOL_DEFINITIONS = `
|
|
76
77
|
You have access to the following tools. When the user's message requires an action,
|
|
77
|
-
output
|
|
78
|
+
output one or more fenced JSON blocks:
|
|
78
79
|
|
|
79
80
|
\`\`\`json
|
|
80
81
|
{"action": "<tool_name>", "params": { ... }}
|
|
81
82
|
\`\`\`
|
|
82
83
|
|
|
83
|
-
You
|
|
84
|
-
|
|
84
|
+
You can include multiple JSON blocks in one response for sequential actions.
|
|
85
|
+
You may include conversational text BEFORE, BETWEEN, or AFTER JSON blocks.
|
|
86
|
+
If no action is needed, respond normally without any JSON block.
|
|
87
|
+
CRITICAL: Never output a JSON block as a "suggestion" or "let me try" — every JSON block WILL be executed immediately. Only output a JSON block when you are certain the action should be performed.
|
|
85
88
|
|
|
86
89
|
TOOLS:
|
|
87
90
|
|
|
@@ -265,42 +268,107 @@ TOOLS:
|
|
|
265
268
|
46. birthday_add(name: string, date: string)
|
|
266
269
|
Add or update a birthday for a contact. Name is the contact name (must exist in Google Contacts — creates one if not found). Date is MM-DD (e.g. "04-06" for April 6) or YYYY-MM-DD.
|
|
267
270
|
|
|
271
|
+
--- WEB SEARCH & FETCH ---
|
|
272
|
+
|
|
273
|
+
47. web_search(query: string, deep?: boolean, screenshot?: boolean)
|
|
274
|
+
Search the web using DuckDuckGo. Returns titles, URLs, and snippets.
|
|
275
|
+
Set deep=true to also fetch and extract the top 3 pages' full content (slower but more detailed).
|
|
276
|
+
Set screenshot=true when the user asks for a screenshot/image of the results — this renders results as a visual page and captures a screenshot. ALWAYS set screenshot=true if the user mentions "screenshot", "screen", "immagine", "foto", "mostra", or "vedi" in relation to search results.
|
|
277
|
+
ALWAYS use this for ANY web search request ("search for X", "find X", "look up X", "cerca X").
|
|
278
|
+
Do NOT open Google/Bing in the browser for searches — use this tool instead. It's faster and never gets blocked.
|
|
279
|
+
|
|
280
|
+
48. fetch_url(url: string)
|
|
281
|
+
Fetch a web page and extract its text content. SSRF-protected (blocks private IPs, localhost).
|
|
282
|
+
Returns: title, excerpt, and body text (max 8000 chars). Only fetches text/html/json/xml.
|
|
283
|
+
Use this when the user provides a specific URL to read, summarize, or analyze.
|
|
284
|
+
|
|
285
|
+
--- BROWSER AUTOMATION ---
|
|
286
|
+
|
|
287
|
+
49. browser_open(url: string, waitForLoad?: boolean)
|
|
288
|
+
Open a URL in a headless Chrome browser. Launches Chrome automatically on first use.
|
|
289
|
+
SSRF-protected (blocks private IPs, localhost). Renders JavaScript, SPAs, and dynamic pages.
|
|
290
|
+
Use this when you need to interact with a page (click, type, screenshot) or when fetch_url fails on JS-rendered content.
|
|
291
|
+
WARNING: Do NOT use this to search the web — use web_search instead. Google/Bing block automated browsers with CAPTCHAs.
|
|
292
|
+
|
|
293
|
+
50. browser_screenshot(saveTo?: string)
|
|
294
|
+
Capture a screenshot of the current browser viewport (what's visible on screen).
|
|
295
|
+
saveTo saves to a file path (e.g. "~/screenshot.png"). Returns base64-encoded image.
|
|
296
|
+
ALWAYS use viewport screenshots (the default). Do NOT pass fullPage=true — it produces oversized images.
|
|
297
|
+
Screenshots are automatically compressed as JPEG for efficiency.
|
|
298
|
+
|
|
299
|
+
51. browser_click(text?: string, selector?: string, x?: number, y?: number)
|
|
300
|
+
Click an element on the page by visible text, CSS selector, or x/y coordinates.
|
|
301
|
+
PREFERRED: use text="Rifiuta tutto" or text="Submit" to click buttons/links by their visible label (case-insensitive partial match).
|
|
302
|
+
CSS selector: selector="#submit-btn", selector="a.nav-link"
|
|
303
|
+
Coordinates: x=500, y=300 for precise clicking.
|
|
304
|
+
Always try text first — it works for buttons, links, and any clickable element regardless of CSS structure.
|
|
305
|
+
|
|
306
|
+
52. browser_type(text: string, selector?: string, clear?: boolean, delay?: number)
|
|
307
|
+
Type text into an input field. If selector is provided, clicks the element first to focus it.
|
|
308
|
+
clear=true clears existing content before typing. delay=50 types with 50ms delay between keys.
|
|
309
|
+
|
|
310
|
+
53. browser_extract(selector?: string, mode?: "text"|"html"|"value"|"attribute", attribute?: string, all?: boolean)
|
|
311
|
+
Extract content from the page. Default: extract all text from body.
|
|
312
|
+
selector="h1" extracts the first h1 text. all=true extracts from ALL matching elements.
|
|
313
|
+
mode="value" gets input values. mode="attribute" with attribute="href" gets link URLs.
|
|
314
|
+
mode="html" gets raw HTML of the element.
|
|
315
|
+
|
|
316
|
+
54. browser_js(code: string)
|
|
317
|
+
Execute arbitrary JavaScript in the browser page context and return the result.
|
|
318
|
+
The code runs inside the page — you have access to document, window, fetch, etc.
|
|
319
|
+
Example: "document.querySelectorAll('.item').length" returns the count of items.
|
|
320
|
+
Use for complex interactions, form filling, or data extraction that other tools can't handle.
|
|
321
|
+
|
|
322
|
+
55. browser_wait(selector: string, timeout?: number, visible?: boolean)
|
|
323
|
+
Wait for an element to appear on the page. Default timeout: 10 seconds.
|
|
324
|
+
visible=true (default) waits for the element to be visible, not just in DOM.
|
|
325
|
+
Use after clicking or navigating to wait for dynamic content to load.
|
|
326
|
+
|
|
327
|
+
56. browser_scroll(direction?: "up"|"down"|"top"|"bottom", amount?: number)
|
|
328
|
+
Scroll the page. direction="down" (default) scrolls down 500px. "top"/"bottom" go to extremes.
|
|
329
|
+
amount=1000 scrolls 1000px instead of default 500.
|
|
330
|
+
|
|
331
|
+
57. browser_key(key: string, ctrl?: boolean, shift?: boolean, alt?: boolean)
|
|
332
|
+
Press a keyboard key. Keys: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Space.
|
|
333
|
+
ctrl=true holds Ctrl (or Cmd on Mac). Example: key="Enter" to submit a form.
|
|
334
|
+
|
|
335
|
+
58. browser_close()
|
|
336
|
+
Close the browser. Frees resources. Browser auto-closes when NHA exits.
|
|
337
|
+
|
|
268
338
|
--- SCHEDULED TASKS ---
|
|
269
339
|
|
|
270
|
-
|
|
340
|
+
59. cron_add(schedule: string, prompt: string, agent?: string)
|
|
271
341
|
Create a recurring background task. The daemon executes it automatically.
|
|
272
342
|
Schedule formats: "every 5m", "every 2h", "every monday 9am", "daily 8:30", "at 14:00", "hourly".
|
|
273
|
-
Example: cron_add("every monday 9am", "check open PRs on my GitHub repos", "forge")
|
|
274
343
|
|
|
275
|
-
|
|
344
|
+
60. cron_list()
|
|
276
345
|
List all scheduled background tasks with their status, run count, and last result.
|
|
277
346
|
|
|
278
|
-
|
|
347
|
+
61. cron_remove(index: number)
|
|
279
348
|
Remove a scheduled task by its number (1-based, from cron_list).
|
|
280
349
|
|
|
281
350
|
--- SCREEN & VISION ---
|
|
282
351
|
|
|
283
|
-
|
|
352
|
+
62. screen_capture(monitor?: number)
|
|
284
353
|
Capture a screenshot of the user's desktop screen. Returns the image for visual analysis.
|
|
285
|
-
Use
|
|
286
|
-
Optional: monitor number (default 1, for multi-monitor setups).
|
|
354
|
+
Use when the user asks to look at their screen or analyze something visible.
|
|
287
355
|
|
|
288
|
-
|
|
289
|
-
Capture the screen AND analyze it with
|
|
290
|
-
Example: screen_analyze("what error is shown in the terminal?")
|
|
356
|
+
63. screen_analyze(question: string)
|
|
357
|
+
Capture the screen AND analyze it with vision. Combines capture + question.
|
|
291
358
|
|
|
292
359
|
--- CANVAS ---
|
|
293
360
|
|
|
294
|
-
|
|
295
|
-
Render HTML
|
|
296
|
-
|
|
297
|
-
You can use inline CSS and JavaScript. External libraries (Chart.js, Mermaid, etc.) can be loaded via CDN.
|
|
298
|
-
Example: canvas_render("<h1>Sales Report</h1><canvas id='chart'></canvas><script src='https://cdn.jsdelivr.net/npm/chart.js'></script>")
|
|
361
|
+
64. canvas_render(html: string, title?: string)
|
|
362
|
+
Render HTML in the web UI canvas panel. Show charts, tables, diagrams, reports.
|
|
363
|
+
Can use Chart.js, Mermaid, or any CDN library via script tags.
|
|
299
364
|
|
|
300
|
-
|
|
365
|
+
65. canvas_clear()
|
|
301
366
|
Clear the canvas panel.
|
|
302
367
|
|
|
303
368
|
RULES:
|
|
369
|
+
- ABSOLUTE RULE: NEVER LIE. NEVER fabricate, invent, or guess information. If you do not know, say "I don't know." If a tool fails, say it failed. If you cannot see something, say so. Honesty is MORE important than being helpful.
|
|
370
|
+
- CRITICAL: For web searches, ALWAYS use web_search — NEVER open Google/Bing/DuckDuckGo in the browser.
|
|
371
|
+
- CRITICAL: For web searches ("search for X", "find X online", "look up X"), ALWAYS use web_search — NEVER open Google/Bing/DuckDuckGo in the browser. web_search is faster, more reliable, and doesn't get blocked by CAPTCHAs. Only use browser_open for interacting with specific websites (filling forms, clicking buttons, taking screenshots of specific pages).
|
|
304
372
|
- For search/read operations, execute immediately and present results conversationally.
|
|
305
373
|
- 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.
|
|
306
374
|
- For schedule_meeting and schedule_draft_email, execute immediately — these are read operations that suggest slots.
|
|
@@ -312,6 +380,9 @@ RULES:
|
|
|
312
380
|
- Dates: today is {{TODAY}}. Infer relative dates from this.
|
|
313
381
|
- The user's timezone is {{TIMEZONE}}.
|
|
314
382
|
- CRITICAL: when creating calendar events, always use LOCAL time in format "YYYY-MM-DDTHH:MM:SS" WITHOUT any Z suffix or timezone offset.
|
|
383
|
+
- LANGUAGE: Respond in {{LANGUAGE}}. All conversational text, explanations, and descriptions must be in {{LANGUAGE}}. Tool names and JSON blocks remain in English.
|
|
384
|
+
- BROWSER TIP: When extracting data from a page, prefer browser_js with code "document.body.innerText.slice(0, 3000)" to get all visible text. This is more reliable than guessing CSS selectors.
|
|
385
|
+
- API TIP: For npm package info, use fetch_url with the registry API: fetch_url("https://registry.npmjs.org/PACKAGE/latest") for version/description, and fetch_url("https://api.npmjs.org/downloads/point/last-week/PACKAGE") for weekly downloads. These are JSON APIs, much more reliable than scraping the npm website.
|
|
315
386
|
`.trim();
|
|
316
387
|
|
|
317
388
|
// ── Action Parser ────────────────────────────────────────────────────────────
|
|
@@ -463,9 +534,23 @@ export function buildSystemPrompt(persona, personaDescription, config, initialCo
|
|
|
463
534
|
const today = new Date().toISOString().split('T')[0];
|
|
464
535
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
465
536
|
|
|
537
|
+
// Detect system language from locale
|
|
538
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
|
|
539
|
+
const langCode = locale.split('-')[0];
|
|
540
|
+
const LANG_MAP = {
|
|
541
|
+
en: 'English', it: 'Italian', es: 'Spanish', fr: 'French', de: 'German',
|
|
542
|
+
pt: 'Portuguese', nl: 'Dutch', pl: 'Polish', ru: 'Russian', ja: 'Japanese',
|
|
543
|
+
ko: 'Korean', zh: 'Chinese', ar: 'Arabic', hi: 'Hindi', tr: 'Turkish',
|
|
544
|
+
sv: 'Swedish', da: 'Danish', no: 'Norwegian', fi: 'Finnish', cs: 'Czech',
|
|
545
|
+
ro: 'Romanian', hu: 'Hungarian', el: 'Greek', th: 'Thai', vi: 'Vietnamese',
|
|
546
|
+
uk: 'Ukrainian', he: 'Hebrew', id: 'Indonesian', ms: 'Malay',
|
|
547
|
+
};
|
|
548
|
+
const language = config?.language || LANG_MAP[langCode] || 'English';
|
|
549
|
+
|
|
466
550
|
let prompt = TOOL_DEFINITIONS
|
|
467
551
|
.replace('{{TODAY}}', today)
|
|
468
|
-
.replace('{{TIMEZONE}}', tz)
|
|
552
|
+
.replace('{{TIMEZONE}}', tz)
|
|
553
|
+
.replace(/\{\{LANGUAGE\}\}/g, language);
|
|
469
554
|
|
|
470
555
|
prompt += `\n\n${personaDescription}`;
|
|
471
556
|
|
|
@@ -1059,63 +1144,303 @@ export async function executeTool(action, params, config) {
|
|
|
1059
1144
|
return `Birthday set for ${contact.name}: ${monthName} ${day}. It will appear in the Birthdays tab.`;
|
|
1060
1145
|
}
|
|
1061
1146
|
|
|
1062
|
-
// ──
|
|
1063
|
-
case '
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1147
|
+
// ── Browser Automation ────────────────────────────────────────────
|
|
1148
|
+
case 'browser_open': {
|
|
1149
|
+
const url = params.url;
|
|
1150
|
+
if (!url) return 'A URL is required.';
|
|
1151
|
+
|
|
1152
|
+
// Intercept search engine URLs — redirect to web_search tool
|
|
1153
|
+
const searchEngines = /^https?:\/\/(www\.)?(google|bing|duckduckgo|yahoo|baidu|yandex)\.(com|it|co\.uk|de|fr|es|org|net)/i;
|
|
1154
|
+
if (searchEngines.test(url)) {
|
|
1155
|
+
// Extract search query if present in URL
|
|
1156
|
+
try {
|
|
1157
|
+
const u = new URL(url);
|
|
1158
|
+
const q = u.searchParams.get('q') || u.searchParams.get('query') || u.searchParams.get('p');
|
|
1159
|
+
if (q) {
|
|
1160
|
+
return `REDIRECT: Use web_search instead. Search engines block automated browsers. Executing web_search for "${q}"...\n\n` +
|
|
1161
|
+
await executeTool('web_search', { query: q }, config);
|
|
1162
|
+
}
|
|
1163
|
+
} catch {}
|
|
1164
|
+
return 'Do NOT open search engines in the browser — they block automated access with CAPTCHAs. Use the web_search tool instead: {"action": "web_search", "params": {"query": "your search terms"}}';
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const be = await import('./browser-engine.mjs');
|
|
1168
|
+
const result = await be.browserOpen(url, {
|
|
1169
|
+
waitForLoad: params.waitForLoad !== false,
|
|
1170
|
+
});
|
|
1171
|
+
if (result.error) return `Browser error: ${result.message}`;
|
|
1068
1172
|
|
|
1069
|
-
|
|
1070
|
-
const question = params.question || 'Describe EXACTLY and ONLY what you see in this screenshot. Do NOT invent or guess anything.';
|
|
1071
|
-
return {
|
|
1072
|
-
__screenshot: true,
|
|
1073
|
-
path: result.path,
|
|
1074
|
-
base64: result.base64,
|
|
1075
|
-
question,
|
|
1076
|
-
displayText: `[SCREENSHOT_FILE]${result.path}[/SCREENSHOT_FILE]`,
|
|
1077
|
-
};
|
|
1173
|
+
return `Page loaded: "${result.title}"\nURL: ${result.url}`;
|
|
1078
1174
|
}
|
|
1079
1175
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1176
|
+
case 'browser_screenshot': {
|
|
1177
|
+
const be = await import('./browser-engine.mjs');
|
|
1178
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1179
|
+
|
|
1180
|
+
// Scroll to top before screenshot so user sees the most important content
|
|
1181
|
+
await be.browserScroll({ direction: 'top' });
|
|
1182
|
+
await new Promise(r => setTimeout(r, 300));
|
|
1183
|
+
|
|
1184
|
+
const saveTo = params.saveTo
|
|
1185
|
+
? params.saveTo.replace(/^~/, os.homedir())
|
|
1186
|
+
: null;
|
|
1187
|
+
|
|
1188
|
+
// Always use JPEG for efficiency (smaller base64, faster rendering in web UI)
|
|
1189
|
+
const result = await be.browserScreenshot({
|
|
1190
|
+
fullPage: false, // Always viewport — fullPage produces oversized images
|
|
1191
|
+
format: 'jpeg',
|
|
1192
|
+
quality: 75,
|
|
1193
|
+
saveTo,
|
|
1194
|
+
});
|
|
1195
|
+
if (result.error) return `Screenshot error: ${result.message}`;
|
|
1196
|
+
|
|
1197
|
+
if (result.savedTo) {
|
|
1198
|
+
return `Screenshot saved to: ${result.savedTo} (${Math.round(result.size / 1024)}KB base64)`;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Return base64 for LLM vision analysis
|
|
1202
|
+
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]`;
|
|
1087
1203
|
}
|
|
1088
1204
|
|
|
1089
|
-
case '
|
|
1090
|
-
|
|
1205
|
+
case 'browser_click': {
|
|
1206
|
+
const be = await import('./browser-engine.mjs');
|
|
1207
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1208
|
+
|
|
1209
|
+
const result = await be.browserClick({
|
|
1210
|
+
text: params.text,
|
|
1211
|
+
selector: params.selector,
|
|
1212
|
+
x: params.x,
|
|
1213
|
+
y: params.y,
|
|
1214
|
+
});
|
|
1215
|
+
if (result.error) return `Click error: ${result.message}`;
|
|
1216
|
+
|
|
1217
|
+
return `Clicked: ${result.selector} at (${result.x}, ${result.y})`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
case 'browser_type': {
|
|
1221
|
+
const be = await import('./browser-engine.mjs');
|
|
1222
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1223
|
+
|
|
1224
|
+
if (!params.text) return 'Text is required.';
|
|
1225
|
+
|
|
1226
|
+
const result = await be.browserType({
|
|
1227
|
+
text: params.text,
|
|
1228
|
+
selector: params.selector,
|
|
1229
|
+
clear: params.clear || false,
|
|
1230
|
+
delay: params.delay || 0,
|
|
1231
|
+
});
|
|
1232
|
+
if (result.error) return `Type error: ${result.message}`;
|
|
1233
|
+
|
|
1234
|
+
return `Typed ${result.length} chars into ${result.selector}`;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
case 'browser_extract': {
|
|
1238
|
+
const be = await import('./browser-engine.mjs');
|
|
1239
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1240
|
+
|
|
1241
|
+
const result = await be.browserExtract({
|
|
1242
|
+
selector: params.selector || 'body',
|
|
1243
|
+
mode: params.mode || 'text',
|
|
1244
|
+
attribute: params.attribute,
|
|
1245
|
+
all: params.all || false,
|
|
1246
|
+
});
|
|
1247
|
+
if (result.error) return `Extract error: ${result.message}`;
|
|
1248
|
+
|
|
1249
|
+
return `[${result.selector}] (${result.length} chars):\n${result.content}`;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
case 'browser_js': {
|
|
1253
|
+
const be = await import('./browser-engine.mjs');
|
|
1254
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1255
|
+
|
|
1256
|
+
if (!params.code) return 'JavaScript code is required.';
|
|
1257
|
+
|
|
1258
|
+
const result = await be.browserEval(params.code);
|
|
1259
|
+
if (result.error) return `JS error: ${result.message}`;
|
|
1260
|
+
|
|
1261
|
+
return `[${result.type}] ${result.result}`;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
case 'browser_wait': {
|
|
1265
|
+
const be = await import('./browser-engine.mjs');
|
|
1266
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1267
|
+
|
|
1268
|
+
if (!params.selector) return 'A CSS selector is required.';
|
|
1269
|
+
|
|
1270
|
+
const result = await be.browserWaitFor(params.selector, {
|
|
1271
|
+
timeout: params.timeout || 10000,
|
|
1272
|
+
visible: params.visible !== false,
|
|
1273
|
+
});
|
|
1274
|
+
if (result.error) return `Wait failed: ${result.message}`;
|
|
1275
|
+
|
|
1276
|
+
return `Element found: ${result.selector} (${result.elapsed}ms)`;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
case 'browser_scroll': {
|
|
1280
|
+
const be = await import('./browser-engine.mjs');
|
|
1281
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1282
|
+
|
|
1283
|
+
const result = await be.browserScroll({
|
|
1284
|
+
direction: params.direction || 'down',
|
|
1285
|
+
amount: params.amount || 500,
|
|
1286
|
+
});
|
|
1287
|
+
if (result.error) return `Scroll error: ${result.message}`;
|
|
1288
|
+
|
|
1289
|
+
return `Scrolled ${result.direction}`;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
case 'browser_key': {
|
|
1293
|
+
const be = await import('./browser-engine.mjs');
|
|
1294
|
+
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1295
|
+
|
|
1296
|
+
if (!params.key) return 'A key name is required (e.g. Enter, Tab, Escape).';
|
|
1297
|
+
|
|
1298
|
+
const result = await be.browserKeyPress(params.key, {
|
|
1299
|
+
ctrl: params.ctrl,
|
|
1300
|
+
shift: params.shift,
|
|
1301
|
+
alt: params.alt,
|
|
1302
|
+
});
|
|
1303
|
+
if (result.error) return `Key error: ${result.message}`;
|
|
1304
|
+
|
|
1305
|
+
return `Pressed: ${result.key}`;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
case 'browser_close': {
|
|
1309
|
+
const be = await import('./browser-engine.mjs');
|
|
1310
|
+
const result = await be.browserClose();
|
|
1311
|
+
return result.message;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// ── Web Search & Fetch ──────────────────────────────────────────────
|
|
1315
|
+
case 'web_search': {
|
|
1316
|
+
const wt = await import('./web-tools.mjs');
|
|
1317
|
+
const query = params.query;
|
|
1318
|
+
if (!query) return 'A search query is required.';
|
|
1319
|
+
|
|
1320
|
+
if (params.deep) {
|
|
1321
|
+
const result = await wt.webSearchDeep(query, 3);
|
|
1322
|
+
if (result.error) return `Search error: ${result.message}`;
|
|
1323
|
+
if (result.results.length === 0) return `No results found for "${query}".`;
|
|
1324
|
+
|
|
1325
|
+
const lines = [`Web search: "${query}" — ${result.resultCount} results, ${result.deepFetched} pages fetched\n`];
|
|
1326
|
+
|
|
1327
|
+
// Deep results first (with content)
|
|
1328
|
+
for (const dr of result.deepResults) {
|
|
1329
|
+
lines.push(`--- ${dr.title} ---`);
|
|
1330
|
+
lines.push(`URL: ${dr.url}`);
|
|
1331
|
+
lines.push(dr.content.slice(0, 2000));
|
|
1332
|
+
lines.push('');
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Remaining results (snippets only)
|
|
1336
|
+
const deepUrls = new Set(result.deepResults.map(d => d.url));
|
|
1337
|
+
for (const r of result.results.filter(r => !deepUrls.has(r.url))) {
|
|
1338
|
+
lines.push(`${r.title}`);
|
|
1339
|
+
lines.push(` ${r.url}`);
|
|
1340
|
+
if (r.snippet) lines.push(` ${r.snippet}`);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return lines.join('\n');
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const result = await wt.webSearch(query);
|
|
1347
|
+
if (result.error) return `Search error: ${result.message}`;
|
|
1348
|
+
if (result.results.length === 0) return `No results found for "${query}".`;
|
|
1349
|
+
|
|
1350
|
+
const textResult = `Web search: "${query}" — ${result.resultCount} results\n\n` +
|
|
1351
|
+
result.results.map((r, i) =>
|
|
1352
|
+
`${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`
|
|
1353
|
+
).join('\n\n');
|
|
1354
|
+
|
|
1355
|
+
// If screenshot requested, render results as HTML in browser and capture
|
|
1356
|
+
if (params.screenshot) {
|
|
1357
|
+
try {
|
|
1358
|
+
const be = await import('./browser-engine.mjs');
|
|
1359
|
+
// Ensure browser is running
|
|
1360
|
+
if (!be.isBrowserRunning()) {
|
|
1361
|
+
await be.browserOpen('https://example.com', { waitForLoad: true });
|
|
1362
|
+
}
|
|
1363
|
+
// Build search results HTML
|
|
1364
|
+
const esc = (s) => (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1365
|
+
const htmlItems = result.results.map((r, i) =>
|
|
1366
|
+
`<div style="margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #3c4043"><div style="font-size:18px;color:#8ab4f8;margin-bottom:4px;font-weight:600">${i + 1}. ${esc(r.title)}</div><div style="font-size:13px;color:#bdc1c6;margin-bottom:6px">${esc(r.url)}</div><div style="font-size:14px;color:#969ba1;line-height:1.5">${esc(r.snippet)}</div></div>`
|
|
1367
|
+
).join('');
|
|
1368
|
+
const fullHtml = `<html><head><style>body{background:#202124;color:#e8eaed;font-family:Arial,Helvetica,sans-serif;padding:24px 40px;max-width:800px;margin:0 auto}h1{font-size:22px;color:#8ab4f8;margin-bottom:24px;border-bottom:2px solid #3c4043;padding-bottom:12px}</style></head><body><h1>Search results: "${esc(query)}"</h1>${htmlItems}<div style="color:#5f6368;font-size:12px;margin-top:16px">Powered by NHA web_search — ${result.resultCount} results via DuckDuckGo</div></body></html>`;
|
|
1369
|
+
// Render by injecting HTML into current page via JS
|
|
1370
|
+
await be.browserEval(`document.open();document.write(${JSON.stringify(fullHtml)});document.close();`);
|
|
1371
|
+
await new Promise(r => setTimeout(r, 300));
|
|
1372
|
+
const ss = await be.browserScreenshot({ fullPage: false, format: 'jpeg', quality: 75 });
|
|
1373
|
+
if (!ss.error) {
|
|
1374
|
+
// Save to disk for persistence
|
|
1375
|
+
const ssDir = path.join(os.homedir(), '.nha', 'screenshots');
|
|
1376
|
+
const fsMod = await import('fs');
|
|
1377
|
+
fsMod.mkdirSync(ssDir, { recursive: true });
|
|
1378
|
+
const ssFilename = `ss-${Date.now()}.jpg`;
|
|
1379
|
+
fsMod.writeFileSync(path.join(ssDir, ssFilename), Buffer.from(ss.base64, 'base64'));
|
|
1380
|
+
return textResult + `\n\n[Screenshot of results captured (${Math.round(ss.size / 1024)}KB) file:${ssFilename}]`;
|
|
1381
|
+
}
|
|
1382
|
+
} catch { /* screenshot failed */ }
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
return textResult;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
case 'fetch_url': {
|
|
1389
|
+
const wt = await import('./web-tools.mjs');
|
|
1390
|
+
const url = params.url;
|
|
1391
|
+
if (!url) return 'A URL is required.';
|
|
1392
|
+
|
|
1393
|
+
const result = await wt.fetchUrl(url);
|
|
1394
|
+
if (result.error) return `Fetch error: ${result.message}`;
|
|
1395
|
+
|
|
1396
|
+
const lines = [];
|
|
1397
|
+
if (result.title) lines.push(`Title: ${result.title}`);
|
|
1398
|
+
lines.push(`URL: ${result.url || url}`);
|
|
1399
|
+
lines.push(`Status: ${result.status}`);
|
|
1400
|
+
if (result.truncated) lines.push('[Content was truncated due to size limits]');
|
|
1401
|
+
lines.push('');
|
|
1402
|
+
lines.push(result.body);
|
|
1403
|
+
|
|
1404
|
+
return lines.join('\n');
|
|
1091
1405
|
}
|
|
1092
1406
|
|
|
1093
1407
|
// ── Cron / Heartbeat ───────────────────────────────────────────────
|
|
1094
1408
|
case 'cron_add': {
|
|
1095
1409
|
const { addCronJob } = await import('./ops-daemon.mjs');
|
|
1096
1410
|
const result = addCronJob(params.schedule, params.prompt, { agent: params.agent || null });
|
|
1097
|
-
if (result.ok) {
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1100
|
-
return `Failed to create scheduled task: ${result.error}`;
|
|
1411
|
+
if (result.ok) return `Scheduled task created: "${params.schedule}" → "${params.prompt}". Start daemon with \`nha ops start\` if not running.`;
|
|
1412
|
+
return `Failed: ${result.error}`;
|
|
1101
1413
|
}
|
|
1102
|
-
|
|
1103
1414
|
case 'cron_list': {
|
|
1104
1415
|
const { listCronJobs } = await import('./ops-daemon.mjs');
|
|
1105
1416
|
const jobs = listCronJobs();
|
|
1106
1417
|
if (jobs.length === 0) return 'No scheduled tasks configured.';
|
|
1107
|
-
return jobs.map((j, i) =>
|
|
1108
|
-
`${i + 1}. [${j.enabled ? 'active' : 'paused'}] ${j.schedule} → ${j.prompt} (runs: ${j.runCount}, last: ${j.lastRun ? new Date(j.lastRun).toLocaleString() : 'never'})`
|
|
1109
|
-
).join('\n');
|
|
1418
|
+
return jobs.map((j, i) => `${i + 1}. [${j.enabled ? 'active' : 'paused'}] ${j.schedule} → ${j.prompt} (runs: ${j.runCount}, last: ${j.lastRun ? new Date(j.lastRun).toLocaleString() : 'never'})`).join('\n');
|
|
1110
1419
|
}
|
|
1111
|
-
|
|
1112
1420
|
case 'cron_remove': {
|
|
1113
1421
|
const { removeCronJob } = await import('./ops-daemon.mjs');
|
|
1114
1422
|
const result = removeCronJob(params.index || params.id);
|
|
1115
|
-
if (result.ok) {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1423
|
+
if (result.ok) return `Removed: "${result.removed.schedule}" → "${result.removed.prompt}"`;
|
|
1424
|
+
return `Failed: ${result.error}`;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// ── Screen Capture + Vision ──────────────────────────────────────────
|
|
1428
|
+
case 'screen_capture':
|
|
1429
|
+
case 'screen_analyze': {
|
|
1430
|
+
const { captureScreen } = await import('./screen-capture.mjs');
|
|
1431
|
+
const result = captureScreen({ monitor: params.monitor || 1 });
|
|
1432
|
+
if (!result.ok) return `Screen capture failed: ${result.error}`;
|
|
1433
|
+
const question = params.question || 'Describe EXACTLY and ONLY what you see in this screenshot.';
|
|
1434
|
+
return { __screenshot: true, path: result.path, base64: result.base64, question };
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// ── Canvas ───────────────────────────────────────────────────────────
|
|
1438
|
+
case 'canvas_render': {
|
|
1439
|
+
if (!params.html) return 'Error: html parameter is required.';
|
|
1440
|
+
return `[CANVAS_RENDER]${JSON.stringify({ html: params.html, title: params.title || 'Canvas' })}[/CANVAS_RENDER]\nRendered in canvas panel.`;
|
|
1441
|
+
}
|
|
1442
|
+
case 'canvas_clear': {
|
|
1443
|
+
return '[CANVAS_CLEAR]Canvas cleared.[/CANVAS_CLEAR]';
|
|
1119
1444
|
}
|
|
1120
1445
|
|
|
1121
1446
|
default:
|