@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.
Files changed (156) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/README.md +310 -0
  3. package/package.json +96 -0
  4. package/packages/cli/package.json +28 -0
  5. package/packages/cli/src/commands/agent-run.ts +168 -0
  6. package/packages/cli/src/commands/agents.ts +398 -0
  7. package/packages/cli/src/commands/chat.ts +142 -0
  8. package/packages/cli/src/commands/config.ts +50 -0
  9. package/packages/cli/src/commands/cron.ts +161 -0
  10. package/packages/cli/src/commands/dev.ts +95 -0
  11. package/packages/cli/src/commands/doctor.ts +133 -0
  12. package/packages/cli/src/commands/gateway.ts +443 -0
  13. package/packages/cli/src/commands/logs.ts +57 -0
  14. package/packages/cli/src/commands/mcp.ts +175 -0
  15. package/packages/cli/src/commands/message.ts +77 -0
  16. package/packages/cli/src/commands/onboard.ts +1868 -0
  17. package/packages/cli/src/commands/security.ts +144 -0
  18. package/packages/cli/src/commands/service.ts +50 -0
  19. package/packages/cli/src/commands/sessions.ts +116 -0
  20. package/packages/cli/src/commands/skills.ts +187 -0
  21. package/packages/cli/src/commands/update.ts +25 -0
  22. package/packages/cli/src/index.ts +185 -0
  23. package/packages/cli/src/utils/token.ts +6 -0
  24. package/packages/code-bridge/README.md +78 -0
  25. package/packages/code-bridge/package.json +18 -0
  26. package/packages/code-bridge/src/index.ts +95 -0
  27. package/packages/code-bridge/src/process-manager.ts +212 -0
  28. package/packages/code-bridge/src/schemas.ts +133 -0
  29. package/packages/core/package.json +46 -0
  30. package/packages/core/src/agent/agent-loop.ts +369 -0
  31. package/packages/core/src/agent/compaction.ts +140 -0
  32. package/packages/core/src/agent/context-compiler.ts +378 -0
  33. package/packages/core/src/agent/context-guard.ts +91 -0
  34. package/packages/core/src/agent/context.ts +138 -0
  35. package/packages/core/src/agent/conversation-store.ts +198 -0
  36. package/packages/core/src/agent/curator.ts +158 -0
  37. package/packages/core/src/agent/hooks.ts +166 -0
  38. package/packages/core/src/agent/index.ts +116 -0
  39. package/packages/core/src/agent/llm-client.ts +503 -0
  40. package/packages/core/src/agent/native-tools.ts +505 -0
  41. package/packages/core/src/agent/prompt-builder.ts +532 -0
  42. package/packages/core/src/agent/providers/index.ts +167 -0
  43. package/packages/core/src/agent/providers.ts +1 -0
  44. package/packages/core/src/agent/reflector.ts +170 -0
  45. package/packages/core/src/agent/service.ts +64 -0
  46. package/packages/core/src/agent/stuck-loop.ts +133 -0
  47. package/packages/core/src/agent/supervisor.ts +39 -0
  48. package/packages/core/src/agent/tracer.ts +102 -0
  49. package/packages/core/src/agent/workspace.ts +110 -0
  50. package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
  51. package/packages/core/src/canvas/canvas-manager.ts +319 -0
  52. package/packages/core/src/canvas/canvas-tools.ts +420 -0
  53. package/packages/core/src/canvas/emitter.ts +115 -0
  54. package/packages/core/src/canvas/index.ts +2 -0
  55. package/packages/core/src/channels/base.ts +138 -0
  56. package/packages/core/src/channels/discord.ts +260 -0
  57. package/packages/core/src/channels/index.ts +7 -0
  58. package/packages/core/src/channels/manager.ts +383 -0
  59. package/packages/core/src/channels/slack.ts +287 -0
  60. package/packages/core/src/channels/telegram.ts +502 -0
  61. package/packages/core/src/channels/webchat.ts +128 -0
  62. package/packages/core/src/channels/whatsapp.ts +375 -0
  63. package/packages/core/src/config/index.ts +12 -0
  64. package/packages/core/src/config/loader.ts +529 -0
  65. package/packages/core/src/events/event-bus.ts +169 -0
  66. package/packages/core/src/gateway/index.ts +5 -0
  67. package/packages/core/src/gateway/initializer.ts +290 -0
  68. package/packages/core/src/gateway/lane-queue.ts +169 -0
  69. package/packages/core/src/gateway/resolver.ts +108 -0
  70. package/packages/core/src/gateway/router.ts +124 -0
  71. package/packages/core/src/gateway/server.ts +3317 -0
  72. package/packages/core/src/gateway/session.ts +95 -0
  73. package/packages/core/src/gateway/slash-commands.ts +192 -0
  74. package/packages/core/src/heartbeat/index.ts +157 -0
  75. package/packages/core/src/index.ts +19 -0
  76. package/packages/core/src/integrations/catalog.ts +286 -0
  77. package/packages/core/src/integrations/env.ts +64 -0
  78. package/packages/core/src/integrations/index.ts +2 -0
  79. package/packages/core/src/memory/index.ts +1 -0
  80. package/packages/core/src/memory/notes.ts +68 -0
  81. package/packages/core/src/plugins/api.ts +128 -0
  82. package/packages/core/src/plugins/index.ts +2 -0
  83. package/packages/core/src/plugins/loader.ts +365 -0
  84. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  85. package/packages/core/src/security/google-chat.ts +269 -0
  86. package/packages/core/src/security/index.ts +192 -0
  87. package/packages/core/src/security/pairing.ts +250 -0
  88. package/packages/core/src/security/rate-limit.ts +270 -0
  89. package/packages/core/src/security/signal.ts +321 -0
  90. package/packages/core/src/state/store.ts +312 -0
  91. package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
  92. package/packages/core/src/storage/crypto.ts +101 -0
  93. package/packages/core/src/storage/db-context.ts +333 -0
  94. package/packages/core/src/storage/onboarding.ts +1087 -0
  95. package/packages/core/src/storage/schema.ts +541 -0
  96. package/packages/core/src/storage/seed.ts +571 -0
  97. package/packages/core/src/storage/sqlite.ts +387 -0
  98. package/packages/core/src/storage/usage.ts +212 -0
  99. package/packages/core/src/tools/bridge-events.ts +74 -0
  100. package/packages/core/src/tools/browser.ts +275 -0
  101. package/packages/core/src/tools/codebridge.ts +421 -0
  102. package/packages/core/src/tools/coordinator-tools.ts +179 -0
  103. package/packages/core/src/tools/cron.ts +611 -0
  104. package/packages/core/src/tools/exec.ts +140 -0
  105. package/packages/core/src/tools/fs.ts +364 -0
  106. package/packages/core/src/tools/index.ts +12 -0
  107. package/packages/core/src/tools/memory.ts +176 -0
  108. package/packages/core/src/tools/notify.ts +113 -0
  109. package/packages/core/src/tools/project-management.ts +376 -0
  110. package/packages/core/src/tools/project.ts +375 -0
  111. package/packages/core/src/tools/read.ts +158 -0
  112. package/packages/core/src/tools/web.ts +436 -0
  113. package/packages/core/src/tools/workspace.ts +171 -0
  114. package/packages/core/src/utils/benchmark.ts +80 -0
  115. package/packages/core/src/utils/crypto.ts +73 -0
  116. package/packages/core/src/utils/date.ts +42 -0
  117. package/packages/core/src/utils/index.ts +4 -0
  118. package/packages/core/src/utils/logger.ts +388 -0
  119. package/packages/core/src/utils/retry.ts +70 -0
  120. package/packages/core/src/voice/index.ts +583 -0
  121. package/packages/core/tsconfig.json +9 -0
  122. package/packages/mcp/package.json +26 -0
  123. package/packages/mcp/src/config.ts +13 -0
  124. package/packages/mcp/src/index.ts +1 -0
  125. package/packages/mcp/src/logger.ts +42 -0
  126. package/packages/mcp/src/manager.ts +434 -0
  127. package/packages/mcp/src/transports/index.ts +67 -0
  128. package/packages/mcp/src/transports/sse.ts +241 -0
  129. package/packages/mcp/src/transports/websocket.ts +159 -0
  130. package/packages/skills/package.json +21 -0
  131. package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
  132. package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
  133. package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
  134. package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
  135. package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
  136. package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
  137. package/packages/skills/src/bundled/memory/SKILL.md +42 -0
  138. package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
  139. package/packages/skills/src/bundled/shell/SKILL.md +43 -0
  140. package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
  141. package/packages/skills/src/bundled/voice/SKILL.md +25 -0
  142. package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
  143. package/packages/skills/src/index.ts +1 -0
  144. package/packages/skills/src/loader.ts +282 -0
  145. package/packages/tools/package.json +43 -0
  146. package/packages/tools/src/browser/browser.test.ts +111 -0
  147. package/packages/tools/src/browser/index.ts +272 -0
  148. package/packages/tools/src/canvas/index.ts +220 -0
  149. package/packages/tools/src/cron/cron.test.ts +164 -0
  150. package/packages/tools/src/cron/index.ts +304 -0
  151. package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
  152. package/packages/tools/src/filesystem/index.ts +379 -0
  153. package/packages/tools/src/git/index.ts +239 -0
  154. package/packages/tools/src/index.ts +4 -0
  155. package/packages/tools/src/shell/detect-env.ts +70 -0
  156. 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
+ }