morpheus-cli 0.4.13 → 0.4.15

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.
@@ -5,7 +5,6 @@ import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
7
  import { spawn } from 'child_process';
8
- import { convert } from 'telegram-markdown-v2';
9
8
  import { ConfigManager } from '../config/manager.js';
10
9
  import { DisplayManager } from '../runtime/display.js';
11
10
  import { createTelephonist } from '../runtime/telephonist.js';
@@ -21,8 +20,20 @@ import { Construtor } from '../runtime/tools/factory.js';
21
20
  * Truncates to Telegram's 4096-char hard limit.
22
21
  * Use for dynamic LLM/Oracle output.
23
22
  */
24
- function toMd(text) {
23
+ // Cached dynamic import of telegram-markdown-v2 (ESM-safe, loaded once on first use)
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ let _convertFn = null;
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ async function getConvert() {
28
+ if (!_convertFn) {
29
+ const mod = await import('telegram-markdown-v2');
30
+ _convertFn = mod.convert;
31
+ }
32
+ return _convertFn;
33
+ }
34
+ async function toMd(text) {
25
35
  const MAX = 4096;
36
+ const convert = await getConvert();
26
37
  const converted = convert(text, 'escape');
27
38
  const safe = converted.length > MAX ? converted.slice(0, MAX - 3) + '\\.\\.\\.' : converted;
28
39
  return { text: safe, parse_mode: 'MarkdownV2' };
@@ -116,10 +127,15 @@ export class TelegramAdapter {
116
127
  const response = await this.oracle.chat(text);
117
128
  if (response) {
118
129
  try {
119
- await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
130
+ await ctx.reply((await toMd(response)).text, { parse_mode: 'MarkdownV2' });
120
131
  }
121
132
  catch {
122
- await ctx.reply(response);
133
+ try {
134
+ ctx.reply(response, { parse_mode: 'MarkdownV2' });
135
+ }
136
+ catch {
137
+ await ctx.reply(response);
138
+ }
123
139
  }
124
140
  this.display.log(`Responded to @${user}: ${response}`, { source: 'Telegram' });
125
141
  }
@@ -200,7 +216,7 @@ export class TelegramAdapter {
200
216
  // }
201
217
  if (response) {
202
218
  try {
203
- await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
219
+ await ctx.reply((await toMd(response)).text, { parse_mode: 'MarkdownV2' });
204
220
  }
205
221
  catch {
206
222
  await ctx.reply(response);
@@ -418,7 +434,7 @@ export class TelegramAdapter {
418
434
  return;
419
435
  }
420
436
  // toMd() already truncates to 4096 chars (Telegram's hard limit)
421
- const { text: mdText, parse_mode } = toMd(text);
437
+ const { text: mdText, parse_mode } = await toMd(text);
422
438
  for (const userId of allowedUsers) {
423
439
  try {
424
440
  await this.bot.telegram.sendMessage(userId, mdText, { parse_mode });
@@ -745,7 +761,7 @@ How can I assist you today?`;
745
761
  let response = await this.oracle.chat(prompt);
746
762
  if (response) {
747
763
  try {
748
- await ctx.reply(toMd(response).text, { parse_mode: 'MarkdownV2' });
764
+ await ctx.reply((await toMd(response)).text, { parse_mode: 'MarkdownV2' });
749
765
  }
750
766
  catch {
751
767
  await ctx.reply(response);
@@ -11,6 +11,7 @@ export const PATHS = {
11
11
  logs: LOGS_DIR,
12
12
  memory: path.join(MORPHEUS_ROOT, 'memory'),
13
13
  cache: path.join(MORPHEUS_ROOT, 'cache'),
14
+ browser: path.join(MORPHEUS_ROOT, 'cache', 'browser'),
14
15
  commands: path.join(MORPHEUS_ROOT, 'commands'),
15
16
  mcps: path.join(MORPHEUS_ROOT, 'mcps.json'),
16
17
  };
@@ -7,4 +7,5 @@ import './tools/network.js';
7
7
  import './tools/git.js';
8
8
  import './tools/packages.js';
9
9
  import './tools/system.js';
10
+ import './tools/browser.js';
10
11
  export { buildDevKit } from './registry.js';
@@ -0,0 +1,388 @@
1
+ import { tool } from '@langchain/core/tools';
2
+ import { z } from 'zod';
3
+ import { truncateOutput } from '../utils.js';
4
+ import { registerToolFactory } from '../registry.js';
5
+ import { PATHS } from '../../config/paths.js';
6
+ // ─── Module-level browser singleton ────────────────────────────────────────
7
+ let browserInstance = null;
8
+ let pageInstance = null;
9
+ let idleTimer = null;
10
+ let installPromise = null;
11
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
12
+ /**
13
+ * Ensures Chromium is downloaded to ~/.morpheus/cache/browser/.
14
+ * Downloads only once; subsequent calls return the cached executablePath.
15
+ */
16
+ async function ensureChromium() {
17
+ const { install, resolveBuildId, detectBrowserPlatform, computeExecutablePath, Browser: PBrowser, } = await import('@puppeteer/browsers');
18
+ const platform = detectBrowserPlatform();
19
+ const buildId = await resolveBuildId(PBrowser.CHROME, platform, 'stable');
20
+ // Check if already installed
21
+ const execPath = computeExecutablePath({
22
+ browser: PBrowser.CHROME,
23
+ buildId,
24
+ cacheDir: PATHS.browser,
25
+ });
26
+ const { default: fs } = await import('fs-extra');
27
+ if (await fs.pathExists(execPath)) {
28
+ return execPath;
29
+ }
30
+ // Download with progress indicator
31
+ process.stdout.write('[Morpheus] Installing Chromium for browser tools (first run, ~150MB)...\n');
32
+ const installed = await install({
33
+ browser: PBrowser.CHROME,
34
+ buildId,
35
+ cacheDir: PATHS.browser,
36
+ downloadProgressCallback: (downloaded, total) => {
37
+ const pct = total > 0 ? Math.round((downloaded / total) * 100) : 0;
38
+ process.stdout.write(`\r[Morpheus] Downloading Chromium: ${pct}% `);
39
+ },
40
+ });
41
+ process.stdout.write('\n[Morpheus] Chromium installed successfully.\n');
42
+ return installed.executablePath;
43
+ }
44
+ /**
45
+ * Returns (or creates) the browser singleton, resetting the idle timer.
46
+ * Handles Chromium lazy-install with a lock to prevent concurrent downloads.
47
+ */
48
+ async function acquireBrowser() {
49
+ const { launch } = await import('puppeteer-core');
50
+ const needsLaunch = !browserInstance || !browserInstance.connected;
51
+ if (needsLaunch) {
52
+ if (!installPromise) {
53
+ installPromise = ensureChromium().finally(() => {
54
+ installPromise = null;
55
+ });
56
+ }
57
+ const executablePath = await installPromise;
58
+ // Re-check after awaiting (another caller may have launched already)
59
+ if (!browserInstance || !browserInstance.connected) {
60
+ browserInstance = await launch({
61
+ executablePath,
62
+ headless: true,
63
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'],
64
+ });
65
+ pageInstance = await browserInstance.newPage();
66
+ }
67
+ }
68
+ else if (!pageInstance || pageInstance.isClosed()) {
69
+ pageInstance = await browserInstance.newPage();
70
+ }
71
+ // Reset idle timeout
72
+ if (idleTimer)
73
+ clearTimeout(idleTimer);
74
+ idleTimer = setTimeout(async () => {
75
+ try {
76
+ await pageInstance?.close();
77
+ }
78
+ catch { /* ignore */ }
79
+ try {
80
+ await browserInstance?.close();
81
+ }
82
+ catch { /* ignore */ }
83
+ pageInstance = null;
84
+ browserInstance = null;
85
+ idleTimer = null;
86
+ }, IDLE_TIMEOUT_MS);
87
+ return { browser: browserInstance, page: pageInstance };
88
+ }
89
+ // Best-effort cleanup on process exit
90
+ process.on('exit', () => {
91
+ try {
92
+ browserInstance?.process()?.kill();
93
+ }
94
+ catch { /* ignore */ }
95
+ });
96
+ // ─── Tool Definitions ───────────────────────────────────────────────────────
97
+ const browserNavigateTool = tool(async ({ url, wait_until, timeout_ms, return_html }) => {
98
+ try {
99
+ const { page } = await acquireBrowser();
100
+ await page.goto(url, {
101
+ waitUntil: (wait_until ?? 'domcontentloaded'),
102
+ timeout: timeout_ms ?? 30_000,
103
+ });
104
+ const title = await page.title();
105
+ const text = await page.evaluate(() => document.body.innerText);
106
+ const result = {
107
+ success: true,
108
+ url,
109
+ current_url: page.url(),
110
+ title,
111
+ text: truncateOutput(text),
112
+ };
113
+ if (return_html) {
114
+ result.html = truncateOutput(await page.content());
115
+ }
116
+ return JSON.stringify(result);
117
+ }
118
+ catch (err) {
119
+ return JSON.stringify({ success: false, url, error: err.message });
120
+ }
121
+ }, {
122
+ name: 'browser_navigate',
123
+ description: 'Navigate to a URL in a real browser (executes JavaScript). Use instead of http_request for SPAs, JS-heavy pages, or sites requiring interaction. Returns page title and text content.',
124
+ schema: z.object({
125
+ url: z.string().describe('Full URL to navigate to (must include https://)'),
126
+ wait_until: z
127
+ .enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2'])
128
+ .optional()
129
+ .describe('Wait condition. Default: domcontentloaded. Use networkidle0 for SPAs.'),
130
+ timeout_ms: z.number().optional().describe('Navigation timeout in ms. Default: 30000'),
131
+ return_html: z
132
+ .boolean()
133
+ .optional()
134
+ .describe('Also return raw HTML in response. Default: false'),
135
+ }),
136
+ });
137
+ const browserGetDomTool = tool(async ({ selector, include_attributes }) => {
138
+ try {
139
+ const { page } = await acquireBrowser();
140
+ const includeAttrs = include_attributes ?? true;
141
+ const dom = await page.evaluate(({ sel, attrs }) => {
142
+ const root = sel
143
+ ? document.querySelector(sel)
144
+ : document.body;
145
+ if (!root)
146
+ return null;
147
+ const RELEVANT_ATTRS = [
148
+ 'href', 'src', 'type', 'name', 'value',
149
+ 'placeholder', 'action', 'id', 'role', 'aria-label',
150
+ ];
151
+ function serialize(el, depth) {
152
+ const hasChildren = el.children.length > 0;
153
+ const node = {
154
+ tag: el.tagName.toLowerCase(),
155
+ };
156
+ if (el.id)
157
+ node.id = el.id;
158
+ if (el.className)
159
+ node.class = el.className;
160
+ if (!hasChildren) {
161
+ const txt = el.textContent?.trim();
162
+ if (txt)
163
+ node.text = txt.slice(0, 120);
164
+ }
165
+ if (attrs && el.attributes.length > 0) {
166
+ const attrMap = {};
167
+ for (const attr of el.attributes) {
168
+ if (RELEVANT_ATTRS.includes(attr.name)) {
169
+ attrMap[attr.name] = attr.value;
170
+ }
171
+ }
172
+ if (Object.keys(attrMap).length)
173
+ node.attrs = attrMap;
174
+ }
175
+ if (depth < 6 && hasChildren) {
176
+ node.children = Array.from(el.children)
177
+ .slice(0, 40)
178
+ .map((c) => serialize(c, depth + 1));
179
+ }
180
+ return node;
181
+ }
182
+ return serialize(root, 0);
183
+ }, { sel: selector ?? null, attrs: includeAttrs });
184
+ if (!dom) {
185
+ return JSON.stringify({ success: false, error: `Element not found: ${selector}` });
186
+ }
187
+ return JSON.stringify({ success: true, current_url: page.url(), dom: truncateOutput(JSON.stringify(dom, null, 2)) });
188
+ }
189
+ catch (err) {
190
+ return JSON.stringify({ success: false, error: err.message });
191
+ }
192
+ }, {
193
+ name: 'browser_get_dom',
194
+ description: 'Get a simplified DOM tree of the current page or a specific element. ' +
195
+ 'ALWAYS call this BEFORE browser_click or browser_fill to inspect page structure and identify the correct CSS selectors. ' +
196
+ 'Never guess selectors — analyze the DOM first.',
197
+ schema: z.object({
198
+ selector: z
199
+ .string()
200
+ .optional()
201
+ .describe('CSS selector to scope the DOM tree to. Omit to get the full body.'),
202
+ include_attributes: z
203
+ .boolean()
204
+ .optional()
205
+ .describe('Include relevant attributes (href, src, type, name, value, placeholder, role, aria-label). Default: true'),
206
+ }),
207
+ });
208
+ const browserClickTool = tool(async ({ selector, text, timeout_ms, wait_after_ms }) => {
209
+ try {
210
+ const { page } = await acquireBrowser();
211
+ if (!selector && !text) {
212
+ return JSON.stringify({ success: false, error: 'Provide either selector or text' });
213
+ }
214
+ const clickTimeout = timeout_ms ?? 10_000;
215
+ if (text) {
216
+ // Use Puppeteer pseudo-selector to find element by visible text
217
+ await page.locator(`::-p-text(${text})`).setTimeout(clickTimeout).click();
218
+ }
219
+ else {
220
+ await page.locator(selector).setTimeout(clickTimeout).click();
221
+ }
222
+ if (wait_after_ms) {
223
+ await new Promise((r) => setTimeout(r, wait_after_ms));
224
+ }
225
+ return JSON.stringify({
226
+ success: true,
227
+ current_url: page.url(),
228
+ title: await page.title(),
229
+ });
230
+ }
231
+ catch (err) {
232
+ return JSON.stringify({ success: false, error: err.message });
233
+ }
234
+ }, {
235
+ name: 'browser_click',
236
+ description: 'Click an element on the current browser page by CSS selector or visible text. ' +
237
+ 'The page must already be loaded via browser_navigate. ' +
238
+ 'Always inspect the DOM with browser_get_dom first to find the correct selector.',
239
+ schema: z.object({
240
+ selector: z
241
+ .string()
242
+ .optional()
243
+ .describe('CSS selector of the element to click (e.g. "button#submit", ".btn-login")'),
244
+ text: z
245
+ .string()
246
+ .optional()
247
+ .describe('Click element containing this visible text (alternative to selector)'),
248
+ timeout_ms: z
249
+ .number()
250
+ .optional()
251
+ .describe('Timeout to wait for the element in ms. Default: 10000'),
252
+ wait_after_ms: z
253
+ .number()
254
+ .optional()
255
+ .describe('Wait this many ms after clicking (for page transitions/animations). Default: 0'),
256
+ }),
257
+ });
258
+ const browserFillTool = tool(async ({ selector, value, press_enter, timeout_ms }) => {
259
+ try {
260
+ const { page } = await acquireBrowser();
261
+ await page.locator(selector).setTimeout(timeout_ms ?? 10_000).fill(value);
262
+ if (press_enter) {
263
+ await page.keyboard.press('Enter');
264
+ }
265
+ return JSON.stringify({ success: true, selector, filled: true });
266
+ }
267
+ catch (err) {
268
+ return JSON.stringify({ success: false, selector, error: err.message });
269
+ }
270
+ }, {
271
+ name: 'browser_fill',
272
+ description: 'Fill a form input or textarea field with a value. Clears any existing content first. ' +
273
+ 'Always inspect the DOM with browser_get_dom first to identify the correct CSS selector.',
274
+ schema: z.object({
275
+ selector: z.string().describe('CSS selector of the input/textarea element'),
276
+ value: z.string().describe('Value to type into the field'),
277
+ press_enter: z
278
+ .boolean()
279
+ .optional()
280
+ .describe('Press Enter after filling (triggers form submit in many cases). Default: false'),
281
+ timeout_ms: z
282
+ .number()
283
+ .optional()
284
+ .describe('Timeout to find the element in ms. Default: 10000'),
285
+ }),
286
+ });
287
+ /**
288
+ * Search via DuckDuckGo Lite (plain HTML, no JS, no bot detection).
289
+ * Uses a simple POST fetch — no browser required, much faster and more reliable
290
+ * than headless browser scraping of Google.
291
+ *
292
+ * DDG Lite returns results as: href="URL" class='result-link'>TITLE</a>
293
+ * and <td class='result-snippet'>SNIPPET</td>, paired by index.
294
+ * Sponsored links have URLs starting with "https://duckduckgo.com/y.js" and are filtered out.
295
+ */
296
+ const browserSearchTool = tool(async ({ query, num_results, language }) => {
297
+ try {
298
+ const max = num_results ?? 10;
299
+ // DDG region codes: "br-pt" for Brazil/Portuguese, "us-en" for US/English, etc.
300
+ // Map from simple lang code to DDG kl param
301
+ const regionMap = {
302
+ pt: 'br-pt', br: 'br-pt',
303
+ en: 'us-en', us: 'us-en',
304
+ es: 'es-es', fr: 'fr-fr',
305
+ de: 'de-de', it: 'it-it',
306
+ jp: 'jp-jp', ar: 'ar-es',
307
+ };
308
+ const lang = language ?? 'pt';
309
+ const kl = regionMap[lang] ?? lang;
310
+ const body = new URLSearchParams({ q: query, kl }).toString();
311
+ const res = await fetch('https://lite.duckduckgo.com/lite/', {
312
+ method: 'POST',
313
+ headers: {
314
+ 'Content-Type': 'application/x-www-form-urlencoded',
315
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
316
+ '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
317
+ },
318
+ body,
319
+ signal: AbortSignal.timeout(20_000),
320
+ });
321
+ if (!res.ok) {
322
+ return JSON.stringify({ success: false, query, error: `HTTP ${res.status}` });
323
+ }
324
+ const html = await res.text();
325
+ // Extract all result-link anchors (href uses double quotes, class uses single quotes)
326
+ const linkPattern = /href="(https?:\/\/[^"]+)"[^>]*class='result-link'>([^<]+)<\/a>/g;
327
+ const snippetPattern = /class='result-snippet'>([\s\S]*?)<\/td>/g;
328
+ const allLinks = [...html.matchAll(linkPattern)];
329
+ const allSnippets = [...html.matchAll(snippetPattern)];
330
+ // Pair links with snippets by index, filtering sponsored (DDG y.js redirect URLs)
331
+ const results = [];
332
+ for (let i = 0; i < allLinks.length && results.length < max; i++) {
333
+ const url = allLinks[i][1];
334
+ const title = allLinks[i][2].trim();
335
+ // Skip sponsored ads (redirected through duckduckgo.com/y.js)
336
+ if (url.startsWith('https://duckduckgo.com/'))
337
+ continue;
338
+ const snippet = allSnippets[i]
339
+ ? allSnippets[i][1].replace(/<[^>]+>/g, '').trim()
340
+ : '';
341
+ results.push({ title, url, snippet });
342
+ }
343
+ if (results.length === 0) {
344
+ return JSON.stringify({
345
+ success: false,
346
+ query,
347
+ error: 'No results found. The query may be too specific or DDG returned an unexpected response.',
348
+ });
349
+ }
350
+ return JSON.stringify({ success: true, query, results });
351
+ }
352
+ catch (err) {
353
+ return JSON.stringify({ success: false, query, error: err.message });
354
+ }
355
+ }, {
356
+ name: 'browser_search',
357
+ description: 'Search the internet using DuckDuckGo and return structured results (title, URL, snippet). ' +
358
+ 'Use this when you need to find current information, news, articles, documentation, or any web content. ' +
359
+ 'Returns up to 10 results by default. Does NOT require browser_navigate first — it is self-contained and fast.',
360
+ schema: z.object({
361
+ query: z.string().describe('Search query'),
362
+ num_results: z
363
+ .number()
364
+ .int()
365
+ .min(1)
366
+ .max(20)
367
+ .optional()
368
+ .describe('Number of results to return. Default: 10, max: 20'),
369
+ language: z
370
+ .string()
371
+ .optional()
372
+ .describe('Language/region code (e.g. "pt" for Portuguese/Brazil, "en" for English). Default: "pt"'),
373
+ }),
374
+ });
375
+ // ─── Factory ────────────────────────────────────────────────────────────────
376
+ export function createBrowserTools(_ctx) {
377
+ if (process.env.MORPHEUS_BROWSER_ENABLED === 'false') {
378
+ return [];
379
+ }
380
+ return [
381
+ browserNavigateTool,
382
+ browserGetDomTool,
383
+ browserClickTool,
384
+ browserFillTool,
385
+ browserSearchTool,
386
+ ];
387
+ }
388
+ registerToolFactory(createBrowserTools);
@@ -41,7 +41,7 @@ export class Apoc {
41
41
  }
42
42
  async initialize() {
43
43
  const apocConfig = this.config.apoc || this.config.llm;
44
- console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
44
+ // console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
45
45
  const working_dir = this.config.apoc?.working_dir || process.cwd();
46
46
  const timeout_ms = this.config.apoc?.timeout_ms || 30_000;
47
47
  // Import all devkit tool factories (side-effect registration)
@@ -86,6 +86,8 @@ Available capabilities:
86
86
  - Perform network operations (curl, DNS, ping)
87
87
  - Manage packages (npm, yarn)
88
88
  - Inspect system information
89
+ - Navigate websites, inspect DOM, click elements, fill forms using a real browser (for JS-heavy pages and SPAs)
90
+ - Search the internet with browser_search (DuckDuckGo, returns structured results)
89
91
 
90
92
  OPERATING RULES:
91
93
  1. Use tools to accomplish the task. Do not speculate.
@@ -94,6 +96,120 @@ OPERATING RULES:
94
96
  4. If something fails, report the error and what you tried.
95
97
  5. Stay focused on the delegated task only.
96
98
 
99
+
100
+ ────────────────────────────────────────
101
+ BROWSER AUTOMATION PROTOCOL
102
+ ────────────────────────────────────────
103
+
104
+ When using browser tools (browser_navigate, browser_get_dom, browser_click, browser_fill), follow this protocol exactly.
105
+
106
+ GENERAL PRINCIPLES
107
+ - Never guess selectors.
108
+ - Never assume page state.
109
+ - Always verify page transitions.
110
+ - Always extract evidence of success.
111
+ - If required user data is missing, STOP and return to Oracle immediately.
112
+
113
+ PHASE 1 — Navigation
114
+ 1. ALWAYS call browser_navigate first.
115
+ 2. Use:
116
+ - wait_until: "networkidle0" for SPAs or JS-heavy pages.
117
+ - wait_until: "domcontentloaded" for simple pages.
118
+ 3. After navigation, confirm current_url and title.
119
+ 4. If navigation fails, report the error and stop.
120
+
121
+ PHASE 2 — DOM Inspection (MANDATORY BEFORE ACTION)
122
+ 1. ALWAYS call browser_get_dom before browser_click or browser_fill.
123
+ 2. Identify stable selectors (prefer id > name > role > unique class).
124
+ 3. Understand page structure and expected flow before interacting.
125
+ 4. Never click or fill blindly.
126
+
127
+ PHASE 3 — Interaction
128
+ When clicking:
129
+ - Prefer stable selectors.
130
+ - If ambiguous, refine selector.
131
+ - Use visible text only if selector is unstable.
132
+
133
+ When filling:
134
+ - Confirm correct input field via DOM.
135
+ - Fill field.
136
+ - Submit using press_enter OR clicking submit button.
137
+
138
+ If login or personal data is required:
139
+ STOP and return required fields clearly.
140
+
141
+ PHASE 4 — State Verification (MANDATORY)
142
+ After ANY interaction:
143
+ 1. Call browser_get_dom again.
144
+ 2. Verify URL change or content change.
145
+ 3. Confirm success or detect error message.
146
+
147
+ If expected change did not occur:
148
+ - Reinspect DOM.
149
+ - Attempt one justified alternative.
150
+ - If still failing, report failure clearly.
151
+
152
+ Maximum 2 attempts per step.
153
+ Never assume success.
154
+
155
+ PHASE 5 — Reporting
156
+ Include:
157
+ - Step-by-step actions
158
+ - Final URL
159
+ - Evidence of success
160
+ - Errors encountered
161
+ - Completion status (true/false)
162
+
163
+
164
+ ────────────────────────────────────────
165
+ WEB RESEARCH PROTOCOL
166
+ ────────────────────────────────────────
167
+
168
+ When using browser_search for factual verification, follow this protocol strictly.
169
+
170
+ PHASE 1 — Query Design
171
+ 1. Identify core entity, information type, and time constraint.
172
+ 2. Build a precise search query.
173
+ 3. If time-sensitive, include the current year.
174
+
175
+ PHASE 2 — Source Discovery
176
+ 1. Call browser_search.
177
+ 2. Collect results.
178
+ 3. Prioritize:
179
+ - Official sources
180
+ - Major authoritative publications
181
+ 4. Reformulate query if necessary.
182
+
183
+ PHASE 3 — Source Validation (MANDATORY)
184
+ 1. Open at least 3 distinct URLs with browser_navigate.
185
+ 2. Read actual page content.
186
+ 3. NEVER rely only on search snippets.
187
+ 4. Ignore inaccessible pages.
188
+
189
+ PHASE 4 — Cross-Verification
190
+ 1. Extract relevant information from each source.
191
+ 2. Compare findings:
192
+ - Agreement → verified
193
+ - Minor differences → report variation
194
+ - Conflict → report discrepancy
195
+ 3. Require confirmation from at least 2 reliable sources.
196
+ 4. If not confirmed, state clearly:
197
+ "Information could not be confidently verified."
198
+
199
+ PHASE 5 — Structured Report
200
+ Include:
201
+ - Direct answer
202
+ - Short explanation
203
+ - Source URLs
204
+ - Confidence level (High / Medium / Low)
205
+
206
+ ANTI-HALLUCINATION RULES
207
+ - Never answer from prior knowledge without verification.
208
+ - Never stop after reading only one source.
209
+ - Treat time-sensitive information as volatile.
210
+
211
+
212
+
97
213
  ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
98
214
  `);
99
215
  const userMessage = new HumanMessage(task);
@@ -165,6 +165,78 @@ If a tool can compute, fetch, inspect, or verify something, prefer tool usage.
165
165
 
166
166
  Never hallucinate values retrievable via tools.
167
167
 
168
+ --------------------------------------------------
169
+ DELEGATION DECISION PROTOCOL
170
+ --------------------------------------------------
171
+
172
+ Before responding, classify the request into one of the following categories:
173
+
174
+ CATEGORY A — Execution / System / External State
175
+ If the request involves:
176
+ - Filesystem access
177
+ - Code execution
178
+ - Git operations
179
+ - Package management
180
+ - Process inspection
181
+ - Networking
182
+ - Environment state
183
+ - Browser automation
184
+ - Web navigation
185
+ - Web research
186
+ - Fact verification
187
+ - Current or time-sensitive data
188
+
189
+ You MUST delegate to the appropriate tool (e.g., apoc_delegate).
190
+
191
+ CATEGORY B — Factual Question Requiring Verification
192
+ If the question involves:
193
+ - Rankings
194
+ - Results
195
+ - Versions
196
+ - News
197
+ - Public figures
198
+ - Statistics
199
+ - Events
200
+ - Anything that may have changed over time
201
+
202
+ You MUST delegate to Apoc for verification.
203
+
204
+ CATEGORY C — Pure Reasoning / Conceptual
205
+ If the request can be fully answered through reasoning alone without external validation,
206
+ you may answer directly without delegating.
207
+
208
+ If uncertainty exists about whether verification is required,
209
+ DELEGATE.
210
+
211
+ --------------------------------------------------
212
+ APOC DELEGATION STANDARD
213
+ --------------------------------------------------
214
+
215
+ When delegating to Apoc:
216
+
217
+ - Provide a clear objective.
218
+ - Specify verification requirements if factual.
219
+ - Define expected output structure if needed.
220
+ - Pass relevant context from the conversation.
221
+ - Never send vague tasks.
222
+
223
+ Weak delegation example (forbidden):
224
+ "Search who won the championship."
225
+
226
+ Correct delegation example:
227
+ "Find who won the 2023 NBA Championship. Verify using at least 3 reliable sources. Provide URLs and confidence level."
228
+
229
+ --------------------------------------------------
230
+ UNCERTAINTY RULE
231
+ --------------------------------------------------
232
+
233
+ If the answer cannot be produced with high confidence without external verification,
234
+ you MUST delegate.
235
+
236
+ Never fabricate certainty.
237
+ Never simulate tool results.
238
+ When in doubt, delegate.
239
+
168
240
 
169
241
  7. FINAL ANSWER POLICY
170
242
 
@@ -33,6 +33,8 @@ Use this tool when the user asks for ANY of the following:
33
33
  - Process management: list processes, kill processes, check ports
34
34
  - Network: ping hosts, curl URLs, DNS lookups
35
35
  - System info: environment variables, OS info, disk space, memory
36
+ - Internet search: search DuckDuckGo and verify facts by reading at least 3 sources via browser_navigate before reporting results.
37
+ - Browser automation: navigate websites (JS/SPA), inspect DOM, click elements, fill forms. Apoc will ask for missing user input (e.g. credentials, form fields) before proceeding.
36
38
 
37
39
  Provide a clear natural language task description. Optionally provide context
38
40
  from the current conversation to help Apoc understand the broader goal.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morpheus-cli",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "Morpheus is a local AI agent for developers, running as a CLI daemon that connects to LLMs, local tools, and MCPs, enabling interaction via Terminal, Telegram, and Discord. Inspired by the character Morpheus from *The Matrix*, the project acts as an intelligent orchestrator, bridging the gap between the developer and complex systems.",
5
5
  "bin": {
6
6
  "morpheus": "./bin/morpheus.js"
@@ -50,6 +50,7 @@
50
50
  "mcp-remote": "^0.1.38",
51
51
  "open": "^11.0.0",
52
52
  "ora": "^9.1.0",
53
+ "puppeteer-core": "^23.11.1",
53
54
  "sqlite-vec": "^0.1.7-alpha.2",
54
55
  "telegraf": "^4.16.3",
55
56
  "telegram-markdown-v2": "^0.0.4",