@johpaz/hive 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- package/packages/tools/tsconfig.json +9 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import type { Tool } from "../agent/native-tools.ts";
|
|
2
|
+
import type { Config } from "../config/loader.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
import { retry } from "../utils/retry.ts";
|
|
5
|
+
|
|
6
|
+
export interface SearchResult {
|
|
7
|
+
title: string;
|
|
8
|
+
url: string;
|
|
9
|
+
snippet: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createWebSearchTool(config: Config): Tool {
|
|
13
|
+
const webConfig = config.tools?.web ?? {};
|
|
14
|
+
const allowlist = webConfig.allowlist ?? [];
|
|
15
|
+
const denylist = webConfig.denylist ?? ["file://", "ftp://"];
|
|
16
|
+
const timeout = (webConfig.timeoutSeconds ?? 30) * 1000;
|
|
17
|
+
|
|
18
|
+
const log = logger.child("web");
|
|
19
|
+
|
|
20
|
+
const isUrlAllowed = (url: string): boolean => {
|
|
21
|
+
for (const denied of denylist) {
|
|
22
|
+
if (url.startsWith(denied)) return false;
|
|
23
|
+
}
|
|
24
|
+
if (allowlist.length === 0) return true;
|
|
25
|
+
return allowlist.some((allowed) => url.includes(allowed));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
name: "web_search",
|
|
30
|
+
description: "Search the web for current information on any topic. Use this to find up-to-date information, news, research, or current events.",
|
|
31
|
+
parameters: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
query: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "The search query - be specific and include relevant keywords",
|
|
37
|
+
},
|
|
38
|
+
numResults: {
|
|
39
|
+
type: "number",
|
|
40
|
+
description: "Number of results to return (default: 5, max: 10)",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ["query"],
|
|
44
|
+
},
|
|
45
|
+
execute: async (params: Record<string, unknown>) => {
|
|
46
|
+
const query = params.query as string;
|
|
47
|
+
const numResults = Math.min((params.numResults as number) ?? 5, 10);
|
|
48
|
+
|
|
49
|
+
log.info(`Web search triggered: "${query}"`, { numResults });
|
|
50
|
+
|
|
51
|
+
// Try multiple search endpoints for better results
|
|
52
|
+
const searchResults = await searchWithFallbacks(query, numResults, timeout, log);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
results: searchResults,
|
|
56
|
+
query,
|
|
57
|
+
count: searchResults.length,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function searchWithFallbacks(
|
|
64
|
+
query: string,
|
|
65
|
+
numResults: number,
|
|
66
|
+
timeout: number,
|
|
67
|
+
log: any
|
|
68
|
+
): Promise<SearchResult[]> {
|
|
69
|
+
// Fallback 1: DuckDuckGo HTML search (more reliable)
|
|
70
|
+
try {
|
|
71
|
+
return await searchDuckDuckGoHTML(query, numResults, timeout, log);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
log.warn(`DuckDuckGo HTML failed: ${(error as Error).message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fallback 2: DuckDuckGo API
|
|
77
|
+
try {
|
|
78
|
+
return await searchDuckDuckGoAPI(query, numResults, timeout, log);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
log.warn(`DuckDuckGo API failed: ${(error as Error).message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback 3: Bing (via HTML scraping)
|
|
84
|
+
try {
|
|
85
|
+
return await searchBing(query, numResults, timeout, log);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.warn(`Bing failed: ${(error as Error).message}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error("All search providers failed");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function searchDuckDuckGoHTML(
|
|
94
|
+
query: string,
|
|
95
|
+
numResults: number,
|
|
96
|
+
timeout: number,
|
|
97
|
+
log: any
|
|
98
|
+
): Promise<SearchResult[]> {
|
|
99
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
100
|
+
|
|
101
|
+
log.info(`Requesting DuckDuckGo HTML: ${searchUrl}`);
|
|
102
|
+
|
|
103
|
+
const response = await fetch(searchUrl, {
|
|
104
|
+
signal: AbortSignal.timeout(timeout),
|
|
105
|
+
headers: {
|
|
106
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
107
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
108
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(`DuckDuckGo HTML failed: ${response.status}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const html = await response.text();
|
|
117
|
+
log.debug(`HTML response length: ${html.length} chars`);
|
|
118
|
+
|
|
119
|
+
const results: SearchResult[] = [];
|
|
120
|
+
|
|
121
|
+
// DuckDuckGo HTML structure: result snippets are in <div class="result"> elements
|
|
122
|
+
// More robust parsing
|
|
123
|
+
const resultBlockRegex = /<div[^>]*class="result[^"]*"[^>]*>([\s\S]*?)<\/div>/gi;
|
|
124
|
+
let resultBlock;
|
|
125
|
+
|
|
126
|
+
while ((resultBlock = resultBlockRegex.exec(html)) && results.length < numResults) {
|
|
127
|
+
const block = resultBlock[1];
|
|
128
|
+
|
|
129
|
+
// Extract title
|
|
130
|
+
const titleMatch = block.match(/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/i);
|
|
131
|
+
if (!titleMatch) continue;
|
|
132
|
+
|
|
133
|
+
const url = decodeDuckDuckGoUrl(titleMatch[1]);
|
|
134
|
+
const title = titleMatch[2].replace(/<[^>]+>/g, "").trim();
|
|
135
|
+
|
|
136
|
+
// Extract snippet
|
|
137
|
+
const snippetMatch = block.match(/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/i);
|
|
138
|
+
const snippet = snippetMatch
|
|
139
|
+
? snippetMatch[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
|
|
140
|
+
: "";
|
|
141
|
+
|
|
142
|
+
if (url && title && url.startsWith("http")) {
|
|
143
|
+
results.push({ title, url, snippet });
|
|
144
|
+
log.debug(`Found result: ${title} - ${url}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
log.debug(`Found ${results.length} results from DuckDuckGo HTML`);
|
|
149
|
+
|
|
150
|
+
// If no results found with class-based parsing, try alternative approach
|
|
151
|
+
if (results.length === 0) {
|
|
152
|
+
log.debug("Trying alternative parsing method...");
|
|
153
|
+
// Try to find any links that look like search results
|
|
154
|
+
const linkRegex = /<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>([^<]+)<\/a>/gi;
|
|
155
|
+
let linkMatch;
|
|
156
|
+
let count = 0;
|
|
157
|
+
|
|
158
|
+
while ((linkMatch = linkRegex.exec(html)) && count < numResults) {
|
|
159
|
+
const url = linkMatch[1];
|
|
160
|
+
const title = linkMatch[2].replace(/<[^>]+>/g, "").trim();
|
|
161
|
+
|
|
162
|
+
// Filter out DuckDuckGo's own links
|
|
163
|
+
if (!url.includes("duckduckgo.com") &&
|
|
164
|
+
!url.includes("about:") &&
|
|
165
|
+
!url.includes("javascript:") &&
|
|
166
|
+
title.length > 10) {
|
|
167
|
+
results.push({ title, url, snippet: "" });
|
|
168
|
+
count++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
log.debug(`Alternative parsing found ${results.length} results`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function decodeDuckDuckGoUrl(url: string): string {
|
|
179
|
+
// DuckDuckGo uses a redirect URL, extract the actual URL
|
|
180
|
+
if (url.includes("uddg=")) {
|
|
181
|
+
const match = url.match(/uddg=([^&]+)/);
|
|
182
|
+
if (match) {
|
|
183
|
+
return decodeURIComponent(match[1]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// If it's a relative URL or starts with http, return as is
|
|
187
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
188
|
+
return url;
|
|
189
|
+
}
|
|
190
|
+
return url;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function searchDuckDuckGoAPI(
|
|
194
|
+
query: string,
|
|
195
|
+
numResults: number,
|
|
196
|
+
timeout: number,
|
|
197
|
+
log: any
|
|
198
|
+
): Promise<SearchResult[]> {
|
|
199
|
+
const searchUrl = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&no_redirect=1&skip_disambig=1`;
|
|
200
|
+
|
|
201
|
+
log.info(`Requesting DuckDuckGo API: ${searchUrl}`);
|
|
202
|
+
|
|
203
|
+
const response = await fetch(searchUrl, {
|
|
204
|
+
signal: AbortSignal.timeout(timeout),
|
|
205
|
+
headers: {
|
|
206
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new Error(`DuckDuckGo API failed: ${response.status}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const data = await response.json() as {
|
|
215
|
+
AbstractText?: string;
|
|
216
|
+
AbstractURL?: string;
|
|
217
|
+
Heading?: string;
|
|
218
|
+
RelatedTopics?: Array<{
|
|
219
|
+
Text?: string;
|
|
220
|
+
FirstURL?: string;
|
|
221
|
+
Content?: string;
|
|
222
|
+
Topics?: Array<{ Text?: string; FirstURL?: string }>;
|
|
223
|
+
}>;
|
|
224
|
+
Results?: Array<{ Text?: string; FirstURL?: string }>;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
log.debug(`DuckDuckGo API response: ${JSON.stringify({
|
|
228
|
+
hasAbstract: !!data.AbstractText,
|
|
229
|
+
relatedTopics: data.RelatedTopics?.length || 0,
|
|
230
|
+
results: data.Results?.length || 0
|
|
231
|
+
})}`);
|
|
232
|
+
|
|
233
|
+
const results: SearchResult[] = [];
|
|
234
|
+
|
|
235
|
+
// Add abstract if available
|
|
236
|
+
if (data.AbstractText && data.AbstractURL) {
|
|
237
|
+
results.push({
|
|
238
|
+
title: data.Heading || "Summary",
|
|
239
|
+
url: data.AbstractURL,
|
|
240
|
+
snippet: data.AbstractText,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Add RelatedTopics (can be nested)
|
|
245
|
+
if (data.RelatedTopics) {
|
|
246
|
+
for (const topic of data.RelatedTopics) {
|
|
247
|
+
if (results.length >= numResults) break;
|
|
248
|
+
|
|
249
|
+
// Direct topic
|
|
250
|
+
if (topic.Text && topic.FirstURL) {
|
|
251
|
+
results.push({
|
|
252
|
+
title: topic.Text.split(" - ")[0] ?? "Result",
|
|
253
|
+
url: topic.FirstURL,
|
|
254
|
+
snippet: topic.Text,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Nested topics
|
|
258
|
+
else if (topic.Topics) {
|
|
259
|
+
for (const subTopic of topic.Topics) {
|
|
260
|
+
if (results.length >= numResults) break;
|
|
261
|
+
if (subTopic.Text && subTopic.FirstURL) {
|
|
262
|
+
results.push({
|
|
263
|
+
title: subTopic.Text.split(" - ")[0] ?? "Result",
|
|
264
|
+
url: subTopic.FirstURL,
|
|
265
|
+
snippet: subTopic.Text,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Add Results if available
|
|
274
|
+
if (data.Results) {
|
|
275
|
+
for (const result of data.Results) {
|
|
276
|
+
if (results.length >= numResults) break;
|
|
277
|
+
if (result.Text && result.FirstURL) {
|
|
278
|
+
results.push({
|
|
279
|
+
title: result.Text.split(" - ")[0] ?? "Result",
|
|
280
|
+
url: result.FirstURL,
|
|
281
|
+
snippet: result.Text,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
log.debug(`Found ${results.length} results from DuckDuckGo API`);
|
|
288
|
+
return results;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function searchBing(
|
|
292
|
+
query: string,
|
|
293
|
+
numResults: number,
|
|
294
|
+
timeout: number,
|
|
295
|
+
log: any
|
|
296
|
+
): Promise<SearchResult[]> {
|
|
297
|
+
const searchUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}`;
|
|
298
|
+
|
|
299
|
+
log.info(`Requesting Bing: ${searchUrl}`);
|
|
300
|
+
|
|
301
|
+
const response = await fetch(searchUrl, {
|
|
302
|
+
signal: AbortSignal.timeout(timeout),
|
|
303
|
+
headers: {
|
|
304
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
305
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
306
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
throw new Error(`Bing failed: ${response.status}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const html = await response.text();
|
|
315
|
+
log.debug(`Bing HTML response: ${html.length} chars`);
|
|
316
|
+
|
|
317
|
+
const results: SearchResult[] = [];
|
|
318
|
+
|
|
319
|
+
// Bing result structure: <li class="b_algo"> contains each result
|
|
320
|
+
const algoRegex = /<li[^>]*class="[^"]*b_algo[^"]*"[^>]*>([\s\S]*?)<\/li>/gi;
|
|
321
|
+
let algoMatch;
|
|
322
|
+
|
|
323
|
+
while ((algoMatch = algoRegex.exec(html)) && results.length < numResults) {
|
|
324
|
+
const content = algoMatch[1];
|
|
325
|
+
|
|
326
|
+
// Extract title and URL from <h2><a>
|
|
327
|
+
const linkMatch = content.match(/<h2[^>]*><a[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/i);
|
|
328
|
+
if (!linkMatch) continue;
|
|
329
|
+
|
|
330
|
+
const url = linkMatch[1];
|
|
331
|
+
const title = linkMatch[2].replace(/<[^>]+>/g, "").trim();
|
|
332
|
+
|
|
333
|
+
// Extract snippet from <p class="b_caption">
|
|
334
|
+
const snippetMatch = content.match(/<p[^>]*class="[^"]*b_caption[^"]*"[^>]*>([\s\S]*?)<\/p>/i);
|
|
335
|
+
const snippet = snippetMatch
|
|
336
|
+
? snippetMatch[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
|
|
337
|
+
: "";
|
|
338
|
+
|
|
339
|
+
if (url && title && url.startsWith("http")) {
|
|
340
|
+
results.push({ title, url, snippet });
|
|
341
|
+
log.debug(`Bing result: ${title}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
log.debug(`Found ${results.length} results from Bing`);
|
|
346
|
+
return results;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function createWebFetchTool(config: Config): Tool {
|
|
350
|
+
const webConfig = config.tools?.web ?? {};
|
|
351
|
+
const allowlist = webConfig.allowlist ?? [];
|
|
352
|
+
const denylist = webConfig.denylist ?? ["file://", "ftp://"];
|
|
353
|
+
const timeout = (webConfig.timeoutSeconds ?? 30) * 1000;
|
|
354
|
+
|
|
355
|
+
const log = logger.child("web");
|
|
356
|
+
|
|
357
|
+
const isUrlAllowed = (url: string): boolean => {
|
|
358
|
+
for (const denied of denylist) {
|
|
359
|
+
if (url.startsWith(denied)) return false;
|
|
360
|
+
}
|
|
361
|
+
if (allowlist.length === 0) return true;
|
|
362
|
+
return allowlist.some((allowed) => url.includes(allowed));
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
name: "web_fetch",
|
|
367
|
+
description: "Fetch content from a URL",
|
|
368
|
+
parameters: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {
|
|
371
|
+
url: {
|
|
372
|
+
type: "string",
|
|
373
|
+
description: "The URL to fetch",
|
|
374
|
+
},
|
|
375
|
+
selector: {
|
|
376
|
+
type: "string",
|
|
377
|
+
description: "CSS selector to extract specific content (optional)",
|
|
378
|
+
},
|
|
379
|
+
maxLength: {
|
|
380
|
+
type: "number",
|
|
381
|
+
description: "Maximum characters to return (default: 10000)",
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
required: ["url"],
|
|
385
|
+
},
|
|
386
|
+
execute: async (params: Record<string, unknown>) => {
|
|
387
|
+
const url = params.url as string;
|
|
388
|
+
const maxLength = (params.maxLength as number) ?? 10000;
|
|
389
|
+
|
|
390
|
+
if (!isUrlAllowed(url)) {
|
|
391
|
+
throw new Error(`URL not allowed: ${url}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
log.info(`Web fetch triggered: ${url}`, { maxLength });
|
|
395
|
+
|
|
396
|
+
return retry(
|
|
397
|
+
async () => {
|
|
398
|
+
const response = await fetch(url, {
|
|
399
|
+
signal: AbortSignal.timeout(timeout),
|
|
400
|
+
headers: {
|
|
401
|
+
"User-Agent": "Mozilla/5.0 (compatible; Hive/0.1)",
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error(`Fetch failed: ${response.status}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
410
|
+
let content: string;
|
|
411
|
+
|
|
412
|
+
if (contentType.includes("application/json")) {
|
|
413
|
+
const json = await response.json();
|
|
414
|
+
content = JSON.stringify(json, null, 2);
|
|
415
|
+
} else {
|
|
416
|
+
content = await response.text();
|
|
417
|
+
|
|
418
|
+
content = content
|
|
419
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
420
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
421
|
+
.replace(/<[^>]+>/g, " ")
|
|
422
|
+
.replace(/\s+/g, " ")
|
|
423
|
+
.trim();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (content.length > maxLength) {
|
|
427
|
+
content = content.slice(0, maxLength) + "\n... (truncated)";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return { content, url, contentType };
|
|
431
|
+
},
|
|
432
|
+
{ maxAttempts: 2, initialDelayMs: 1000 }
|
|
433
|
+
);
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Tool } from "../agent/native-tools.ts";
|
|
2
|
+
import type { WorkspaceLoader } from "../agent/workspace.ts";
|
|
3
|
+
|
|
4
|
+
export function createWorkspaceTools(loader: WorkspaceLoader): Tool[] {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
name: "workspace_read",
|
|
8
|
+
description:
|
|
9
|
+
"Lee el contenido actual de SOUL.md (tu identidad), " +
|
|
10
|
+
"USER.md (contexto del usuario) o ETHICS.md (lineamientos éticos). " +
|
|
11
|
+
"Usa esta herramienta cuando necesites consultar tu configuración actual " +
|
|
12
|
+
"o verificar qué información tienes sobre el usuario.",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
file: {
|
|
17
|
+
type: "string",
|
|
18
|
+
enum: ["soul", "user", "ethics"],
|
|
19
|
+
description:
|
|
20
|
+
"soul = tu identidad y personalidad, " +
|
|
21
|
+
"user = información sobre el usuario, " +
|
|
22
|
+
"ethics = lineamientos éticos (solo lectura)",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["file"],
|
|
26
|
+
},
|
|
27
|
+
execute: async (params: Record<string, unknown>) => {
|
|
28
|
+
const file = params.file as "soul" | "user" | "ethics";
|
|
29
|
+
const content = await loader.read(file);
|
|
30
|
+
return {
|
|
31
|
+
file,
|
|
32
|
+
content,
|
|
33
|
+
length: content.length,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
name: "workspace_write",
|
|
40
|
+
description:
|
|
41
|
+
"Reescribe completamente SOUL.md o USER.md. " +
|
|
42
|
+
"Usa esta herramienta solo cuando necesites hacer cambios extensos. " +
|
|
43
|
+
"Para cambios pequeños, usa workspace_patch en su lugar. " +
|
|
44
|
+
"NUNCA puedes escribir en ETHICS.md.",
|
|
45
|
+
parameters: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
file: {
|
|
49
|
+
type: "string",
|
|
50
|
+
enum: ["soul", "user"],
|
|
51
|
+
description: "El archivo a escribir. ETHICS.md no está permitido.",
|
|
52
|
+
},
|
|
53
|
+
content: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "El contenido completo del archivo en formato Markdown.",
|
|
56
|
+
},
|
|
57
|
+
reason: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description:
|
|
60
|
+
"Razón del cambio. Se registra en los logs. " +
|
|
61
|
+
"Ej: 'Usuario indicó nueva ubicación'",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ["file", "content", "reason"],
|
|
65
|
+
},
|
|
66
|
+
execute: async (params: Record<string, unknown>) => {
|
|
67
|
+
const file = params.file as "soul" | "user";
|
|
68
|
+
const content = params.content as string;
|
|
69
|
+
const reason = params.reason as string;
|
|
70
|
+
await loader.write(file, content);
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
file,
|
|
74
|
+
reason,
|
|
75
|
+
savedAt: new Date().toISOString(),
|
|
76
|
+
message: `${file}.md actualizado correctamente`,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
name: "workspace_patch",
|
|
83
|
+
description:
|
|
84
|
+
"Modifica una sección específica de SOUL.md o USER.md " +
|
|
85
|
+
"sin reescribir el archivo completo. " +
|
|
86
|
+
"Esta es la herramienta preferida para cambios pequeños y precisos. " +
|
|
87
|
+
"La sección se identifica por su encabezado Markdown (## Sección). " +
|
|
88
|
+
"Si la sección no existe, se crea al final del archivo.",
|
|
89
|
+
parameters: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
file: {
|
|
93
|
+
type: "string",
|
|
94
|
+
enum: ["soul", "user"],
|
|
95
|
+
description: "El archivo a modificar.",
|
|
96
|
+
},
|
|
97
|
+
section: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description:
|
|
100
|
+
"El nombre de la sección a modificar, sin los ## del encabezado. " +
|
|
101
|
+
"Ej: 'Ubicación', 'Preferencias', 'Trabajo'",
|
|
102
|
+
},
|
|
103
|
+
newContent: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "El nuevo contenido de esa sección en Markdown.",
|
|
106
|
+
},
|
|
107
|
+
reason: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "Razón del cambio para el registro de logs.",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["file", "section", "newContent", "reason"],
|
|
113
|
+
},
|
|
114
|
+
execute: async (params: Record<string, unknown>) => {
|
|
115
|
+
const file = params.file as "soul" | "user";
|
|
116
|
+
const section = params.section as string;
|
|
117
|
+
const newContent = params.newContent as string;
|
|
118
|
+
const reason = params.reason as string;
|
|
119
|
+
await loader.patch(file, section, newContent);
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
file,
|
|
123
|
+
section,
|
|
124
|
+
reason,
|
|
125
|
+
savedAt: new Date().toISOString(),
|
|
126
|
+
message: `Sección "${section}" de ${file}.md actualizada`,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
{
|
|
132
|
+
name: "workspace_append",
|
|
133
|
+
description:
|
|
134
|
+
"Añade nueva información a una sección de USER.md " +
|
|
135
|
+
"sin eliminar el contenido existente. " +
|
|
136
|
+
"Ideal para acumular información sobre el usuario con el tiempo.",
|
|
137
|
+
parameters: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
section: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "El nombre de la sección donde añadir la información.",
|
|
143
|
+
},
|
|
144
|
+
content: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "La información a añadir.",
|
|
147
|
+
},
|
|
148
|
+
reason: {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "Por qué se añade esta información.",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ["section", "content", "reason"],
|
|
154
|
+
},
|
|
155
|
+
execute: async (params: Record<string, unknown>) => {
|
|
156
|
+
const section = params.section as string;
|
|
157
|
+
const content = params.content as string;
|
|
158
|
+
const reason = params.reason as string;
|
|
159
|
+
await loader.append("user", section, content);
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
file: "user",
|
|
163
|
+
section,
|
|
164
|
+
reason,
|
|
165
|
+
savedAt: new Date().toISOString(),
|
|
166
|
+
message: `Información añadida a la sección "${section}" de USER.md`,
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for measuring resource usage (time and memory).
|
|
3
|
+
*/
|
|
4
|
+
export class Benchmark {
|
|
5
|
+
private startTime: number = 0;
|
|
6
|
+
private startMem: NodeJS.MemoryUsage | null = null;
|
|
7
|
+
private name: string;
|
|
8
|
+
|
|
9
|
+
constructor(name: string = "Benchmark") {
|
|
10
|
+
this.name = name;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Starts the benchmark timer and records initial memory usage.
|
|
15
|
+
*/
|
|
16
|
+
start() {
|
|
17
|
+
this.startTime = performance.now();
|
|
18
|
+
this.startMem = process.memoryUsage();
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stops the benchmark and returns the metrics.
|
|
24
|
+
*/
|
|
25
|
+
stop() {
|
|
26
|
+
const endTime = performance.now();
|
|
27
|
+
const endMem = process.memoryUsage();
|
|
28
|
+
const duration = endTime - this.startTime;
|
|
29
|
+
|
|
30
|
+
const metrics = {
|
|
31
|
+
name: this.name,
|
|
32
|
+
durationMs: duration.toFixed(2),
|
|
33
|
+
heapUsed: this.formatMB(endMem.heapUsed),
|
|
34
|
+
heapTotal: this.formatMB(endMem.heapTotal),
|
|
35
|
+
rss: this.formatMB(endMem.rss),
|
|
36
|
+
external: this.formatMB(endMem.external),
|
|
37
|
+
heapDelta: this.startMem
|
|
38
|
+
? this.formatMB(endMem.heapUsed - this.startMem.heapUsed)
|
|
39
|
+
: "0.00 MB",
|
|
40
|
+
raw: {
|
|
41
|
+
duration,
|
|
42
|
+
memory: endMem,
|
|
43
|
+
delta: this.startMem
|
|
44
|
+
? endMem.heapUsed - this.startMem.heapUsed
|
|
45
|
+
: 0
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return metrics;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pretty-prints the benchmark results to the console.
|
|
54
|
+
*/
|
|
55
|
+
print() {
|
|
56
|
+
const m = this.stop();
|
|
57
|
+
console.log(`\n📊 Benchmark: ${m.name}`);
|
|
58
|
+
console.table({
|
|
59
|
+
'Time': `${m.durationMs} ms`,
|
|
60
|
+
'Heap Used': m.heapUsed,
|
|
61
|
+
'Heap Total': m.heapTotal,
|
|
62
|
+
'RSS': m.rss,
|
|
63
|
+
'External': m.external,
|
|
64
|
+
'Memory Delta': m.heapDelta
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private formatMB(bytes: number) {
|
|
69
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* static helper for quick one-off benchmarks.
|
|
74
|
+
*/
|
|
75
|
+
static async run(name: string, fn: () => Promise<any> | any) {
|
|
76
|
+
const b = new Benchmark(name).start();
|
|
77
|
+
await fn();
|
|
78
|
+
b.print();
|
|
79
|
+
}
|
|
80
|
+
}
|