brownian-code 2026.2.10
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/LICENSE +21 -0
- package/README.md +97 -0
- package/bin/brownian +25 -0
- package/env.example +21 -0
- package/package.json +87 -0
- package/src/agent/agent.test.ts +414 -0
- package/src/agent/agent.ts +385 -0
- package/src/agent/index.ts +27 -0
- package/src/agent/prompts.ts +271 -0
- package/src/agent/scratchpad.test.ts +482 -0
- package/src/agent/scratchpad.ts +526 -0
- package/src/agent/token-counter.test.ts +59 -0
- package/src/agent/token-counter.ts +33 -0
- package/src/agent/types.ts +137 -0
- package/src/cli.tsx +385 -0
- package/src/commands/builtin.test.ts +271 -0
- package/src/commands/builtin.ts +200 -0
- package/src/commands/registry.test.ts +188 -0
- package/src/commands/registry.ts +111 -0
- package/src/commands/types.ts +64 -0
- package/src/components/AgentEventView.tsx +487 -0
- package/src/components/AnswerBox.tsx +81 -0
- package/src/components/ApiKeyPrompt.tsx +75 -0
- package/src/components/CommandMenu.test.tsx +64 -0
- package/src/components/CommandMenu.tsx +38 -0
- package/src/components/CursorText.tsx +43 -0
- package/src/components/DebugPanel.tsx +48 -0
- package/src/components/ErrorBox.test.tsx +58 -0
- package/src/components/ErrorBox.tsx +26 -0
- package/src/components/HelpView.test.tsx +70 -0
- package/src/components/HelpView.tsx +61 -0
- package/src/components/HistoryItemView.tsx +108 -0
- package/src/components/Input.tsx +193 -0
- package/src/components/Intro.test.tsx +59 -0
- package/src/components/Intro.tsx +35 -0
- package/src/components/ModelSelector.tsx +288 -0
- package/src/components/StatusBar.test.tsx +78 -0
- package/src/components/StatusBar.tsx +56 -0
- package/src/components/WorkingIndicator.tsx +133 -0
- package/src/components/index.ts +23 -0
- package/src/e2e/agent-flow.test.ts +378 -0
- package/src/evals/components/EvalApp.tsx +206 -0
- package/src/evals/components/EvalCurrentQuestion.tsx +42 -0
- package/src/evals/components/EvalProgress.tsx +33 -0
- package/src/evals/components/EvalRecentResults.tsx +63 -0
- package/src/evals/components/EvalStats.tsx +49 -0
- package/src/evals/components/index.ts +5 -0
- package/src/evals/dataset/crypto_agent.csv +16 -0
- package/src/evals/run.ts +355 -0
- package/src/gateway/channels/whatsapp/auth-store.ts +15 -0
- package/src/gateway/channels/whatsapp/inbound.ts +86 -0
- package/src/gateway/channels/whatsapp/login.ts +28 -0
- package/src/gateway/channels/whatsapp/outbound.ts +27 -0
- package/src/gateway/channels/whatsapp/session.ts +69 -0
- package/src/gateway/config.ts +81 -0
- package/src/gateway/index.ts +62 -0
- package/src/hooks/useAgentRunner.ts +317 -0
- package/src/hooks/useDebugLogs.ts +22 -0
- package/src/hooks/useInputHistory.ts +106 -0
- package/src/hooks/useModelSelection.ts +249 -0
- package/src/hooks/useTextBuffer.test.ts +121 -0
- package/src/hooks/useTextBuffer.ts +97 -0
- package/src/index.tsx +74 -0
- package/src/mcp/cache.ts +205 -0
- package/src/mcp/client.test.ts +126 -0
- package/src/mcp/client.ts +145 -0
- package/src/mcp/index.ts +2 -0
- package/src/model/llm.test.ts +158 -0
- package/src/model/llm.ts +233 -0
- package/src/providers.ts +94 -0
- package/src/skills/index.ts +17 -0
- package/src/skills/loader.ts +73 -0
- package/src/skills/registry.ts +125 -0
- package/src/skills/types.ts +31 -0
- package/src/test-utils/mocks.ts +110 -0
- package/src/theme.ts +21 -0
- package/src/tools/browser/browser.ts +357 -0
- package/src/tools/browser/index.ts +1 -0
- package/src/tools/crypto/hive-tools.ts +171 -0
- package/src/tools/crypto/index.ts +1 -0
- package/src/tools/descriptions/browser.ts +105 -0
- package/src/tools/descriptions/crypto-search.ts +58 -0
- package/src/tools/descriptions/index.ts +8 -0
- package/src/tools/descriptions/web-fetch.ts +44 -0
- package/src/tools/descriptions/web-search.ts +26 -0
- package/src/tools/fetch/cache.ts +95 -0
- package/src/tools/fetch/external-content.ts +200 -0
- package/src/tools/fetch/index.ts +1 -0
- package/src/tools/fetch/web-fetch-utils.ts +122 -0
- package/src/tools/fetch/web-fetch.ts +371 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/registry.ts +130 -0
- package/src/tools/search/exa.ts +43 -0
- package/src/tools/search/index.ts +2 -0
- package/src/tools/search/tavily.ts +35 -0
- package/src/tools/skill.ts +62 -0
- package/src/tools/types.ts +53 -0
- package/src/utils/ai-message.ts +26 -0
- package/src/utils/config.ts +54 -0
- package/src/utils/cost-calculator.test.ts +101 -0
- package/src/utils/cost-calculator.ts +74 -0
- package/src/utils/env.ts +101 -0
- package/src/utils/error-classifier.test.ts +146 -0
- package/src/utils/error-classifier.ts +91 -0
- package/src/utils/in-memory-chat-history.test.ts +291 -0
- package/src/utils/in-memory-chat-history.ts +224 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/input-key-handlers.test.ts +155 -0
- package/src/utils/input-key-handlers.ts +64 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/long-term-chat-history.ts +138 -0
- package/src/utils/markdown-table.ts +227 -0
- package/src/utils/ollama.ts +37 -0
- package/src/utils/progress-channel.ts +84 -0
- package/src/utils/text-navigation.test.ts +222 -0
- package/src/utils/text-navigation.ts +81 -0
- package/src/utils/thinking-verbs.ts +29 -0
- package/src/utils/tokens.test.ts +163 -0
- package/src/utils/tokens.ts +67 -0
- package/src/utils/tool-description.ts +88 -0
package/src/theme.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const colors = {
|
|
2
|
+
primary: '#258bff',
|
|
3
|
+
primaryLight: '#a5cfff',
|
|
4
|
+
success: 'green',
|
|
5
|
+
error: 'red',
|
|
6
|
+
warning: 'yellow',
|
|
7
|
+
muted: '#a6a6a6',
|
|
8
|
+
mutedDark: '#303030',
|
|
9
|
+
accent: 'cyan',
|
|
10
|
+
highlight: 'magenta',
|
|
11
|
+
white: '#ffffff',
|
|
12
|
+
info: '#6CB6FF',
|
|
13
|
+
queryBg: '#3D3D3D',
|
|
14
|
+
claude: '#E5896A',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const dimensions = {
|
|
18
|
+
boxWidth: 80,
|
|
19
|
+
introWidth: 68,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import { chromium, Browser, Page } from 'playwright';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { formatToolResult } from '../types.js';
|
|
5
|
+
import { logger } from '@/utils';
|
|
6
|
+
|
|
7
|
+
let browser: Browser | null = null;
|
|
8
|
+
let page: Page | null = null;
|
|
9
|
+
|
|
10
|
+
// Store refs from the last snapshot for action resolution
|
|
11
|
+
let currentRefs: Map<string, { role: string; name?: string; nth?: number }> = new Map();
|
|
12
|
+
|
|
13
|
+
// Type for Playwright's _snapshotForAI result
|
|
14
|
+
interface SnapshotForAIResult {
|
|
15
|
+
full?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Extended Page type with _snapshotForAI method
|
|
19
|
+
interface PageWithSnapshotForAI extends Page {
|
|
20
|
+
_snapshotForAI?: (opts: { timeout: number; track: string }) => Promise<SnapshotForAIResult>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect if running as a compiled binary (not via bun runtime).
|
|
25
|
+
*/
|
|
26
|
+
function isCompiledBinary(): boolean {
|
|
27
|
+
return !process.argv[0]?.includes('bun');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ensure browser and page are initialized.
|
|
32
|
+
* Lazily launches a headless Chromium browser on first use.
|
|
33
|
+
* If Playwright's Chromium isn't installed, attempts auto-install for source installs
|
|
34
|
+
* or provides instructions for compiled binaries.
|
|
35
|
+
*/
|
|
36
|
+
async function ensureBrowser(): Promise<Page> {
|
|
37
|
+
if (!browser) {
|
|
38
|
+
try {
|
|
39
|
+
browser = await chromium.launch({ headless: false });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
if (msg.includes('Executable doesn\'t exist') || msg.includes('browserType.launch')) {
|
|
43
|
+
if (isCompiledBinary()) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'Playwright Chromium is not installed.\n\n' +
|
|
46
|
+
'To use the browser tool, install Playwright Chromium separately:\n' +
|
|
47
|
+
' npx playwright install chromium\n\n' +
|
|
48
|
+
'This is a one-time setup required for the browser tool.'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
// Source install: attempt auto-install
|
|
52
|
+
const { execSync } = await import('child_process');
|
|
53
|
+
try {
|
|
54
|
+
execSync('bunx playwright install chromium', { stdio: 'inherit' });
|
|
55
|
+
browser = await chromium.launch({ headless: false });
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Failed to auto-install Playwright Chromium.\n\n' +
|
|
59
|
+
'Please install manually:\n' +
|
|
60
|
+
' bunx playwright install chromium'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!page) {
|
|
69
|
+
const context = await browser.newContext();
|
|
70
|
+
page = await context.newPage();
|
|
71
|
+
}
|
|
72
|
+
return page;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Close the browser and reset state.
|
|
77
|
+
*/
|
|
78
|
+
async function closeBrowser(): Promise<void> {
|
|
79
|
+
if (browser) {
|
|
80
|
+
await browser.close();
|
|
81
|
+
browser = null;
|
|
82
|
+
page = null;
|
|
83
|
+
currentRefs.clear();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse refs from the AI snapshot format.
|
|
89
|
+
* Extracts [ref=eN] patterns and builds a ref map.
|
|
90
|
+
*/
|
|
91
|
+
function parseRefsFromSnapshot(snapshot: string): Map<string, { role: string; name?: string; nth?: number }> {
|
|
92
|
+
const refs = new Map<string, { role: string; name?: string; nth?: number }>();
|
|
93
|
+
const lines = snapshot.split('\n');
|
|
94
|
+
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
// Match patterns like: - button "Click me" [ref=e12]
|
|
97
|
+
const refMatch = line.match(/\[ref=(e\d+)\]/);
|
|
98
|
+
if (!refMatch) continue;
|
|
99
|
+
|
|
100
|
+
const ref = refMatch[1];
|
|
101
|
+
|
|
102
|
+
// Extract role (first word after "- ")
|
|
103
|
+
const roleMatch = line.match(/^\s*-\s*(\w+)/);
|
|
104
|
+
const role = roleMatch ? roleMatch[1] : 'generic';
|
|
105
|
+
|
|
106
|
+
// Extract name (text in quotes)
|
|
107
|
+
const nameMatch = line.match(/"([^"]+)"/);
|
|
108
|
+
const name = nameMatch ? nameMatch[1] : undefined;
|
|
109
|
+
|
|
110
|
+
// Extract nth if present
|
|
111
|
+
const nthMatch = line.match(/\[nth=(\d+)\]/);
|
|
112
|
+
const nth = nthMatch ? parseInt(nthMatch[1], 10) : undefined;
|
|
113
|
+
|
|
114
|
+
refs.set(ref, { role, name, nth });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return refs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a ref to a Playwright locator using stored ref data.
|
|
122
|
+
*/
|
|
123
|
+
function resolveRefToLocator(p: Page, ref: string): ReturnType<Page['locator']> {
|
|
124
|
+
const refData = currentRefs.get(ref);
|
|
125
|
+
|
|
126
|
+
if (!refData) {
|
|
127
|
+
// Fallback to aria-ref selector if ref not in map
|
|
128
|
+
return p.locator(`aria-ref=${ref}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Use getByRole with the stored role and name for reliable resolution
|
|
132
|
+
const options: { name?: string | RegExp; exact?: boolean } = {};
|
|
133
|
+
if (refData.name) {
|
|
134
|
+
options.name = refData.name;
|
|
135
|
+
options.exact = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let locator = p.getByRole(refData.role as Parameters<Page['getByRole']>[0], options);
|
|
139
|
+
|
|
140
|
+
// Handle nth occurrence if specified
|
|
141
|
+
if (typeof refData.nth === 'number' && refData.nth > 0) {
|
|
142
|
+
locator = locator.nth(refData.nth);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return locator;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Take an AI-optimized snapshot using Playwright's _snapshotForAI method.
|
|
150
|
+
* Falls back to ariaSnapshot if _snapshotForAI is not available.
|
|
151
|
+
*/
|
|
152
|
+
async function takeSnapshot(p: Page, maxChars?: number): Promise<{ snapshot: string; truncated: boolean }> {
|
|
153
|
+
const pageWithSnapshot = p as PageWithSnapshotForAI;
|
|
154
|
+
|
|
155
|
+
let snapshot: string;
|
|
156
|
+
|
|
157
|
+
if (pageWithSnapshot._snapshotForAI) {
|
|
158
|
+
// Use the AI-optimized snapshot method
|
|
159
|
+
const result = await pageWithSnapshot._snapshotForAI({ timeout: 10000, track: 'response' });
|
|
160
|
+
snapshot = String(result?.full ?? '');
|
|
161
|
+
} else {
|
|
162
|
+
// Fallback to standard ariaSnapshot
|
|
163
|
+
snapshot = await p.locator(':root').ariaSnapshot();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Parse and store refs for later action resolution
|
|
167
|
+
currentRefs = parseRefsFromSnapshot(snapshot);
|
|
168
|
+
|
|
169
|
+
// Truncate if needed
|
|
170
|
+
let truncated = false;
|
|
171
|
+
const limit = maxChars ?? 50000;
|
|
172
|
+
if (snapshot.length > limit) {
|
|
173
|
+
snapshot = `${snapshot.slice(0, limit)}\n\n[...TRUNCATED - page too large, use read action for full text]`;
|
|
174
|
+
truncated = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { snapshot, truncated };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Schema for the act action's request object
|
|
181
|
+
const actRequestSchema = z.object({
|
|
182
|
+
kind: z.enum(['click', 'type', 'press', 'hover', 'scroll', 'wait']).describe('The type of interaction'),
|
|
183
|
+
ref: z.string().optional().describe('Element ref from snapshot (e.g., e12)'),
|
|
184
|
+
text: z.string().optional().describe('Text for type action'),
|
|
185
|
+
key: z.string().optional().describe('Key for press action (e.g., Enter, Tab)'),
|
|
186
|
+
direction: z.enum(['up', 'down']).optional().describe('Scroll direction'),
|
|
187
|
+
timeMs: z.number().optional().describe('Wait time in milliseconds'),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export const browserTool = new DynamicStructuredTool({
|
|
191
|
+
name: 'browser',
|
|
192
|
+
description: 'Navigate websites, read content, and interact with pages. Use for accessing websites, documentation, and dynamic content.',
|
|
193
|
+
schema: z.object({
|
|
194
|
+
action: z.enum(['navigate', 'open', 'snapshot', 'act', 'read', 'close']).describe('The browser action to perform'),
|
|
195
|
+
url: z.string().optional().describe('URL for navigate action'),
|
|
196
|
+
maxChars: z.number().optional().describe('Max characters for snapshot (default 50000)'),
|
|
197
|
+
request: actRequestSchema.optional().describe('Request object for act action'),
|
|
198
|
+
}),
|
|
199
|
+
func: async ({ action, url, maxChars, request }) => {
|
|
200
|
+
try {
|
|
201
|
+
switch (action) {
|
|
202
|
+
case 'navigate': {
|
|
203
|
+
if (!url) {
|
|
204
|
+
return formatToolResult({ error: 'url is required for navigate action' });
|
|
205
|
+
}
|
|
206
|
+
const p = await ensureBrowser();
|
|
207
|
+
// Use networkidle for better JS rendering on dynamic sites
|
|
208
|
+
await p.goto(url, { timeout: 30000, waitUntil: 'networkidle' });
|
|
209
|
+
return formatToolResult({
|
|
210
|
+
ok: true,
|
|
211
|
+
url: p.url(),
|
|
212
|
+
title: await p.title(),
|
|
213
|
+
hint: 'Page loaded. Call snapshot to see page structure and find elements to interact with.',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'open': {
|
|
218
|
+
if (!url) {
|
|
219
|
+
return formatToolResult({ error: 'url is required for open action' });
|
|
220
|
+
}
|
|
221
|
+
const currentPage = await ensureBrowser();
|
|
222
|
+
const context = currentPage.context();
|
|
223
|
+
const newPage = await context.newPage();
|
|
224
|
+
await newPage.goto(url, { timeout: 30000, waitUntil: 'networkidle' });
|
|
225
|
+
// Switch to the new page
|
|
226
|
+
page = newPage;
|
|
227
|
+
return formatToolResult({
|
|
228
|
+
ok: true,
|
|
229
|
+
url: newPage.url(),
|
|
230
|
+
title: await newPage.title(),
|
|
231
|
+
hint: 'New tab opened. Call snapshot to see page structure and find elements to interact with.',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case 'snapshot': {
|
|
236
|
+
const p = await ensureBrowser();
|
|
237
|
+
// Wait for any dynamic content to settle
|
|
238
|
+
await p.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
239
|
+
|
|
240
|
+
const { snapshot, truncated } = await takeSnapshot(p, maxChars);
|
|
241
|
+
|
|
242
|
+
return formatToolResult({
|
|
243
|
+
url: p.url(),
|
|
244
|
+
title: await p.title(),
|
|
245
|
+
snapshot,
|
|
246
|
+
truncated,
|
|
247
|
+
refCount: currentRefs.size,
|
|
248
|
+
refs: Object.fromEntries(currentRefs),
|
|
249
|
+
hint: 'Use act with kind="click" and ref="eN" to click elements. Or navigate directly to a /url visible in the snapshot.',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case 'act': {
|
|
254
|
+
if (!request) {
|
|
255
|
+
return formatToolResult({ error: 'request is required for act action' });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const p = await ensureBrowser();
|
|
259
|
+
const { kind, ref, text, key, direction, timeMs } = request;
|
|
260
|
+
|
|
261
|
+
switch (kind) {
|
|
262
|
+
case 'click': {
|
|
263
|
+
if (!ref) {
|
|
264
|
+
return formatToolResult({ error: 'ref is required for click' });
|
|
265
|
+
}
|
|
266
|
+
const locator = resolveRefToLocator(p, ref);
|
|
267
|
+
await locator.click({ timeout: 8000 });
|
|
268
|
+
// Wait for navigation/content to load
|
|
269
|
+
await p.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
270
|
+
return formatToolResult({
|
|
271
|
+
ok: true,
|
|
272
|
+
clicked: ref,
|
|
273
|
+
hint: 'Click successful. Call snapshot to see the updated page.',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case 'type': {
|
|
278
|
+
if (!ref) {
|
|
279
|
+
return formatToolResult({ error: 'ref is required for type' });
|
|
280
|
+
}
|
|
281
|
+
if (!text) {
|
|
282
|
+
return formatToolResult({ error: 'text is required for type' });
|
|
283
|
+
}
|
|
284
|
+
const locator = resolveRefToLocator(p, ref);
|
|
285
|
+
await locator.fill(text, { timeout: 8000 });
|
|
286
|
+
return formatToolResult({ ok: true, ref, typed: text });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case 'press': {
|
|
290
|
+
if (!key) {
|
|
291
|
+
return formatToolResult({ error: 'key is required for press' });
|
|
292
|
+
}
|
|
293
|
+
await p.keyboard.press(key);
|
|
294
|
+
await p.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
295
|
+
return formatToolResult({ ok: true, pressed: key });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
case 'hover': {
|
|
299
|
+
if (!ref) {
|
|
300
|
+
return formatToolResult({ error: 'ref is required for hover' });
|
|
301
|
+
}
|
|
302
|
+
const locator = resolveRefToLocator(p, ref);
|
|
303
|
+
await locator.hover({ timeout: 8000 });
|
|
304
|
+
return formatToolResult({ ok: true, hovered: ref });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
case 'scroll': {
|
|
308
|
+
const scrollDirection = direction ?? 'down';
|
|
309
|
+
const amount = scrollDirection === 'down' ? 500 : -500;
|
|
310
|
+
await p.mouse.wheel(0, amount);
|
|
311
|
+
await p.waitForTimeout(500);
|
|
312
|
+
return formatToolResult({ ok: true, scrolled: scrollDirection });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case 'wait': {
|
|
316
|
+
const waitTime = Math.min(timeMs ?? 2000, 10000);
|
|
317
|
+
await p.waitForTimeout(waitTime);
|
|
318
|
+
return formatToolResult({ ok: true, waited: waitTime });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
default:
|
|
322
|
+
return formatToolResult({ error: `Unknown act kind: ${kind}` });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case 'read': {
|
|
327
|
+
const p = await ensureBrowser();
|
|
328
|
+
// Wait for content to be fully loaded
|
|
329
|
+
await p.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
330
|
+
|
|
331
|
+
// Extract visible text from main content area, falling back to body
|
|
332
|
+
const content = await p.evaluate(() => {
|
|
333
|
+
const main = document.querySelector('main, article, [role="main"], .content, #content') as HTMLElement | null;
|
|
334
|
+
return (main || document.body).innerText;
|
|
335
|
+
});
|
|
336
|
+
return formatToolResult({
|
|
337
|
+
url: p.url(),
|
|
338
|
+
title: await p.title(),
|
|
339
|
+
content,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case 'close': {
|
|
344
|
+
await closeBrowser();
|
|
345
|
+
return formatToolResult({ ok: true, message: 'Browser closed' });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
default:
|
|
349
|
+
return formatToolResult({ error: `Unknown action: ${action}` });
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
353
|
+
logger.error(`[Browser (Playwright)] error: ${message}`);
|
|
354
|
+
return formatToolResult({ error: `[Browser (Playwright)] ${message}` });
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { browserTool } from './browser.js';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain-compatible tool wrappers for Hive MCP crypto endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Each tool wraps a Hive MCP call behind a DynamicStructuredTool with
|
|
5
|
+
* Zod validation, MCP caching, and progress reporting.
|
|
6
|
+
*
|
|
7
|
+
* 12 tools total:
|
|
8
|
+
* - get_api_endpoint_schema: get schema for any endpoint
|
|
9
|
+
* - invoke_api_endpoint: invoke any endpoint with args
|
|
10
|
+
* - 10 category discovery tools (one per Hive MCP category)
|
|
11
|
+
*/
|
|
12
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { callHiveTool } from '../../mcp/client.js';
|
|
15
|
+
import { readMCPCache, writeMCPCache } from '../../mcp/cache.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Helper: cached MCP call
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a result looks like an error response that should NOT be cached.
|
|
23
|
+
*/
|
|
24
|
+
function isErrorResponse(result: string): boolean {
|
|
25
|
+
const lower = result.toLowerCase().trim();
|
|
26
|
+
if (lower.startsWith('error:') || lower.startsWith('error -')) return true;
|
|
27
|
+
if (lower.startsWith('**error:**') || lower.startsWith('**error**')) return true;
|
|
28
|
+
if (lower.startsWith('{"error"') || lower.startsWith('{"message":"error')) return true;
|
|
29
|
+
if (result.length < 80 && (lower.includes('not found') || lower.includes('invalid') || lower.includes('failed'))) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function cachedHiveCall(
|
|
34
|
+
toolName: string,
|
|
35
|
+
args: Record<string, unknown>,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
// Check cache first
|
|
38
|
+
const cached = readMCPCache(toolName, args);
|
|
39
|
+
if (cached) return cached;
|
|
40
|
+
|
|
41
|
+
const result = await callHiveTool(toolName, args);
|
|
42
|
+
|
|
43
|
+
// Only cache successful responses — never cache errors
|
|
44
|
+
if (!isErrorResponse(result)) {
|
|
45
|
+
writeMCPCache(toolName, args, result);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Category discovery tools
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
interface CategoryToolDef {
|
|
56
|
+
hiveName: string;
|
|
57
|
+
langchainName: string;
|
|
58
|
+
description: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const CATEGORY_TOOLS: CategoryToolDef[] = [
|
|
62
|
+
{
|
|
63
|
+
hiveName: 'get_market_and_price_endpoints',
|
|
64
|
+
langchainName: 'get_market_and_price_endpoints',
|
|
65
|
+
description: 'List available market data and price endpoints. Use for: token prices, market caps, OHLCV charts, trading volume, price history, trending tokens.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
hiveName: 'get_onchain_dex_pool_endpoints',
|
|
69
|
+
langchainName: 'get_onchain_dex_pool_endpoints',
|
|
70
|
+
description: 'List available on-chain DEX and pool endpoints. Use for: DEX trades, liquidity pools, pool analytics, swap data, AMM metrics.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
hiveName: 'get_portfolio_wallet_endpoints',
|
|
74
|
+
langchainName: 'get_portfolio_wallet_endpoints',
|
|
75
|
+
description: 'List available portfolio and wallet endpoints. Use for: wallet balances, token holdings, transaction history, portfolio tracking, wallet PnL.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
hiveName: 'get_token_contract_endpoints',
|
|
79
|
+
langchainName: 'get_token_contract_endpoints',
|
|
80
|
+
description: 'List available token and contract endpoints. Use for: token metadata, contract info, token holders, supply data, token details.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
hiveName: 'get_defi_protocol_endpoints',
|
|
84
|
+
langchainName: 'get_defi_protocol_endpoints',
|
|
85
|
+
description: 'List available DeFi protocol endpoints. Use for: TVL data, protocol analytics, yield farming, lending rates, DeFi protocol comparisons.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
hiveName: 'get_nft_analytics_endpoints',
|
|
89
|
+
langchainName: 'get_nft_analytics_endpoints',
|
|
90
|
+
description: 'List available NFT analytics endpoints. Use for: NFT collections, floor prices, NFT sales, collection rankings, NFT market data.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
hiveName: 'get_security_risk_endpoints',
|
|
94
|
+
langchainName: 'get_security_risk_endpoints',
|
|
95
|
+
description: 'List available security and risk endpoints. Use for: token security audits, honeypot detection, rug pull checks, contract risk assessment. ALWAYS use for unknown tokens.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
hiveName: 'get_network_infrastructure_endpoints',
|
|
99
|
+
langchainName: 'get_network_infrastructure_endpoints',
|
|
100
|
+
description: 'List available network and infrastructure endpoints. Use for: gas prices, network stats, block data, chain comparisons, network health.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
hiveName: 'get_search_discovery_endpoints',
|
|
104
|
+
langchainName: 'get_search_discovery_endpoints',
|
|
105
|
+
description: 'List available search and discovery endpoints. Use for: token search by name/symbol, trending tokens, new listings, discovery tools.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
hiveName: 'get_social_sentiment_endpoints',
|
|
109
|
+
langchainName: 'get_social_sentiment_endpoints',
|
|
110
|
+
description: 'List available social and sentiment endpoints. Use for: social media metrics, sentiment analysis, community activity, influencer tracking.',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
function createCategoryTool(def: CategoryToolDef): DynamicStructuredTool {
|
|
115
|
+
return new DynamicStructuredTool({
|
|
116
|
+
name: def.langchainName,
|
|
117
|
+
description: def.description,
|
|
118
|
+
schema: z.object({}),
|
|
119
|
+
func: async () => {
|
|
120
|
+
return cachedHiveCall(def.hiveName, {});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Schema tool
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
const getApiEndpointSchemaTool = new DynamicStructuredTool({
|
|
130
|
+
name: 'get_api_endpoint_schema',
|
|
131
|
+
description:
|
|
132
|
+
'Get the parameter schema for any Hive MCP endpoint by name. Call this after discovering endpoints via a category tool to learn what arguments an endpoint accepts before invoking it.',
|
|
133
|
+
schema: z.object({
|
|
134
|
+
endpoint_name: z.string().describe('The exact endpoint name to get the schema for (e.g., "simple_price_browser", "portfolio_wallet")'),
|
|
135
|
+
}),
|
|
136
|
+
func: async (input) => {
|
|
137
|
+
return cachedHiveCall('get_api_endpoint_schema', { endpoint: input.endpoint_name });
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Invoke tool
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
const invokeApiEndpointTool = new DynamicStructuredTool({
|
|
146
|
+
name: 'invoke_api_endpoint',
|
|
147
|
+
description:
|
|
148
|
+
'Invoke any Hive MCP endpoint with arguments. This is the primary data-fetching tool. Pass the endpoint_name and its required arguments (from the schema). Returns live crypto data.',
|
|
149
|
+
schema: z.object({
|
|
150
|
+
endpoint_name: z.string().describe('The endpoint to invoke (e.g., "simple_price_browser", "coins_market_data_browser")'),
|
|
151
|
+
arguments: z.record(z.string(), z.unknown()).optional().describe('Arguments for the endpoint as a JSON object (keys and values from the schema)'),
|
|
152
|
+
}),
|
|
153
|
+
func: async (input) => {
|
|
154
|
+
return cachedHiveCall('invoke_api_endpoint', {
|
|
155
|
+
endpoint_name: input.endpoint_name,
|
|
156
|
+
args: input.arguments ?? {},
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Export all tools
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
export function createCryptoTools(): DynamicStructuredTool[] {
|
|
166
|
+
return [
|
|
167
|
+
getApiEndpointSchemaTool,
|
|
168
|
+
invokeApiEndpointTool,
|
|
169
|
+
...CATEGORY_TOOLS.map(createCategoryTool),
|
|
170
|
+
];
|
|
171
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createCryptoTools } from './hive-tools.js';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich description for the browser tool.
|
|
3
|
+
* Used in the system prompt to guide the LLM on when and how to use this tool.
|
|
4
|
+
*/
|
|
5
|
+
export const BROWSER_DESCRIPTION = `
|
|
6
|
+
Control a web browser to navigate websites and extract information.
|
|
7
|
+
|
|
8
|
+
**NOTE: For simply reading a web page's content, prefer web_fetch which returns content directly in a single call. Use browser only for interactive tasks requiring JavaScript rendering, clicking, or form filling.**
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
- Accessing dynamic/JavaScript-rendered content that requires a real browser
|
|
13
|
+
- Multi-step web navigation (click links, fill search boxes)
|
|
14
|
+
- Interacting with SPAs or pages that require JavaScript to load content
|
|
15
|
+
- When web_fetch fails or returns incomplete content due to JS-dependent rendering
|
|
16
|
+
|
|
17
|
+
## When NOT to Use
|
|
18
|
+
|
|
19
|
+
- Reading static web pages or articles (use **web_fetch** instead — it is faster and returns content in a single call)
|
|
20
|
+
- Simple queries that web_search can already answer
|
|
21
|
+
- General knowledge questions
|
|
22
|
+
|
|
23
|
+
## CRITICAL: Navigate Returns NO Content
|
|
24
|
+
|
|
25
|
+
The \`navigate\` action only loads the page - it does NOT return page content.
|
|
26
|
+
You MUST call \`snapshot\` after navigate to see what's on the page.
|
|
27
|
+
|
|
28
|
+
## CRITICAL: Use Visible URLs - Do NOT Guess
|
|
29
|
+
|
|
30
|
+
When the snapshot shows a link with a URL (e.g., \`/url: https://...\`):
|
|
31
|
+
1. **Option A**: Click the link using its ref (e.g., act with kind="click", ref="e22")
|
|
32
|
+
2. **Option B**: Navigate directly to the URL shown in the snapshot
|
|
33
|
+
|
|
34
|
+
**NEVER make up or guess URLs based on common patterns**. If you need to reach a page:
|
|
35
|
+
1. Take a snapshot
|
|
36
|
+
2. Find the link in the snapshot
|
|
37
|
+
3. Either click it OR navigate to its visible /url value
|
|
38
|
+
|
|
39
|
+
Bad: Guessing https://company.com/news-events/press-releases
|
|
40
|
+
Good: Using the /url value you SEE in the snapshot
|
|
41
|
+
|
|
42
|
+
## Available Actions
|
|
43
|
+
|
|
44
|
+
- **navigate** - Navigate to a URL in the current tab (returns only url/title, no content)
|
|
45
|
+
- **open** - Open a URL in a NEW tab (use when starting a fresh browsing session)
|
|
46
|
+
- **snapshot** - See page structure with clickable refs (e.g., e1, e2, e3)
|
|
47
|
+
- **act** - Interact with elements using refs (click, type, press, scroll)
|
|
48
|
+
- **read** - Extract full text content from the page
|
|
49
|
+
- **close** - Free browser resources when done
|
|
50
|
+
|
|
51
|
+
## Workflow (MUST FOLLOW)
|
|
52
|
+
|
|
53
|
+
1. **navigate** or **open** - Load a URL (returns only url/title, no content)
|
|
54
|
+
2. **snapshot** - See page structure with clickable refs (e.g., e1, e2, e3)
|
|
55
|
+
3. **act** - Interact with elements using refs:
|
|
56
|
+
- kind="click", ref="e5" - Click a link/button
|
|
57
|
+
- kind="type", ref="e3", text="search query" - Type in an input
|
|
58
|
+
- kind="press", key="Enter" - Press a key
|
|
59
|
+
- kind="scroll", direction="down" - Scroll the page
|
|
60
|
+
4. **snapshot** again - See updated page after interaction
|
|
61
|
+
5. **Repeat steps 3-4** until you find the content you need
|
|
62
|
+
6. **read** - Extract full text content from the page
|
|
63
|
+
7. **close** - Free browser resources when done
|
|
64
|
+
|
|
65
|
+
## Snapshot Format
|
|
66
|
+
|
|
67
|
+
The snapshot returns an AI-optimized accessibility tree with refs:
|
|
68
|
+
- navigation [ref=e1]:
|
|
69
|
+
- link "Home" [ref=e2]
|
|
70
|
+
- link "Investors" [ref=e3]
|
|
71
|
+
- link "Press Releases" [ref=e4]
|
|
72
|
+
- main:
|
|
73
|
+
- heading "Welcome to Acme Protocol" [ref=e5]
|
|
74
|
+
- paragraph: Latest news and updates
|
|
75
|
+
- link "Governance Proposal #42" [ref=e6]
|
|
76
|
+
- link "View All Updates" [ref=e7]
|
|
77
|
+
|
|
78
|
+
## Act Action Examples
|
|
79
|
+
|
|
80
|
+
To click a link with ref=e4:
|
|
81
|
+
action="act", request with kind="click" and ref="e4"
|
|
82
|
+
|
|
83
|
+
To type in a search box with ref=e10:
|
|
84
|
+
action="act", request with kind="type", ref="e10", text="aave tvl"
|
|
85
|
+
|
|
86
|
+
To press Enter:
|
|
87
|
+
action="act", request with kind="press" and key="Enter"
|
|
88
|
+
|
|
89
|
+
## Example: Reading a Protocol's Documentation
|
|
90
|
+
|
|
91
|
+
1. navigate to https://docs.aave.com
|
|
92
|
+
2. snapshot - see links like "Getting Started" [ref=e4]
|
|
93
|
+
3. act with kind="click", ref="e4" - click Getting Started link
|
|
94
|
+
4. snapshot - see documentation content
|
|
95
|
+
5. read - extract the full page text
|
|
96
|
+
|
|
97
|
+
## Usage Notes
|
|
98
|
+
|
|
99
|
+
- Always call snapshot after navigate/open - they return only url/title, no content
|
|
100
|
+
- Use **open** to start a fresh tab; use **navigate** to go to a URL within the current tab
|
|
101
|
+
- After clicking, always call snapshot again to see the new page
|
|
102
|
+
- The browser persists across calls - no need to re-navigate to the same URL
|
|
103
|
+
- Use read for bulk text extraction once you've navigated to the right page
|
|
104
|
+
- Close the browser when done to free system resources
|
|
105
|
+
`.trim();
|