nothumanallowed 9.5.2 → 9.6.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 +154 -305
- package/bin/nha.mjs +34 -3
- package/package.json +2 -2
- package/src/cli.mjs +105 -153
- package/src/commands/ask.mjs +18 -206
- package/src/commands/chat.mjs +64 -482
- package/src/commands/ui.mjs +41 -837
- package/src/config.mjs +0 -2
- package/src/constants.mjs +1 -1
- package/src/services/google-oauth.mjs +21 -12
- package/src/services/llm.mjs +0 -138
- package/src/services/ops-daemon.mjs +236 -0
- package/src/services/screen-capture.mjs +160 -0
- package/src/services/tool-executor.mjs +88 -335
- package/src/services/web-ui.mjs +126 -423
- package/src/services/browser-engine.mjs +0 -1240
- package/src/services/conversations.mjs +0 -277
- package/src/services/web-tools.mjs +0 -430
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
* Zero external dependencies — pure Node.js 22.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import os from 'os';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
|
|
13
10
|
import {
|
|
14
11
|
listMessages,
|
|
15
12
|
getMessage,
|
|
@@ -65,6 +62,8 @@ export const DESTRUCTIVE_ACTIONS = new Set([
|
|
|
65
62
|
'notify_remind',
|
|
66
63
|
'slack_send',
|
|
67
64
|
'github_create_issue',
|
|
65
|
+
'cron_add',
|
|
66
|
+
'cron_remove',
|
|
68
67
|
]);
|
|
69
68
|
|
|
70
69
|
// ── Tool Definitions (for system prompt) ─────────────────────────────────────
|
|
@@ -75,16 +74,14 @@ export const DESTRUCTIVE_ACTIONS = new Set([
|
|
|
75
74
|
*/
|
|
76
75
|
export const TOOL_DEFINITIONS = `
|
|
77
76
|
You have access to the following tools. When the user's message requires an action,
|
|
78
|
-
output
|
|
77
|
+
output EXACTLY ONE fenced JSON block per action:
|
|
79
78
|
|
|
80
79
|
\`\`\`json
|
|
81
80
|
{"action": "<tool_name>", "params": { ... }}
|
|
82
81
|
\`\`\`
|
|
83
82
|
|
|
84
|
-
You
|
|
85
|
-
|
|
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.
|
|
83
|
+
You may include conversational text BEFORE or AFTER the JSON block. If no action
|
|
84
|
+
is needed, respond normally without any JSON block.
|
|
88
85
|
|
|
89
86
|
TOOLS:
|
|
90
87
|
|
|
@@ -268,75 +265,42 @@ TOOLS:
|
|
|
268
265
|
46. birthday_add(name: string, date: string)
|
|
269
266
|
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.
|
|
270
267
|
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
47.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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.
|
|
268
|
+
--- SCHEDULED TASKS ---
|
|
269
|
+
|
|
270
|
+
47. cron_add(schedule: string, prompt: string, agent?: string)
|
|
271
|
+
Create a recurring background task. The daemon executes it automatically.
|
|
272
|
+
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
|
+
|
|
275
|
+
48. cron_list()
|
|
276
|
+
List all scheduled background tasks with their status, run count, and last result.
|
|
277
|
+
|
|
278
|
+
49. cron_remove(index: number)
|
|
279
|
+
Remove a scheduled task by its number (1-based, from cron_list).
|
|
280
|
+
|
|
281
|
+
--- SCREEN & VISION ---
|
|
282
|
+
|
|
283
|
+
50. screen_capture(monitor?: number)
|
|
284
|
+
Capture a screenshot of the user's desktop screen. Returns the image for visual analysis.
|
|
285
|
+
Use this when the user asks you to look at their screen, analyze what's visible, help with something on screen, etc.
|
|
286
|
+
Optional: monitor number (default 1, for multi-monitor setups).
|
|
287
|
+
|
|
288
|
+
51. screen_analyze(question: string)
|
|
289
|
+
Capture the screen AND analyze it with a specific question. Combines capture + vision in one call.
|
|
290
|
+
Example: screen_analyze("what error is shown in the terminal?")
|
|
291
|
+
|
|
292
|
+
--- CANVAS ---
|
|
293
|
+
|
|
294
|
+
52. canvas_render(html: string, title?: string)
|
|
295
|
+
Render HTML content in the web dashboard's canvas panel. Use this to show charts, tables, diagrams,
|
|
296
|
+
dashboards, formatted reports, or any visual content to the user. The HTML is rendered in an isolated iframe.
|
|
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>")
|
|
299
|
+
|
|
300
|
+
53. canvas_clear()
|
|
301
|
+
Clear the canvas panel.
|
|
337
302
|
|
|
338
303
|
RULES:
|
|
339
|
-
- 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).
|
|
340
304
|
- For search/read operations, execute immediately and present results conversationally.
|
|
341
305
|
- 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.
|
|
342
306
|
- For schedule_meeting and schedule_draft_email, execute immediately — these are read operations that suggest slots.
|
|
@@ -348,9 +312,6 @@ RULES:
|
|
|
348
312
|
- Dates: today is {{TODAY}}. Infer relative dates from this.
|
|
349
313
|
- The user's timezone is {{TIMEZONE}}.
|
|
350
314
|
- CRITICAL: when creating calendar events, always use LOCAL time in format "YYYY-MM-DDTHH:MM:SS" WITHOUT any Z suffix or timezone offset.
|
|
351
|
-
- LANGUAGE: Respond in {{LANGUAGE}}. All conversational text, explanations, and descriptions must be in {{LANGUAGE}}. Tool names and JSON blocks remain in English.
|
|
352
|
-
- 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.
|
|
353
|
-
- 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.
|
|
354
315
|
`.trim();
|
|
355
316
|
|
|
356
317
|
// ── Action Parser ────────────────────────────────────────────────────────────
|
|
@@ -502,23 +463,9 @@ export function buildSystemPrompt(persona, personaDescription, config, initialCo
|
|
|
502
463
|
const today = new Date().toISOString().split('T')[0];
|
|
503
464
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
504
465
|
|
|
505
|
-
// Detect system language from locale
|
|
506
|
-
const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
|
|
507
|
-
const langCode = locale.split('-')[0];
|
|
508
|
-
const LANG_MAP = {
|
|
509
|
-
en: 'English', it: 'Italian', es: 'Spanish', fr: 'French', de: 'German',
|
|
510
|
-
pt: 'Portuguese', nl: 'Dutch', pl: 'Polish', ru: 'Russian', ja: 'Japanese',
|
|
511
|
-
ko: 'Korean', zh: 'Chinese', ar: 'Arabic', hi: 'Hindi', tr: 'Turkish',
|
|
512
|
-
sv: 'Swedish', da: 'Danish', no: 'Norwegian', fi: 'Finnish', cs: 'Czech',
|
|
513
|
-
ro: 'Romanian', hu: 'Hungarian', el: 'Greek', th: 'Thai', vi: 'Vietnamese',
|
|
514
|
-
uk: 'Ukrainian', he: 'Hebrew', id: 'Indonesian', ms: 'Malay',
|
|
515
|
-
};
|
|
516
|
-
const language = config?.language || LANG_MAP[langCode] || 'English';
|
|
517
|
-
|
|
518
466
|
let prompt = TOOL_DEFINITIONS
|
|
519
467
|
.replace('{{TODAY}}', today)
|
|
520
|
-
.replace('{{TIMEZONE}}', tz)
|
|
521
|
-
.replace(/\{\{LANGUAGE\}\}/g, language);
|
|
468
|
+
.replace('{{TIMEZONE}}', tz);
|
|
522
469
|
|
|
523
470
|
prompt += `\n\n${personaDescription}`;
|
|
524
471
|
|
|
@@ -1112,264 +1059,70 @@ export async function executeTool(action, params, config) {
|
|
|
1112
1059
|
return `Birthday set for ${contact.name}: ${monthName} ${day}. It will appear in the Birthdays tab.`;
|
|
1113
1060
|
}
|
|
1114
1061
|
|
|
1115
|
-
// ──
|
|
1116
|
-
case '
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
// Intercept search engine URLs — redirect to web_search tool
|
|
1121
|
-
const searchEngines = /^https?:\/\/(www\.)?(google|bing|duckduckgo|yahoo|baidu|yandex)\.(com|it|co\.uk|de|fr|es|org|net)/i;
|
|
1122
|
-
if (searchEngines.test(url)) {
|
|
1123
|
-
// Extract search query if present in URL
|
|
1124
|
-
try {
|
|
1125
|
-
const u = new URL(url);
|
|
1126
|
-
const q = u.searchParams.get('q') || u.searchParams.get('query') || u.searchParams.get('p');
|
|
1127
|
-
if (q) {
|
|
1128
|
-
return `REDIRECT: Use web_search instead. Search engines block automated browsers. Executing web_search for "${q}"...\n\n` +
|
|
1129
|
-
await executeTool('web_search', { query: q }, config);
|
|
1130
|
-
}
|
|
1131
|
-
} catch {}
|
|
1132
|
-
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"}}';
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
const be = await import('./browser-engine.mjs');
|
|
1136
|
-
const result = await be.browserOpen(url, {
|
|
1137
|
-
waitForLoad: params.waitForLoad !== false,
|
|
1138
|
-
});
|
|
1139
|
-
if (result.error) return `Browser error: ${result.message}`;
|
|
1140
|
-
|
|
1141
|
-
return `Page loaded: "${result.title}"\nURL: ${result.url}`;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
case 'browser_screenshot': {
|
|
1145
|
-
const be = await import('./browser-engine.mjs');
|
|
1146
|
-
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1147
|
-
|
|
1148
|
-
// Scroll to top before screenshot so user sees the most important content
|
|
1149
|
-
await be.browserScroll({ direction: 'top' });
|
|
1150
|
-
await new Promise(r => setTimeout(r, 300));
|
|
1151
|
-
|
|
1152
|
-
const saveTo = params.saveTo
|
|
1153
|
-
? params.saveTo.replace(/^~/, os.homedir())
|
|
1154
|
-
: null;
|
|
1155
|
-
|
|
1156
|
-
// Always use JPEG for efficiency (smaller base64, faster rendering in web UI)
|
|
1157
|
-
const result = await be.browserScreenshot({
|
|
1158
|
-
fullPage: false, // Always viewport — fullPage produces oversized images
|
|
1159
|
-
format: 'jpeg',
|
|
1160
|
-
quality: 75,
|
|
1161
|
-
saveTo,
|
|
1162
|
-
});
|
|
1163
|
-
if (result.error) return `Screenshot error: ${result.message}`;
|
|
1164
|
-
|
|
1165
|
-
if (result.savedTo) {
|
|
1166
|
-
return `Screenshot saved to: ${result.savedTo} (${Math.round(result.size / 1024)}KB base64)`;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// Return base64 for LLM vision analysis
|
|
1170
|
-
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]`;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
case 'browser_click': {
|
|
1174
|
-
const be = await import('./browser-engine.mjs');
|
|
1175
|
-
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1176
|
-
|
|
1177
|
-
const result = await be.browserClick({
|
|
1178
|
-
text: params.text,
|
|
1179
|
-
selector: params.selector,
|
|
1180
|
-
x: params.x,
|
|
1181
|
-
y: params.y,
|
|
1182
|
-
});
|
|
1183
|
-
if (result.error) return `Click error: ${result.message}`;
|
|
1184
|
-
|
|
1185
|
-
return `Clicked: ${result.selector} at (${result.x}, ${result.y})`;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
case 'browser_type': {
|
|
1189
|
-
const be = await import('./browser-engine.mjs');
|
|
1190
|
-
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1191
|
-
|
|
1192
|
-
if (!params.text) return 'Text is required.';
|
|
1193
|
-
|
|
1194
|
-
const result = await be.browserType({
|
|
1195
|
-
text: params.text,
|
|
1196
|
-
selector: params.selector,
|
|
1197
|
-
clear: params.clear || false,
|
|
1198
|
-
delay: params.delay || 0,
|
|
1199
|
-
});
|
|
1200
|
-
if (result.error) return `Type error: ${result.message}`;
|
|
1201
|
-
|
|
1202
|
-
return `Typed ${result.length} chars into ${result.selector}`;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
case 'browser_extract': {
|
|
1206
|
-
const be = await import('./browser-engine.mjs');
|
|
1207
|
-
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1062
|
+
// ── Screen Capture + Vision ────────────────────────────────────────
|
|
1063
|
+
case 'screen_capture': {
|
|
1064
|
+
const { captureScreen } = await import('./screen-capture.mjs');
|
|
1065
|
+
const result = captureScreen({ monitor: params.monitor || 1 });
|
|
1066
|
+
if (!result.ok) return `Screen capture failed: ${result.error}`;
|
|
1208
1067
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
mode: params.mode || 'text',
|
|
1212
|
-
attribute: params.attribute,
|
|
1213
|
-
all: params.all || false,
|
|
1214
|
-
});
|
|
1215
|
-
if (result.error) return `Extract error: ${result.message}`;
|
|
1068
|
+
// Save screenshot to file for the UI to display
|
|
1069
|
+
const screenshotPath = result.path;
|
|
1216
1070
|
|
|
1217
|
-
|
|
1071
|
+
// Return a compact description + mark that we have an image
|
|
1072
|
+
// The calling layer (chat/ui) will handle displaying the image
|
|
1073
|
+
return `[SCREENSHOT]${screenshotPath}[/SCREENSHOT]\nScreenshot captured successfully. I can see your screen. What would you like me to analyze?`;
|
|
1218
1074
|
}
|
|
1219
1075
|
|
|
1220
|
-
case '
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (!params.code) return 'JavaScript code is required.';
|
|
1076
|
+
case 'screen_analyze': {
|
|
1077
|
+
const { captureScreen } = await import('./screen-capture.mjs');
|
|
1078
|
+
const result = captureScreen({ monitor: params.monitor || 1 });
|
|
1079
|
+
if (!result.ok) return `Screen capture failed: ${result.error}`;
|
|
1225
1080
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
return `[${result.
|
|
1081
|
+
// For vision analysis, we need to send the image to the LLM
|
|
1082
|
+
// Return the base64 with a marker so the chat layer sends it as a vision message
|
|
1083
|
+
const question = params.question || 'Describe what you see on screen.';
|
|
1084
|
+
return `[VISION_REQUEST]${result.base64}[/VISION_REQUEST]\n[VISION_QUESTION]${question}[/VISION_QUESTION]`;
|
|
1230
1085
|
}
|
|
1231
1086
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
if (!
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
timeout: params.timeout || 10000,
|
|
1240
|
-
visible: params.visible !== false,
|
|
1241
|
-
});
|
|
1242
|
-
if (result.error) return `Wait failed: ${result.message}`;
|
|
1243
|
-
|
|
1244
|
-
return `Element found: ${result.selector} (${result.elapsed}ms)`;
|
|
1087
|
+
// ── Canvas ───────────────────────────────────────────────────────────
|
|
1088
|
+
case 'canvas_render': {
|
|
1089
|
+
const html = params.html;
|
|
1090
|
+
const title = params.title || 'Canvas';
|
|
1091
|
+
if (!html) return 'Error: html parameter is required.';
|
|
1092
|
+
// Return a marker that the UI layer will intercept to render in the canvas panel
|
|
1093
|
+
return `[CANVAS_RENDER]${JSON.stringify({ html, title })}[/CANVAS_RENDER]\nRendered "${title}" in the canvas panel.`;
|
|
1245
1094
|
}
|
|
1246
1095
|
|
|
1247
|
-
case '
|
|
1248
|
-
|
|
1249
|
-
if (!be.isBrowserRunning()) return 'No browser open. Use browser_open first.';
|
|
1250
|
-
|
|
1251
|
-
const result = await be.browserScroll({
|
|
1252
|
-
direction: params.direction || 'down',
|
|
1253
|
-
amount: params.amount || 500,
|
|
1254
|
-
});
|
|
1255
|
-
if (result.error) return `Scroll error: ${result.message}`;
|
|
1256
|
-
|
|
1257
|
-
return `Scrolled ${result.direction}`;
|
|
1096
|
+
case 'canvas_clear': {
|
|
1097
|
+
return '[CANVAS_CLEAR]Canvas cleared.[/CANVAS_CLEAR]';
|
|
1258
1098
|
}
|
|
1259
1099
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
if (
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
shift: params.shift,
|
|
1269
|
-
alt: params.alt,
|
|
1270
|
-
});
|
|
1271
|
-
if (result.error) return `Key error: ${result.message}`;
|
|
1272
|
-
|
|
1273
|
-
return `Pressed: ${result.key}`;
|
|
1100
|
+
// ── Cron / Heartbeat ───────────────────────────────────────────────
|
|
1101
|
+
case 'cron_add': {
|
|
1102
|
+
const { addCronJob } = await import('./ops-daemon.mjs');
|
|
1103
|
+
const result = addCronJob(params.schedule, params.prompt, { agent: params.agent || null });
|
|
1104
|
+
if (result.ok) {
|
|
1105
|
+
return `Scheduled task created: "${params.schedule}" → "${params.prompt}". The daemon will execute it automatically. Start daemon with \`nha ops start\` if not running.`;
|
|
1106
|
+
}
|
|
1107
|
+
return `Failed to create scheduled task: ${result.error}`;
|
|
1274
1108
|
}
|
|
1275
1109
|
|
|
1276
|
-
case '
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1279
|
-
return
|
|
1110
|
+
case 'cron_list': {
|
|
1111
|
+
const { listCronJobs } = await import('./ops-daemon.mjs');
|
|
1112
|
+
const jobs = listCronJobs();
|
|
1113
|
+
if (jobs.length === 0) return 'No scheduled tasks configured.';
|
|
1114
|
+
return jobs.map((j, i) =>
|
|
1115
|
+
`${i + 1}. [${j.enabled ? 'active' : 'paused'}] ${j.schedule} → ${j.prompt} (runs: ${j.runCount}, last: ${j.lastRun ? new Date(j.lastRun).toLocaleString() : 'never'})`
|
|
1116
|
+
).join('\n');
|
|
1280
1117
|
}
|
|
1281
1118
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (params.deep) {
|
|
1289
|
-
const result = await wt.webSearchDeep(query, 3);
|
|
1290
|
-
if (result.error) return `Search error: ${result.message}`;
|
|
1291
|
-
if (result.results.length === 0) return `No results found for "${query}".`;
|
|
1292
|
-
|
|
1293
|
-
const lines = [`Web search: "${query}" — ${result.resultCount} results, ${result.deepFetched} pages fetched\n`];
|
|
1294
|
-
|
|
1295
|
-
// Deep results first (with content)
|
|
1296
|
-
for (const dr of result.deepResults) {
|
|
1297
|
-
lines.push(`--- ${dr.title} ---`);
|
|
1298
|
-
lines.push(`URL: ${dr.url}`);
|
|
1299
|
-
lines.push(dr.content.slice(0, 2000));
|
|
1300
|
-
lines.push('');
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// Remaining results (snippets only)
|
|
1304
|
-
const deepUrls = new Set(result.deepResults.map(d => d.url));
|
|
1305
|
-
for (const r of result.results.filter(r => !deepUrls.has(r.url))) {
|
|
1306
|
-
lines.push(`${r.title}`);
|
|
1307
|
-
lines.push(` ${r.url}`);
|
|
1308
|
-
if (r.snippet) lines.push(` ${r.snippet}`);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
return lines.join('\n');
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const result = await wt.webSearch(query);
|
|
1315
|
-
if (result.error) return `Search error: ${result.message}`;
|
|
1316
|
-
if (result.results.length === 0) return `No results found for "${query}".`;
|
|
1317
|
-
|
|
1318
|
-
const textResult = `Web search: "${query}" — ${result.resultCount} results\n\n` +
|
|
1319
|
-
result.results.map((r, i) =>
|
|
1320
|
-
`${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`
|
|
1321
|
-
).join('\n\n');
|
|
1322
|
-
|
|
1323
|
-
// If screenshot requested, render results as HTML in browser and capture
|
|
1324
|
-
if (params.screenshot) {
|
|
1325
|
-
try {
|
|
1326
|
-
const be = await import('./browser-engine.mjs');
|
|
1327
|
-
// Ensure browser is running
|
|
1328
|
-
if (!be.isBrowserRunning()) {
|
|
1329
|
-
await be.browserOpen('https://example.com', { waitForLoad: true });
|
|
1330
|
-
}
|
|
1331
|
-
// Build search results HTML
|
|
1332
|
-
const esc = (s) => (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1333
|
-
const htmlItems = result.results.map((r, i) =>
|
|
1334
|
-
`<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>`
|
|
1335
|
-
).join('');
|
|
1336
|
-
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>`;
|
|
1337
|
-
// Render by injecting HTML into current page via JS
|
|
1338
|
-
await be.browserEval(`document.open();document.write(${JSON.stringify(fullHtml)});document.close();`);
|
|
1339
|
-
await new Promise(r => setTimeout(r, 300));
|
|
1340
|
-
const ss = await be.browserScreenshot({ fullPage: false, format: 'jpeg', quality: 75 });
|
|
1341
|
-
if (!ss.error) {
|
|
1342
|
-
// Save to disk for persistence
|
|
1343
|
-
const ssDir = path.join(os.homedir(), '.nha', 'screenshots');
|
|
1344
|
-
const fsMod = await import('fs');
|
|
1345
|
-
fsMod.mkdirSync(ssDir, { recursive: true });
|
|
1346
|
-
const ssFilename = `ss-${Date.now()}.jpg`;
|
|
1347
|
-
fsMod.writeFileSync(path.join(ssDir, ssFilename), Buffer.from(ss.base64, 'base64'));
|
|
1348
|
-
return textResult + `\n\n[Screenshot of results captured (${Math.round(ss.size / 1024)}KB) file:${ssFilename}]`;
|
|
1349
|
-
}
|
|
1350
|
-
} catch { /* screenshot failed */ }
|
|
1119
|
+
case 'cron_remove': {
|
|
1120
|
+
const { removeCronJob } = await import('./ops-daemon.mjs');
|
|
1121
|
+
const result = removeCronJob(params.index || params.id);
|
|
1122
|
+
if (result.ok) {
|
|
1123
|
+
return `Removed scheduled task: "${result.removed.schedule}" → "${result.removed.prompt}"`;
|
|
1351
1124
|
}
|
|
1352
|
-
|
|
1353
|
-
return textResult;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
case 'fetch_url': {
|
|
1357
|
-
const wt = await import('./web-tools.mjs');
|
|
1358
|
-
const url = params.url;
|
|
1359
|
-
if (!url) return 'A URL is required.';
|
|
1360
|
-
|
|
1361
|
-
const result = await wt.fetchUrl(url);
|
|
1362
|
-
if (result.error) return `Fetch error: ${result.message}`;
|
|
1363
|
-
|
|
1364
|
-
const lines = [];
|
|
1365
|
-
if (result.title) lines.push(`Title: ${result.title}`);
|
|
1366
|
-
lines.push(`URL: ${result.url || url}`);
|
|
1367
|
-
lines.push(`Status: ${result.status}`);
|
|
1368
|
-
if (result.truncated) lines.push('[Content was truncated due to size limits]');
|
|
1369
|
-
lines.push('');
|
|
1370
|
-
lines.push(result.body);
|
|
1371
|
-
|
|
1372
|
-
return lines.join('\n');
|
|
1125
|
+
return `Failed to remove: ${result.error}`;
|
|
1373
1126
|
}
|
|
1374
1127
|
|
|
1375
1128
|
default:
|