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.
- package/dist/channels/telegram.js +23 -7
- package/dist/config/paths.js +1 -0
- package/dist/devkit/index.js +1 -0
- package/dist/devkit/tools/browser.js +388 -0
- package/dist/runtime/apoc.js +117 -1
- package/dist/runtime/oracle.js +72 -0
- package/dist/runtime/tools/apoc-tool.js +2 -0
- package/package.json +2 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/config/paths.js
CHANGED
|
@@ -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
|
};
|
package/dist/devkit/index.js
CHANGED
|
@@ -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);
|
package/dist/runtime/apoc.js
CHANGED
|
@@ -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);
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -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.
|
|
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",
|