@dreb/coding-agent 2.11.0 → 2.12.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/CHANGELOG.md CHANGED
@@ -27,6 +27,8 @@
27
27
 
28
28
  - Added skill system enhancements: `argument-hint` frontmatter field shown in `/` menu autocomplete, `user-invocable` field to hide skills from the `/` menu while keeping them available to the model, `disable-model-invocation` field to restrict skills to user-only invocation, and a dedicated `skill` tool for model-invocable skill execution with full content substitution (`$ARGUMENTS`, `$0`..`$N`, `$@`, `${@:N}`, `${DREB_SKILL_DIR}`, `${DREB_SESSION_ID}`) ([#7](https://github.com/aebrer/dreb/issues/7))
29
29
  - Added `sessionDir` setting support in global and project `settings.json` so session storage can be configured without passing `--session-dir` on every invocation ([#2598](https://github.com/badlogic/pi-mono/pull/2598) by [@smcllns](https://github.com/smcllns))
30
+ - Added process-safe rate-limited queue for `web_search` tool — throttles rapid successive searches (including from parallel subagents) using `proper-lockfile` + timestamp file for cross-process coordination. Default minimum spacing is 10 seconds. Configurable via `DREB_WEB_SEARCH_RATE_LIMIT_MS` env var or `search.rate_limit_ms` in config file. Explore agents now have access to `web_search` and `web_fetch`. ([#180](https://github.com/aebrer/dreb/issues/180))
31
+
30
32
  - Added `/dream` memory consolidation command — backs up all memory directories, merges duplicates, scans session history for unrecorded patterns, prunes stale entries, and validates links. Uses tar.gz archives with retention policy (keep last 10), lockfile-based concurrency protection, and a 10-step LLM pipeline with explicit backup verification. Configurable archive path via `dream.archivePath` setting. ([#99](https://github.com/aebrer/dreb/issues/99))
31
33
 
32
34
  ### Fixed
package/README.md CHANGED
@@ -665,6 +665,10 @@ dreb --thinking high "Solve this complex problem"
665
665
  | `DREB_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) |
666
666
  | `DREB_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
667
667
  | `DREB_OFFLINE` | Disable startup network ops (same as `--offline`) |
668
+ | `DREB_SEARCH_BACKEND` | Search backend: `ddg` (default), `searxng`, or `brave` |
669
+ | `DREB_SEARXNG_URL` | Base URL for SearXNG backend (default: `http://localhost:8888`) |
670
+ | `DREB_BRAVE_API_KEY` | API key for Brave search backend |
671
+ | `DREB_WEB_SEARCH_RATE_LIMIT_MS` | Minimum delay between web searches in milliseconds (default: `10000`) |
668
672
  | `VISUAL`, `EDITOR` | External editor for Ctrl+G |
669
673
 
670
674
  ---
package/agents/explore.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Explore
3
- description: Codebase exploration — find files, search code, answer questions. Read-only.
4
- tools: read, grep, find, ls, bash, search
3
+ description: Codebase and web exploration — find files, search code, search the web, answer questions. Read-only.
4
+ tools: read, grep, find, ls, bash, search, web_search, web_fetch
5
5
  model: zai/glm-5-turbo, anthropic/sonnet
6
6
  ---
7
7
 
@@ -0,0 +1,13 @@
1
+ export interface WebSearchQueueOptions {
2
+ rateLimitMs?: number;
3
+ lockFilePath?: string;
4
+ timeFilePath?: string;
5
+ }
6
+ export declare class WebSearchQueue {
7
+ private readonly rateLimitMs;
8
+ private readonly lockFilePath;
9
+ private readonly timeFilePath;
10
+ constructor(options?: WebSearchQueueOptions);
11
+ enqueue<T>(fn: () => Promise<T>): Promise<T>;
12
+ }
13
+ //# sourceMappingURL=web-search-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-search-queue.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web-search-queue.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,qBAAqB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,YAAY,OAAO,GAAE,qBAA0B,EAI9C;IAEK,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAiEjD;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../../config.js\";\n\nexport interface WebSearchQueueOptions {\n\trateLimitMs?: number;\n\tlockFilePath?: string;\n\ttimeFilePath?: string;\n}\n\ninterface TimestampData {\n\tlastSearchTime: number;\n}\n\nexport class WebSearchQueue {\n\tprivate readonly rateLimitMs: number;\n\tprivate readonly lockFilePath: string;\n\tprivate readonly timeFilePath: string;\n\n\tconstructor(options: WebSearchQueueOptions = {}) {\n\t\tthis.rateLimitMs = options.rateLimitMs ?? 10_000;\n\t\tthis.lockFilePath = options.lockFilePath ?? join(getAgentDir(), \"web-search-queue.lock\");\n\t\tthis.timeFilePath = options.timeFilePath ?? join(getAgentDir(), \"web-search-queue.time\");\n\t}\n\n\tasync enqueue<T>(fn: () => Promise<T>): Promise<T> {\n\t\t// Ensure parent directory and lock file exist (proper-lockfile requires the file to exist)\n\t\ttry {\n\t\t\tconst dir = dirname(this.lockFilePath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true });\n\t\t\t}\n\t\t\tif (!existsSync(this.lockFilePath)) {\n\t\t\t\twriteFileSync(this.lockFilePath, \"\");\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tthrow new Error(`Failed to initialize web search queue lock file at ${this.lockFilePath}: ${msg}`);\n\t\t}\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\ttry {\n\t\t\trelease = await lockfile.lock(this.lockFilePath, {\n\t\t\t\tstale: 60_000,\n\t\t\t\tretries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true },\n\t\t\t});\n\n\t\t\t// Read last search timestamp\n\t\t\tlet lastSearchTime = 0;\n\t\t\ttry {\n\t\t\t\tconst raw = readFileSync(this.timeFilePath, \"utf-8\");\n\t\t\t\tconst data = JSON.parse(raw) as TimestampData;\n\t\t\t\tif (typeof data.lastSearchTime === \"number\") {\n\t\t\t\t\tlastSearchTime = data.lastSearchTime;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Missing or malformed — treat as 0\n\t\t\t}\n\n\t\t\t// Enforce rate limit\n\t\t\tconst delayNeeded = Math.max(0, this.rateLimitMs - (Date.now() - lastSearchTime));\n\t\t\tif (delayNeeded > 0) {\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, delayNeeded));\n\t\t\t}\n\n\t\t\t// Execute the search operation\n\t\t\ttry {\n\t\t\t\treturn await fn();\n\t\t\t} finally {\n\t\t\t\t// Ensure time file directory exists before writing\n\t\t\t\tconst timeDir = dirname(this.timeFilePath);\n\t\t\t\tif (!existsSync(timeDir)) {\n\t\t\t\t\tmkdirSync(timeDir, { recursive: true });\n\t\t\t\t}\n\t\t\t\t// Update timestamp even on error to prevent retry storms\n\t\t\t\ttry {\n\t\t\t\t\tconst timestampData: TimestampData = { lastSearchTime: Date.now() };\n\t\t\t\t\twriteFileSync(this.timeFilePath, JSON.stringify(timestampData));\n\t\t\t\t} catch (tsErr) {\n\t\t\t\t\t// Don't let timestamp write failure mask the original error\n\t\t\t\t\tconsole.error(`Failed to write search timestamp: ${tsErr}`);\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tawait release?.();\n\t\t\t} catch {\n\t\t\t\t// Swallow unlock errors\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,83 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import lockfile from "proper-lockfile";
4
+ import { getAgentDir } from "../../config.js";
5
+ export class WebSearchQueue {
6
+ rateLimitMs;
7
+ lockFilePath;
8
+ timeFilePath;
9
+ constructor(options = {}) {
10
+ this.rateLimitMs = options.rateLimitMs ?? 10_000;
11
+ this.lockFilePath = options.lockFilePath ?? join(getAgentDir(), "web-search-queue.lock");
12
+ this.timeFilePath = options.timeFilePath ?? join(getAgentDir(), "web-search-queue.time");
13
+ }
14
+ async enqueue(fn) {
15
+ // Ensure parent directory and lock file exist (proper-lockfile requires the file to exist)
16
+ try {
17
+ const dir = dirname(this.lockFilePath);
18
+ if (!existsSync(dir)) {
19
+ mkdirSync(dir, { recursive: true });
20
+ }
21
+ if (!existsSync(this.lockFilePath)) {
22
+ writeFileSync(this.lockFilePath, "");
23
+ }
24
+ }
25
+ catch (err) {
26
+ const msg = err instanceof Error ? err.message : String(err);
27
+ throw new Error(`Failed to initialize web search queue lock file at ${this.lockFilePath}: ${msg}`);
28
+ }
29
+ let release;
30
+ try {
31
+ release = await lockfile.lock(this.lockFilePath, {
32
+ stale: 60_000,
33
+ retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true },
34
+ });
35
+ // Read last search timestamp
36
+ let lastSearchTime = 0;
37
+ try {
38
+ const raw = readFileSync(this.timeFilePath, "utf-8");
39
+ const data = JSON.parse(raw);
40
+ if (typeof data.lastSearchTime === "number") {
41
+ lastSearchTime = data.lastSearchTime;
42
+ }
43
+ }
44
+ catch {
45
+ // Missing or malformed — treat as 0
46
+ }
47
+ // Enforce rate limit
48
+ const delayNeeded = Math.max(0, this.rateLimitMs - (Date.now() - lastSearchTime));
49
+ if (delayNeeded > 0) {
50
+ await new Promise((resolve) => setTimeout(resolve, delayNeeded));
51
+ }
52
+ // Execute the search operation
53
+ try {
54
+ return await fn();
55
+ }
56
+ finally {
57
+ // Ensure time file directory exists before writing
58
+ const timeDir = dirname(this.timeFilePath);
59
+ if (!existsSync(timeDir)) {
60
+ mkdirSync(timeDir, { recursive: true });
61
+ }
62
+ // Update timestamp even on error to prevent retry storms
63
+ try {
64
+ const timestampData = { lastSearchTime: Date.now() };
65
+ writeFileSync(this.timeFilePath, JSON.stringify(timestampData));
66
+ }
67
+ catch (tsErr) {
68
+ // Don't let timestamp write failure mask the original error
69
+ console.error(`Failed to write search timestamp: ${tsErr}`);
70
+ }
71
+ }
72
+ }
73
+ finally {
74
+ try {
75
+ await release?.();
76
+ }
77
+ catch {
78
+ // Swallow unlock errors
79
+ }
80
+ }
81
+ }
82
+ }
83
+ //# sourceMappingURL=web-search-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-search-queue.js","sourceRoot":"","sources":["../../../src/core/tools/web-search-queue.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../../config.js\";\n\nexport interface WebSearchQueueOptions {\n\trateLimitMs?: number;\n\tlockFilePath?: string;\n\ttimeFilePath?: string;\n}\n\ninterface TimestampData {\n\tlastSearchTime: number;\n}\n\nexport class WebSearchQueue {\n\tprivate readonly rateLimitMs: number;\n\tprivate readonly lockFilePath: string;\n\tprivate readonly timeFilePath: string;\n\n\tconstructor(options: WebSearchQueueOptions = {}) {\n\t\tthis.rateLimitMs = options.rateLimitMs ?? 10_000;\n\t\tthis.lockFilePath = options.lockFilePath ?? join(getAgentDir(), \"web-search-queue.lock\");\n\t\tthis.timeFilePath = options.timeFilePath ?? join(getAgentDir(), \"web-search-queue.time\");\n\t}\n\n\tasync enqueue<T>(fn: () => Promise<T>): Promise<T> {\n\t\t// Ensure parent directory and lock file exist (proper-lockfile requires the file to exist)\n\t\ttry {\n\t\t\tconst dir = dirname(this.lockFilePath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true });\n\t\t\t}\n\t\t\tif (!existsSync(this.lockFilePath)) {\n\t\t\t\twriteFileSync(this.lockFilePath, \"\");\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tthrow new Error(`Failed to initialize web search queue lock file at ${this.lockFilePath}: ${msg}`);\n\t\t}\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\ttry {\n\t\t\trelease = await lockfile.lock(this.lockFilePath, {\n\t\t\t\tstale: 60_000,\n\t\t\t\tretries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true },\n\t\t\t});\n\n\t\t\t// Read last search timestamp\n\t\t\tlet lastSearchTime = 0;\n\t\t\ttry {\n\t\t\t\tconst raw = readFileSync(this.timeFilePath, \"utf-8\");\n\t\t\t\tconst data = JSON.parse(raw) as TimestampData;\n\t\t\t\tif (typeof data.lastSearchTime === \"number\") {\n\t\t\t\t\tlastSearchTime = data.lastSearchTime;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Missing or malformed — treat as 0\n\t\t\t}\n\n\t\t\t// Enforce rate limit\n\t\t\tconst delayNeeded = Math.max(0, this.rateLimitMs - (Date.now() - lastSearchTime));\n\t\t\tif (delayNeeded > 0) {\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, delayNeeded));\n\t\t\t}\n\n\t\t\t// Execute the search operation\n\t\t\ttry {\n\t\t\t\treturn await fn();\n\t\t\t} finally {\n\t\t\t\t// Ensure time file directory exists before writing\n\t\t\t\tconst timeDir = dirname(this.timeFilePath);\n\t\t\t\tif (!existsSync(timeDir)) {\n\t\t\t\t\tmkdirSync(timeDir, { recursive: true });\n\t\t\t\t}\n\t\t\t\t// Update timestamp even on error to prevent retry storms\n\t\t\t\ttry {\n\t\t\t\t\tconst timestampData: TimestampData = { lastSearchTime: Date.now() };\n\t\t\t\t\twriteFileSync(this.timeFilePath, JSON.stringify(timestampData));\n\t\t\t\t} catch (tsErr) {\n\t\t\t\t\t// Don't let timestamp write failure mask the original error\n\t\t\t\t\tconsole.error(`Failed to write search timestamp: ${tsErr}`);\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tawait release?.();\n\t\t\t} catch {\n\t\t\t\t// Swallow unlock errors\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -9,11 +9,19 @@ export type WebSearchToolInput = Static<typeof webSearchSchema>;
9
9
  export interface WebSearchToolDetails {
10
10
  truncation?: TruncationResult;
11
11
  }
12
+ interface SearchResult {
13
+ title: string;
14
+ url: string;
15
+ snippet: string;
16
+ }
12
17
  export interface WebSearchConfig {
13
18
  backend?: "ddg" | "searxng" | "brave";
14
19
  searxngUrl?: string;
15
20
  braveApiKey?: string;
21
+ rateLimitMs?: number;
16
22
  }
23
+ export declare function getSearchConfig(): WebSearchConfig;
24
+ export declare function executeSearch(query: string): Promise<SearchResult[]>;
17
25
  export declare function createWebSearchToolDefinition(_cwd: string): ToolDefinition<typeof webSearchSchema, WebSearchToolDetails | undefined>;
18
26
  export declare function createWebSearchTool(cwd: string): AgentTool<typeof webSearchSchema>;
19
27
  export declare const webSearchToolDefinition: ToolDefinition<import("@sinclair/typebox").TObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AA+KnG,QAAA,MAAM,eAAe;;EAEnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AA6FD,MAAM,WAAW,eAAe;IAC/B,OAAO,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAwGD,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA2C1E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,eAAe,CAAC,CAElF;AAED,eAAO,MAAM,uBAAuB;;0CAA+C,CAAC;AACpF,eAAO,MAAM,aAAa;;QAAqC,CAAC;AAMhE,QAAA,MAAM,cAAc;;EAElB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B;AA0CD,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CAmHxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,cAAc,CAAC,CAEhF;AAED,eAAO,MAAM,sBAAsB;;yCAA8C,CAAC;AAClF,eAAO,MAAM,YAAY;;QAAoC,CAAC","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { CONFIG_DIR_NAME } from \"../../config.js\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n// ---------------------------------------------------------------------------\n// Shared: HTTP fetching and HTML extraction\n// ---------------------------------------------------------------------------\n\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_CONTENT_LENGTH = 100_000;\nconst CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes\n\nconst fetchCache = new Map<string, { content: WebFetchResult; timestamp: number }>();\n\ninterface WebFetchResult {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\tfetchedAt: string;\n}\n\nfunction stripHtmlToText(html: string): string {\n\tlet text = html;\n\t// Remove script/style/nav/footer blocks entirely\n\ttext = text.replace(/<(script|style|nav|footer|header|aside|iframe|noscript)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, \"\");\n\t// Convert block elements to newlines\n\ttext = text.replace(/<\\/(p|div|li|tr|h[1-6]|blockquote|pre|section|article)>/gi, \"\\n\");\n\ttext = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n\t// Convert links to text with URL\n\ttext = text.replace(/<a\\b[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$2 ($1)\");\n\t// Convert headings to markdown-style\n\ttext = text.replace(/<h([1-6])\\b[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_match, level, content) => {\n\t\treturn `\\n${\"#\".repeat(Number(level))} ${content.trim()}\\n`;\n\t});\n\t// Convert list items\n\ttext = text.replace(/<li\\b[^>]*>/gi, \"\\n- \");\n\t// Strip all remaining tags\n\ttext = text.replace(/<[^>]+>/g, \"\");\n\t// Decode common HTML entities\n\ttext = text.replace(/&amp;/g, \"&\");\n\ttext = text.replace(/&lt;/g, \"<\");\n\ttext = text.replace(/&gt;/g, \">\");\n\ttext = text.replace(/&quot;/g, '\"');\n\ttext = text.replace(/&#39;/g, \"'\");\n\ttext = text.replace(/&nbsp;/g, \" \");\n\t// Collapse whitespace\n\ttext = text.replace(/[ \\t]+/g, \" \");\n\ttext = text.replace(/\\n{3,}/g, \"\\n\\n\");\n\treturn text.trim();\n}\n\nfunction extractTitle(html: string): string {\n\tconst match = html.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i);\n\treturn match ? match[1].trim().replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : \"\";\n}\n\nconst FETCH_HEADERS = {\n\t\"User-Agent\": \"dreb/1.0 (web fetch tool)\",\n\tAccept: \"text/html,application/xhtml+xml,text/plain,application/pdf\",\n};\n\n// Block fetches to private/internal networks to prevent SSRF\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"0.0.0.0\"]);\n\nfunction isPrivateHost(hostname: string): boolean {\n\tif (BLOCKED_HOSTNAMES.has(hostname)) return true;\n\t// IPv4 private ranges\n\tconst ipv4Match = hostname.match(/^(\\d+)\\.(\\d+)\\.\\d+\\.\\d+$/);\n\tif (ipv4Match) {\n\t\tconst [, first, second] = ipv4Match.map(Number);\n\t\tif (first === 10) return true; // 10.0.0.0/8\n\t\tif (first === 172 && second >= 16 && second <= 31) return true; // 172.16.0.0/12\n\t\tif (first === 192 && second === 168) return true; // 192.168.0.0/16\n\t\tif (first === 169 && second === 254) return true; // link-local 169.254.0.0/16\n\t}\n\t// IPv6 loopback, link-local, and ULA (fc00::/7)\n\tif (hostname.startsWith(\"[\")) {\n\t\tconst lh = hostname.toLowerCase();\n\t\tif (lh.includes(\"::1\") || lh.startsWith(\"[fe80:\") || lh.startsWith(\"[fc\") || lh.startsWith(\"[fd\")) return true;\n\t}\n\treturn false;\n}\n\nfunction buildResponse(response: Response): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst ct = response.headers.get(\"content-type\") || \"\";\n\tif (ct.includes(\"application/pdf\")) {\n\t\treturn response.arrayBuffer().then((buf) => ({ body: Buffer.from(buf), contentType: ct }));\n\t}\n\treturn response.text().then((text) => ({ body: text, contentType: ct }));\n}\n\nasync function httpFetch(url: string): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst originalHost = new URL(url).hostname;\n\tif (isPrivateHost(originalHost)) {\n\t\tthrow new Error(`Blocked: ${originalHost} is a private/internal address`);\n\t}\n\n\t// Manual redirect loop to enforce same-host on every hop\n\tlet currentUrl = url;\n\tconst maxRedirects = 10;\n\tfor (let i = 0; i <= maxRedirects; i++) {\n\t\tconst response = await fetch(currentUrl, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: FETCH_HEADERS,\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t\t});\n\n\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) {\n\t\t\t\tthrow new Error(`HTTP ${response.status}: redirect with no Location header`);\n\t\t\t}\n\t\t\tconst redirectUrl = new URL(location, currentUrl);\n\t\t\t// Block private IPs before revealing them in cross-host messages\n\t\t\tif (isPrivateHost(redirectUrl.hostname)) {\n\t\t\t\tthrow new Error(`Blocked: redirect to private/internal address`);\n\t\t\t}\n\t\t\tif (redirectUrl.hostname !== originalHost) {\n\t\t\t\treturn {\n\t\t\t\t\tbody: `Cross-host redirect detected.\\nOriginal: ${url}\\nRedirects to: ${redirectUrl.href}\\n\\nThe redirect target is on a different host. Fetch the new URL directly if you want to follow it.`,\n\t\t\t\t\tcontentType: \"text/plain\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tcurrentUrl = redirectUrl.href;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst errorBody = await response.text();\n\t\t\tthrow new Error(`HTTP ${response.status}: ${errorBody.slice(0, 200)}`);\n\t\t}\n\n\t\treturn buildResponse(response);\n\t}\n\tthrow new Error(`Too many redirects (${maxRedirects})`);\n}\n\n// -- PDF text extraction (basic) ---------------------------------------------\n\nfunction extractPdfText(buffer: Buffer): string {\n\t// Minimal PDF text extraction — only works on uncompressed PDFs with literal\n\t// string objects in BT/ET text blocks. Most production PDFs use FlateDecode\n\t// compression and will fall through to the failure message.\n\t// latin1 preserves raw byte values 0-255 as code points for safe regex matching.\n\tconst raw = buffer.toString(\"latin1\");\n\tconst textChunks: string[] = [];\n\n\tconst btEtRegex = /BT\\s([\\s\\S]*?)ET/g;\n\tfor (const match of raw.matchAll(btEtRegex)) {\n\t\tconst block = match[1];\n\t\tconst strRegex = /\\(([^)]*)\\)/g;\n\t\tfor (const strMatch of block.matchAll(strRegex)) {\n\t\t\tconst decoded = strMatch[1]\n\t\t\t\t.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t.replace(/\\\\r/g, \"\\r\")\n\t\t\t\t.replace(/\\\\t/g, \"\\t\")\n\t\t\t\t.replace(/\\\\\\(/g, \"(\")\n\t\t\t\t.replace(/\\\\\\)/g, \")\")\n\t\t\t\t.replace(/\\\\\\\\/g, \"\\\\\");\n\t\t\tif (decoded.trim()) {\n\t\t\t\ttextChunks.push(decoded);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (textChunks.length === 0) {\n\t\treturn \"[PDF text extraction failed — the PDF may use embedded fonts or image-based content that requires OCR]\";\n\t}\n\n\treturn textChunks.join(\" \").replace(/\\s+/g, \" \").trim();\n}\n\n// ---------------------------------------------------------------------------\n// web_search tool\n// ---------------------------------------------------------------------------\n\nconst webSearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n});\n\nexport type WebSearchToolInput = Static<typeof webSearchSchema>;\n\nexport interface WebSearchToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface SearchResult {\n\ttitle: string;\n\turl: string;\n\tsnippet: string;\n}\n\nasync function searchDuckDuckGo(query: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\t\"User-Agent\": \"dreb/1.0 (web search tool)\",\n\t\t\tAccept: \"text/html\",\n\t\t},\n\t\tredirect: \"follow\",\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`DuckDuckGo search failed: HTTP ${response.status}`);\n\t}\n\tconst html = await response.text();\n\tconst results: SearchResult[] = [];\n\n\t// Parse DuckDuckGo HTML results — split on result block class\n\tconst resultBlocks = html.split(/class=\"result results_links/);\n\tfor (const block of resultBlocks.slice(1, 11)) {\n\t\tconst titleMatch = block.match(/class=\"result__a\"[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/);\n\t\tconst snippetMatch = block.match(/class=\"result__snippet\"[^>]*>([\\s\\S]*?)<\\/(?:a|td|div)/);\n\n\t\tif (titleMatch) {\n\t\t\tlet url = titleMatch[1];\n\t\t\t// DDG wraps URLs in a redirect — extract the actual URL\n\t\t\tconst uddgMatch = url.match(/uddg=([^&]*)/);\n\t\t\tif (uddgMatch) {\n\t\t\t\turl = decodeURIComponent(uddgMatch[1]);\n\t\t\t}\n\t\t\tconst title = titleMatch[2].replace(/<[^>]+>/g, \"\").trim();\n\t\t\tconst snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, \"\").trim() : \"\";\n\t\t\tif (title && url) {\n\t\t\t\tresults.push({ title, url, snippet });\n\t\t\t}\n\t\t}\n\t}\n\tif (results.length === 0 && html.length > 1000) {\n\t\t// Got a substantial response but parsed 0 results — DDG HTML structure may have changed\n\t\tconsole.error(\"Warning: DDG returned HTML but 0 results were parsed. The HTML structure may have changed.\");\n\t}\n\treturn results;\n}\n\nasync function searchSearXNG(query: string, baseUrl: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`${baseUrl}/search?q=${encodedQuery}&format=json`, {\n\t\tmethod: \"GET\",\n\t\theaders: { Accept: \"application/json\" },\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`SearXNG search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as { results?: Array<{ title: string; url: string; content?: string }> };\n\treturn (data.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.content || \"\",\n\t}));\n}\n\nasync function searchBrave(query: string, apiKey: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"X-Subscription-Token\": apiKey,\n\t\t},\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Brave search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as {\n\t\tweb?: { results?: Array<{ title: string; url: string; description?: string }> };\n\t};\n\treturn (data.web?.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.description || \"\",\n\t}));\n}\n\nexport interface WebSearchConfig {\n\tbackend?: \"ddg\" | \"searxng\" | \"brave\";\n\tsearxngUrl?: string;\n\tbraveApiKey?: string;\n}\n\ninterface DrebConfig {\n\tsearch?: {\n\t\tbackend?: string;\n\t\tsearxng_url?: string;\n\t\tbrave_api_key?: string;\n\t};\n}\n\nconst VALID_BACKENDS = [\"ddg\", \"searxng\", \"brave\"] as const;\n\nfunction loadDrebConfig(): DrebConfig {\n\t// Config file precedence: project-local > home directory. First valid file wins.\n\tconst candidates = [\n\t\tjoin(process.cwd(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(process.cwd(), \".dreb\", \"config.json\"),\n\t\tjoin(homedir(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(homedir(), \".dreb\", \"config.json\"),\n\t];\n\tfor (const configPath of candidates) {\n\t\tif (existsSync(configPath)) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(readFileSync(configPath, \"utf-8\")) as DrebConfig;\n\t\t\t} catch (err) {\n\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\tconsole.error(`Warning: failed to parse config at ${configPath}: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nfunction getSearchConfig(): WebSearchConfig {\n\tconst fileConfig = loadDrebConfig();\n\t// Environment variables override config file\n\tconst rawBackend = process.env.DREB_SEARCH_BACKEND || fileConfig.search?.backend;\n\tlet backend: WebSearchConfig[\"backend\"] = \"ddg\";\n\tif (rawBackend) {\n\t\tif ((VALID_BACKENDS as readonly string[]).includes(rawBackend)) {\n\t\t\tbackend = rawBackend as WebSearchConfig[\"backend\"];\n\t\t} else {\n\t\t\tconsole.error(\n\t\t\t\t`Warning: unrecognized search backend \"${rawBackend}\", falling back to ddg. Valid: ${VALID_BACKENDS.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\treturn {\n\t\tbackend,\n\t\tsearxngUrl: process.env.DREB_SEARXNG_URL || fileConfig.search?.searxng_url || \"http://localhost:8888\",\n\t\tbraveApiKey: process.env.DREB_BRAVE_API_KEY || fileConfig.search?.brave_api_key,\n\t};\n}\n\nasync function executeSearch(query: string): Promise<SearchResult[]> {\n\tconst config = getSearchConfig();\n\tswitch (config.backend) {\n\t\tcase \"searxng\":\n\t\t\treturn searchSearXNG(query, config.searxngUrl!);\n\t\tcase \"brave\":\n\t\t\tif (!config.braveApiKey) throw new Error(\"DREB_BRAVE_API_KEY not set\");\n\t\t\treturn searchBrave(query, config.braveApiKey);\n\t\tdefault:\n\t\t\treturn searchDuckDuckGo(query);\n\t}\n}\n\nfunction formatSearchCall(\n\targs: { query: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst invalidArg = invalidArgText(theme);\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"web_search\")) +\n\t\t\" \" +\n\t\t(query === null ? invalidArg : theme.fg(\"accent\", `\"${query}\"`))\n\t);\n}\n\nfunction formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebSearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webSearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web. Returns titles, URLs, and snippets. Configure backend via DREB_SEARCH_BACKEND env var (ddg, searxng, brave).\",\n\t\tpromptSnippet: \"Search the web for information\",\n\t\tparameters: webSearchSchema,\n\t\tasync execute(_toolCallId, { query }: { query: string }) {\n\t\t\tlet results: SearchResult[];\n\t\t\ttry {\n\t\t\t\tresults = await executeSearch(query);\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Search failed for \"${query}\": ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `No results found for: ${query}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst formatted = results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join(\"\\n\\n\");\n\t\t\tconst output = `Search results for: ${query}\\n\\n${formatted}`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string): AgentTool<typeof webSearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd));\n}\n\nexport const webSearchToolDefinition = createWebSearchToolDefinition(process.cwd());\nexport const webSearchTool = createWebSearchTool(process.cwd());\n\n// ---------------------------------------------------------------------------\n// web_fetch tool\n// ---------------------------------------------------------------------------\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch\" }),\n});\n\nexport type WebFetchToolInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchToolDetails {\n\ttruncation?: TruncationResult;\n\ttruncatedContent?: boolean;\n}\n\nfunction formatFetchCall(\n\targs: { url: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst url = str(args?.url);\n\tconst invalidArg = invalidArgText(theme);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"web_fetch\"))} ${url === null ? invalidArg : theme.fg(\"accent\", url || \"\")}`;\n}\n\nfunction formatFetchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebFetchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 30;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst details = result.details;\n\tif (details?.truncatedContent || details?.truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (details.truncatedContent) warnings.push(`~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB content limit`);\n\t\tif (details.truncation?.truncated) warnings.push(`${formatSize(DEFAULT_MAX_BYTES)} output limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webFetchSchema, WebFetchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: `Fetch a URL and return its text content. Extracts readable text from HTML pages. Supports PDF text extraction. Content truncated to ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB. Results cached for 15 minutes.`,\n\t\tpromptSnippet: \"Fetch a URL and extract its text content\",\n\t\tparameters: webFetchSchema,\n\t\tasync execute(_toolCallId, { url }: { url: string }) {\n\t\t\t// Validate URL\n\t\t\tlet parsed: URL;\n\t\t\ttry {\n\t\t\t\tparsed = new URL(url);\n\t\t\t} catch {\n\t\t\t\t// URL constructor threw — input is not a valid URL\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Invalid URL: ${url}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!parsed.protocol.startsWith(\"http\")) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported protocol: ${parsed.protocol}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check cache (15-minute TTL, evict stale entries)\n\t\t\tconst cached = fetchCache.get(url);\n\t\t\tif (cached) {\n\t\t\t\tif (Date.now() - cached.timestamp < CACHE_TTL_MS) {\n\t\t\t\t\tconst r = cached.content;\n\t\t\t\t\tconst output = `${r.title}\\n${r.url}\\nFetched: ${r.fetchedAt} (cached)\\n\\n${r.content}`;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tfetchCache.delete(url);\n\t\t\t}\n\n\t\t\t// Fetch (with same-host redirect enforcement)\n\t\t\tlet body: string | Buffer;\n\t\t\tlet contentType: string;\n\t\t\ttry {\n\t\t\t\tconst result = await httpFetch(url);\n\t\t\t\tbody = result.body;\n\t\t\t\tcontentType = result.contentType;\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Failed to fetch ${url}: ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Extract content based on content type\n\t\t\tlet text: string;\n\t\t\tlet title: string;\n\t\t\tconst details: WebFetchToolDetails = {};\n\t\t\tconst fetchedAt = new Date().toISOString();\n\n\t\t\tif (contentType.includes(\"application/pdf\")) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = extractPdfText(body as Buffer);\n\t\t\t} else if (contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml\")) {\n\t\t\t\tconst htmlBody = body as string;\n\t\t\t\ttitle = extractTitle(htmlBody) || url;\n\t\t\t\ttext = stripHtmlToText(htmlBody);\n\t\t\t} else if (\n\t\t\t\tcontentType.includes(\"text/plain\") ||\n\t\t\t\tcontentType.includes(\"application/json\") ||\n\t\t\t\tcontentType.includes(\"text/xml\") ||\n\t\t\t\tcontentType.includes(\"application/xml\")\n\t\t\t) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = body as string;\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported content type: ${contentType}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Truncate to prevent context overflow (~100K characters)\n\t\t\tif (text.length > MAX_CONTENT_LENGTH) {\n\t\t\t\ttext = `${text.slice(0, MAX_CONTENT_LENGTH)}\\n\\n[Content truncated at ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB]`;\n\t\t\t\tdetails.truncatedContent = true;\n\t\t\t}\n\n\t\t\tconst fetchResult: WebFetchResult = { url, title, content: text, fetchedAt };\n\t\t\tfetchCache.set(url, { content: fetchResult, timestamp: Date.now() });\n\n\t\t\tconst output = `${title}\\n${url}\\nFetched: ${fetchedAt}\\n\\n${text}`;\n\t\t\tconst truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails.truncation = truncation;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: truncation.content }],\n\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string): AgentTool<typeof webFetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd));\n}\n\nexport const webFetchToolDefinition = createWebFetchToolDefinition(process.cwd());\nexport const webFetchTool = createWebFetchTool(process.cwd());\n"]}
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAgLnG,QAAA,MAAM,eAAe;;EAEnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,UAAU,YAAY;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB;AAuFD,MAAM,WAAW,eAAe;IAC/B,OAAO,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAkCD,wBAAgB,eAAe,IAAI,eAAe,CAyCjD;AAMD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAa1E;AAuCD,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA2C1E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,eAAe,CAAC,CAElF;AAED,eAAO,MAAM,uBAAuB;;0CAA+C,CAAC;AACpF,eAAO,MAAM,aAAa;;QAAqC,CAAC;AAMhE,QAAA,MAAM,cAAc;;EAElB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B;AA0CD,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CAmHxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,cAAc,CAAC,CAEhF;AAED,eAAO,MAAM,sBAAsB;;yCAA8C,CAAC;AAClF,eAAO,MAAM,YAAY;;QAAoC,CAAC","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { CONFIG_DIR_NAME } from \"../../config.js\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\nimport { WebSearchQueue } from \"./web-search-queue.js\";\n\n// ---------------------------------------------------------------------------\n// Shared: HTTP fetching and HTML extraction\n// ---------------------------------------------------------------------------\n\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_CONTENT_LENGTH = 100_000;\nconst CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes\n\nconst fetchCache = new Map<string, { content: WebFetchResult; timestamp: number }>();\n\ninterface WebFetchResult {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\tfetchedAt: string;\n}\n\nfunction stripHtmlToText(html: string): string {\n\tlet text = html;\n\t// Remove script/style/nav/footer blocks entirely\n\ttext = text.replace(/<(script|style|nav|footer|header|aside|iframe|noscript)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, \"\");\n\t// Convert block elements to newlines\n\ttext = text.replace(/<\\/(p|div|li|tr|h[1-6]|blockquote|pre|section|article)>/gi, \"\\n\");\n\ttext = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n\t// Convert links to text with URL\n\ttext = text.replace(/<a\\b[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$2 ($1)\");\n\t// Convert headings to markdown-style\n\ttext = text.replace(/<h([1-6])\\b[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_match, level, content) => {\n\t\treturn `\\n${\"#\".repeat(Number(level))} ${content.trim()}\\n`;\n\t});\n\t// Convert list items\n\ttext = text.replace(/<li\\b[^>]*>/gi, \"\\n- \");\n\t// Strip all remaining tags\n\ttext = text.replace(/<[^>]+>/g, \"\");\n\t// Decode common HTML entities\n\ttext = text.replace(/&amp;/g, \"&\");\n\ttext = text.replace(/&lt;/g, \"<\");\n\ttext = text.replace(/&gt;/g, \">\");\n\ttext = text.replace(/&quot;/g, '\"');\n\ttext = text.replace(/&#39;/g, \"'\");\n\ttext = text.replace(/&nbsp;/g, \" \");\n\t// Collapse whitespace\n\ttext = text.replace(/[ \\t]+/g, \" \");\n\ttext = text.replace(/\\n{3,}/g, \"\\n\\n\");\n\treturn text.trim();\n}\n\nfunction extractTitle(html: string): string {\n\tconst match = html.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i);\n\treturn match ? match[1].trim().replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : \"\";\n}\n\nconst FETCH_HEADERS = {\n\t\"User-Agent\": \"dreb/1.0 (web fetch tool)\",\n\tAccept: \"text/html,application/xhtml+xml,text/plain,application/pdf\",\n};\n\n// Block fetches to private/internal networks to prevent SSRF\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"0.0.0.0\"]);\n\nfunction isPrivateHost(hostname: string): boolean {\n\tif (BLOCKED_HOSTNAMES.has(hostname)) return true;\n\t// IPv4 private ranges\n\tconst ipv4Match = hostname.match(/^(\\d+)\\.(\\d+)\\.\\d+\\.\\d+$/);\n\tif (ipv4Match) {\n\t\tconst [, first, second] = ipv4Match.map(Number);\n\t\tif (first === 10) return true; // 10.0.0.0/8\n\t\tif (first === 172 && second >= 16 && second <= 31) return true; // 172.16.0.0/12\n\t\tif (first === 192 && second === 168) return true; // 192.168.0.0/16\n\t\tif (first === 169 && second === 254) return true; // link-local 169.254.0.0/16\n\t}\n\t// IPv6 loopback, link-local, and ULA (fc00::/7)\n\tif (hostname.startsWith(\"[\")) {\n\t\tconst lh = hostname.toLowerCase();\n\t\tif (lh.includes(\"::1\") || lh.startsWith(\"[fe80:\") || lh.startsWith(\"[fc\") || lh.startsWith(\"[fd\")) return true;\n\t}\n\treturn false;\n}\n\nfunction buildResponse(response: Response): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst ct = response.headers.get(\"content-type\") || \"\";\n\tif (ct.includes(\"application/pdf\")) {\n\t\treturn response.arrayBuffer().then((buf) => ({ body: Buffer.from(buf), contentType: ct }));\n\t}\n\treturn response.text().then((text) => ({ body: text, contentType: ct }));\n}\n\nasync function httpFetch(url: string): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst originalHost = new URL(url).hostname;\n\tif (isPrivateHost(originalHost)) {\n\t\tthrow new Error(`Blocked: ${originalHost} is a private/internal address`);\n\t}\n\n\t// Manual redirect loop to enforce same-host on every hop\n\tlet currentUrl = url;\n\tconst maxRedirects = 10;\n\tfor (let i = 0; i <= maxRedirects; i++) {\n\t\tconst response = await fetch(currentUrl, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: FETCH_HEADERS,\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t\t});\n\n\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) {\n\t\t\t\tthrow new Error(`HTTP ${response.status}: redirect with no Location header`);\n\t\t\t}\n\t\t\tconst redirectUrl = new URL(location, currentUrl);\n\t\t\t// Block private IPs before revealing them in cross-host messages\n\t\t\tif (isPrivateHost(redirectUrl.hostname)) {\n\t\t\t\tthrow new Error(`Blocked: redirect to private/internal address`);\n\t\t\t}\n\t\t\tif (redirectUrl.hostname !== originalHost) {\n\t\t\t\treturn {\n\t\t\t\t\tbody: `Cross-host redirect detected.\\nOriginal: ${url}\\nRedirects to: ${redirectUrl.href}\\n\\nThe redirect target is on a different host. Fetch the new URL directly if you want to follow it.`,\n\t\t\t\t\tcontentType: \"text/plain\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tcurrentUrl = redirectUrl.href;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst errorBody = await response.text();\n\t\t\tthrow new Error(`HTTP ${response.status}: ${errorBody.slice(0, 200)}`);\n\t\t}\n\n\t\treturn buildResponse(response);\n\t}\n\tthrow new Error(`Too many redirects (${maxRedirects})`);\n}\n\n// -- PDF text extraction (basic) ---------------------------------------------\n\nfunction extractPdfText(buffer: Buffer): string {\n\t// Minimal PDF text extraction — only works on uncompressed PDFs with literal\n\t// string objects in BT/ET text blocks. Most production PDFs use FlateDecode\n\t// compression and will fall through to the failure message.\n\t// latin1 preserves raw byte values 0-255 as code points for safe regex matching.\n\tconst raw = buffer.toString(\"latin1\");\n\tconst textChunks: string[] = [];\n\n\tconst btEtRegex = /BT\\s([\\s\\S]*?)ET/g;\n\tfor (const match of raw.matchAll(btEtRegex)) {\n\t\tconst block = match[1];\n\t\tconst strRegex = /\\(([^)]*)\\)/g;\n\t\tfor (const strMatch of block.matchAll(strRegex)) {\n\t\t\tconst decoded = strMatch[1]\n\t\t\t\t.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t.replace(/\\\\r/g, \"\\r\")\n\t\t\t\t.replace(/\\\\t/g, \"\\t\")\n\t\t\t\t.replace(/\\\\\\(/g, \"(\")\n\t\t\t\t.replace(/\\\\\\)/g, \")\")\n\t\t\t\t.replace(/\\\\\\\\/g, \"\\\\\");\n\t\t\tif (decoded.trim()) {\n\t\t\t\ttextChunks.push(decoded);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (textChunks.length === 0) {\n\t\treturn \"[PDF text extraction failed — the PDF may use embedded fonts or image-based content that requires OCR]\";\n\t}\n\n\treturn textChunks.join(\" \").replace(/\\s+/g, \" \").trim();\n}\n\n// ---------------------------------------------------------------------------\n// web_search tool\n// ---------------------------------------------------------------------------\n\nconst webSearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n});\n\nexport type WebSearchToolInput = Static<typeof webSearchSchema>;\n\nexport interface WebSearchToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface SearchResult {\n\ttitle: string;\n\turl: string;\n\tsnippet: string;\n}\n\nasync function searchDuckDuckGo(query: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\t\"User-Agent\": \"dreb/1.0 (web search tool)\",\n\t\t\tAccept: \"text/html\",\n\t\t},\n\t\tredirect: \"follow\",\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`DuckDuckGo search failed: HTTP ${response.status}`);\n\t}\n\tconst html = await response.text();\n\tconst results: SearchResult[] = [];\n\n\t// Parse DuckDuckGo HTML results — split on result block class\n\tconst resultBlocks = html.split(/class=\"result results_links/);\n\tfor (const block of resultBlocks.slice(1, 11)) {\n\t\tconst titleMatch = block.match(/class=\"result__a\"[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/);\n\t\tconst snippetMatch = block.match(/class=\"result__snippet\"[^>]*>([\\s\\S]*?)<\\/(?:a|td|div)/);\n\n\t\tif (titleMatch) {\n\t\t\tlet url = titleMatch[1];\n\t\t\t// DDG wraps URLs in a redirect — extract the actual URL\n\t\t\tconst uddgMatch = url.match(/uddg=([^&]*)/);\n\t\t\tif (uddgMatch) {\n\t\t\t\turl = decodeURIComponent(uddgMatch[1]);\n\t\t\t}\n\t\t\tconst title = titleMatch[2].replace(/<[^>]+>/g, \"\").trim();\n\t\t\tconst snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, \"\").trim() : \"\";\n\t\t\tif (title && url) {\n\t\t\t\tresults.push({ title, url, snippet });\n\t\t\t}\n\t\t}\n\t}\n\tif (results.length === 0 && html.length > 1000) {\n\t\t// Got a substantial response but parsed 0 results — DDG HTML structure may have changed\n\t\tconsole.error(\"Warning: DDG returned HTML but 0 results were parsed. The HTML structure may have changed.\");\n\t}\n\treturn results;\n}\n\nasync function searchSearXNG(query: string, baseUrl: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`${baseUrl}/search?q=${encodedQuery}&format=json`, {\n\t\tmethod: \"GET\",\n\t\theaders: { Accept: \"application/json\" },\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`SearXNG search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as { results?: Array<{ title: string; url: string; content?: string }> };\n\treturn (data.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.content || \"\",\n\t}));\n}\n\nasync function searchBrave(query: string, apiKey: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"X-Subscription-Token\": apiKey,\n\t\t},\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Brave search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as {\n\t\tweb?: { results?: Array<{ title: string; url: string; description?: string }> };\n\t};\n\treturn (data.web?.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.description || \"\",\n\t}));\n}\n\nexport interface WebSearchConfig {\n\tbackend?: \"ddg\" | \"searxng\" | \"brave\";\n\tsearxngUrl?: string;\n\tbraveApiKey?: string;\n\trateLimitMs?: number;\n}\n\ninterface DrebConfig {\n\tsearch?: {\n\t\tbackend?: string;\n\t\tsearxng_url?: string;\n\t\tbrave_api_key?: string;\n\t\trate_limit_ms?: number;\n\t};\n}\n\nconst VALID_BACKENDS = [\"ddg\", \"searxng\", \"brave\"] as const;\n\nfunction loadDrebConfig(): DrebConfig {\n\t// Config file precedence: project-local > home directory. First valid file wins.\n\tconst candidates = [\n\t\tjoin(process.cwd(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(process.cwd(), \".dreb\", \"config.json\"),\n\t\tjoin(homedir(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(homedir(), \".dreb\", \"config.json\"),\n\t];\n\tfor (const configPath of candidates) {\n\t\tif (existsSync(configPath)) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(readFileSync(configPath, \"utf-8\")) as DrebConfig;\n\t\t\t} catch (err) {\n\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\tconsole.error(`Warning: failed to parse config at ${configPath}: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nexport function getSearchConfig(): WebSearchConfig {\n\tconst fileConfig = loadDrebConfig();\n\t// Environment variables override config file\n\tconst rawBackend = process.env.DREB_SEARCH_BACKEND || fileConfig.search?.backend;\n\tlet backend: WebSearchConfig[\"backend\"] = \"ddg\";\n\tif (rawBackend) {\n\t\tif ((VALID_BACKENDS as readonly string[]).includes(rawBackend)) {\n\t\t\tbackend = rawBackend as WebSearchConfig[\"backend\"];\n\t\t} else {\n\t\t\tconsole.error(\n\t\t\t\t`Warning: unrecognized search backend \"${rawBackend}\", falling back to ddg. Valid: ${VALID_BACKENDS.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst rateLimitEnv = process.env.DREB_WEB_SEARCH_RATE_LIMIT_MS;\n\tlet rateLimitMs = 10_000;\n\tif (rateLimitEnv) {\n\t\tconst parsed = parseInt(rateLimitEnv, 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tconsole.error(`Warning: invalid DREB_WEB_SEARCH_RATE_LIMIT_MS \"${rateLimitEnv}\", using default`);\n\t\t}\n\t} else if (fileConfig.search?.rate_limit_ms !== undefined) {\n\t\tconst parsed = parseInt(String(fileConfig.search.rate_limit_ms), 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tconsole.error(\n\t\t\t\t`Warning: invalid search.rate_limit_ms in config file \"${fileConfig.search.rate_limit_ms}\", using default`,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn {\n\t\tbackend,\n\t\tsearxngUrl: process.env.DREB_SEARXNG_URL || fileConfig.search?.searxng_url || \"http://localhost:8888\",\n\t\tbraveApiKey: process.env.DREB_BRAVE_API_KEY || fileConfig.search?.brave_api_key,\n\t\trateLimitMs,\n\t};\n}\n\nfunction getSearchQueue(): WebSearchQueue {\n\treturn new WebSearchQueue({ rateLimitMs: getSearchConfig().rateLimitMs });\n}\n\nexport async function executeSearch(query: string): Promise<SearchResult[]> {\n\treturn getSearchQueue().enqueue(async () => {\n\t\tconst config = getSearchConfig();\n\t\tswitch (config.backend) {\n\t\t\tcase \"searxng\":\n\t\t\t\treturn searchSearXNG(query, config.searxngUrl!);\n\t\t\tcase \"brave\":\n\t\t\t\tif (!config.braveApiKey) throw new Error(\"DREB_BRAVE_API_KEY not set\");\n\t\t\t\treturn searchBrave(query, config.braveApiKey);\n\t\t\tdefault:\n\t\t\t\treturn searchDuckDuckGo(query);\n\t\t}\n\t});\n}\n\nfunction formatSearchCall(\n\targs: { query: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst invalidArg = invalidArgText(theme);\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"web_search\")) +\n\t\t\" \" +\n\t\t(query === null ? invalidArg : theme.fg(\"accent\", `\"${query}\"`))\n\t);\n}\n\nfunction formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebSearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webSearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web. Returns titles, URLs, and snippets. Configure backend via DREB_SEARCH_BACKEND env var (ddg, searxng, brave).\",\n\t\tpromptSnippet: \"Search the web for information\",\n\t\tparameters: webSearchSchema,\n\t\tasync execute(_toolCallId, { query }: { query: string }) {\n\t\t\tlet results: SearchResult[];\n\t\t\ttry {\n\t\t\t\tresults = await executeSearch(query);\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Search failed for \"${query}\": ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `No results found for: ${query}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst formatted = results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join(\"\\n\\n\");\n\t\t\tconst output = `Search results for: ${query}\\n\\n${formatted}`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string): AgentTool<typeof webSearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd));\n}\n\nexport const webSearchToolDefinition = createWebSearchToolDefinition(process.cwd());\nexport const webSearchTool = createWebSearchTool(process.cwd());\n\n// ---------------------------------------------------------------------------\n// web_fetch tool\n// ---------------------------------------------------------------------------\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch\" }),\n});\n\nexport type WebFetchToolInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchToolDetails {\n\ttruncation?: TruncationResult;\n\ttruncatedContent?: boolean;\n}\n\nfunction formatFetchCall(\n\targs: { url: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst url = str(args?.url);\n\tconst invalidArg = invalidArgText(theme);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"web_fetch\"))} ${url === null ? invalidArg : theme.fg(\"accent\", url || \"\")}`;\n}\n\nfunction formatFetchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebFetchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 30;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst details = result.details;\n\tif (details?.truncatedContent || details?.truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (details.truncatedContent) warnings.push(`~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB content limit`);\n\t\tif (details.truncation?.truncated) warnings.push(`${formatSize(DEFAULT_MAX_BYTES)} output limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webFetchSchema, WebFetchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: `Fetch a URL and return its text content. Extracts readable text from HTML pages. Supports PDF text extraction. Content truncated to ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB. Results cached for 15 minutes.`,\n\t\tpromptSnippet: \"Fetch a URL and extract its text content\",\n\t\tparameters: webFetchSchema,\n\t\tasync execute(_toolCallId, { url }: { url: string }) {\n\t\t\t// Validate URL\n\t\t\tlet parsed: URL;\n\t\t\ttry {\n\t\t\t\tparsed = new URL(url);\n\t\t\t} catch {\n\t\t\t\t// URL constructor threw — input is not a valid URL\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Invalid URL: ${url}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!parsed.protocol.startsWith(\"http\")) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported protocol: ${parsed.protocol}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check cache (15-minute TTL, evict stale entries)\n\t\t\tconst cached = fetchCache.get(url);\n\t\t\tif (cached) {\n\t\t\t\tif (Date.now() - cached.timestamp < CACHE_TTL_MS) {\n\t\t\t\t\tconst r = cached.content;\n\t\t\t\t\tconst output = `${r.title}\\n${r.url}\\nFetched: ${r.fetchedAt} (cached)\\n\\n${r.content}`;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tfetchCache.delete(url);\n\t\t\t}\n\n\t\t\t// Fetch (with same-host redirect enforcement)\n\t\t\tlet body: string | Buffer;\n\t\t\tlet contentType: string;\n\t\t\ttry {\n\t\t\t\tconst result = await httpFetch(url);\n\t\t\t\tbody = result.body;\n\t\t\t\tcontentType = result.contentType;\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Failed to fetch ${url}: ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Extract content based on content type\n\t\t\tlet text: string;\n\t\t\tlet title: string;\n\t\t\tconst details: WebFetchToolDetails = {};\n\t\t\tconst fetchedAt = new Date().toISOString();\n\n\t\t\tif (contentType.includes(\"application/pdf\")) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = extractPdfText(body as Buffer);\n\t\t\t} else if (contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml\")) {\n\t\t\t\tconst htmlBody = body as string;\n\t\t\t\ttitle = extractTitle(htmlBody) || url;\n\t\t\t\ttext = stripHtmlToText(htmlBody);\n\t\t\t} else if (\n\t\t\t\tcontentType.includes(\"text/plain\") ||\n\t\t\t\tcontentType.includes(\"application/json\") ||\n\t\t\t\tcontentType.includes(\"text/xml\") ||\n\t\t\t\tcontentType.includes(\"application/xml\")\n\t\t\t) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = body as string;\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported content type: ${contentType}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Truncate to prevent context overflow (~100K characters)\n\t\t\tif (text.length > MAX_CONTENT_LENGTH) {\n\t\t\t\ttext = `${text.slice(0, MAX_CONTENT_LENGTH)}\\n\\n[Content truncated at ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB]`;\n\t\t\t\tdetails.truncatedContent = true;\n\t\t\t}\n\n\t\t\tconst fetchResult: WebFetchResult = { url, title, content: text, fetchedAt };\n\t\t\tfetchCache.set(url, { content: fetchResult, timestamp: Date.now() });\n\n\t\t\tconst output = `${title}\\n${url}\\nFetched: ${fetchedAt}\\n\\n${text}`;\n\t\t\tconst truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails.truncation = truncation;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: truncation.content }],\n\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string): AgentTool<typeof webFetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd));\n}\n\nexport const webFetchToolDefinition = createWebFetchToolDefinition(process.cwd());\nexport const webFetchTool = createWebFetchTool(process.cwd());\n"]}
@@ -8,6 +8,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
8
8
  import { getTextOutput, invalidArgText, str } from "./render-utils.js";
9
9
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
10
10
  import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
11
+ import { WebSearchQueue } from "./web-search-queue.js";
11
12
  // ---------------------------------------------------------------------------
12
13
  // Shared: HTTP fetching and HTML extraction
13
14
  // ---------------------------------------------------------------------------
@@ -263,7 +264,7 @@ function loadDrebConfig() {
263
264
  }
264
265
  return {};
265
266
  }
266
- function getSearchConfig() {
267
+ export function getSearchConfig() {
267
268
  const fileConfig = loadDrebConfig();
268
269
  // Environment variables override config file
269
270
  const rawBackend = process.env.DREB_SEARCH_BACKEND || fileConfig.search?.backend;
@@ -276,24 +277,50 @@ function getSearchConfig() {
276
277
  console.error(`Warning: unrecognized search backend "${rawBackend}", falling back to ddg. Valid: ${VALID_BACKENDS.join(", ")}`);
277
278
  }
278
279
  }
280
+ const rateLimitEnv = process.env.DREB_WEB_SEARCH_RATE_LIMIT_MS;
281
+ let rateLimitMs = 10_000;
282
+ if (rateLimitEnv) {
283
+ const parsed = parseInt(rateLimitEnv, 10);
284
+ if (!Number.isNaN(parsed) && parsed >= 0) {
285
+ rateLimitMs = parsed;
286
+ }
287
+ else {
288
+ console.error(`Warning: invalid DREB_WEB_SEARCH_RATE_LIMIT_MS "${rateLimitEnv}", using default`);
289
+ }
290
+ }
291
+ else if (fileConfig.search?.rate_limit_ms !== undefined) {
292
+ const parsed = parseInt(String(fileConfig.search.rate_limit_ms), 10);
293
+ if (!Number.isNaN(parsed) && parsed >= 0) {
294
+ rateLimitMs = parsed;
295
+ }
296
+ else {
297
+ console.error(`Warning: invalid search.rate_limit_ms in config file "${fileConfig.search.rate_limit_ms}", using default`);
298
+ }
299
+ }
279
300
  return {
280
301
  backend,
281
302
  searxngUrl: process.env.DREB_SEARXNG_URL || fileConfig.search?.searxng_url || "http://localhost:8888",
282
303
  braveApiKey: process.env.DREB_BRAVE_API_KEY || fileConfig.search?.brave_api_key,
304
+ rateLimitMs,
283
305
  };
284
306
  }
285
- async function executeSearch(query) {
286
- const config = getSearchConfig();
287
- switch (config.backend) {
288
- case "searxng":
289
- return searchSearXNG(query, config.searxngUrl);
290
- case "brave":
291
- if (!config.braveApiKey)
292
- throw new Error("DREB_BRAVE_API_KEY not set");
293
- return searchBrave(query, config.braveApiKey);
294
- default:
295
- return searchDuckDuckGo(query);
296
- }
307
+ function getSearchQueue() {
308
+ return new WebSearchQueue({ rateLimitMs: getSearchConfig().rateLimitMs });
309
+ }
310
+ export async function executeSearch(query) {
311
+ return getSearchQueue().enqueue(async () => {
312
+ const config = getSearchConfig();
313
+ switch (config.backend) {
314
+ case "searxng":
315
+ return searchSearXNG(query, config.searxngUrl);
316
+ case "brave":
317
+ if (!config.braveApiKey)
318
+ throw new Error("DREB_BRAVE_API_KEY not set");
319
+ return searchBrave(query, config.braveApiKey);
320
+ default:
321
+ return searchDuckDuckGo(query);
322
+ }
323
+ });
297
324
  }
298
325
  function formatSearchCall(args, theme) {
299
326
  const query = str(args?.query);
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../../src/core/tools/web.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { CONFIG_DIR_NAME } from \"../../config.js\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n// ---------------------------------------------------------------------------\n// Shared: HTTP fetching and HTML extraction\n// ---------------------------------------------------------------------------\n\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_CONTENT_LENGTH = 100_000;\nconst CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes\n\nconst fetchCache = new Map<string, { content: WebFetchResult; timestamp: number }>();\n\ninterface WebFetchResult {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\tfetchedAt: string;\n}\n\nfunction stripHtmlToText(html: string): string {\n\tlet text = html;\n\t// Remove script/style/nav/footer blocks entirely\n\ttext = text.replace(/<(script|style|nav|footer|header|aside|iframe|noscript)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, \"\");\n\t// Convert block elements to newlines\n\ttext = text.replace(/<\\/(p|div|li|tr|h[1-6]|blockquote|pre|section|article)>/gi, \"\\n\");\n\ttext = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n\t// Convert links to text with URL\n\ttext = text.replace(/<a\\b[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$2 ($1)\");\n\t// Convert headings to markdown-style\n\ttext = text.replace(/<h([1-6])\\b[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_match, level, content) => {\n\t\treturn `\\n${\"#\".repeat(Number(level))} ${content.trim()}\\n`;\n\t});\n\t// Convert list items\n\ttext = text.replace(/<li\\b[^>]*>/gi, \"\\n- \");\n\t// Strip all remaining tags\n\ttext = text.replace(/<[^>]+>/g, \"\");\n\t// Decode common HTML entities\n\ttext = text.replace(/&amp;/g, \"&\");\n\ttext = text.replace(/&lt;/g, \"<\");\n\ttext = text.replace(/&gt;/g, \">\");\n\ttext = text.replace(/&quot;/g, '\"');\n\ttext = text.replace(/&#39;/g, \"'\");\n\ttext = text.replace(/&nbsp;/g, \" \");\n\t// Collapse whitespace\n\ttext = text.replace(/[ \\t]+/g, \" \");\n\ttext = text.replace(/\\n{3,}/g, \"\\n\\n\");\n\treturn text.trim();\n}\n\nfunction extractTitle(html: string): string {\n\tconst match = html.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i);\n\treturn match ? match[1].trim().replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : \"\";\n}\n\nconst FETCH_HEADERS = {\n\t\"User-Agent\": \"dreb/1.0 (web fetch tool)\",\n\tAccept: \"text/html,application/xhtml+xml,text/plain,application/pdf\",\n};\n\n// Block fetches to private/internal networks to prevent SSRF\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"0.0.0.0\"]);\n\nfunction isPrivateHost(hostname: string): boolean {\n\tif (BLOCKED_HOSTNAMES.has(hostname)) return true;\n\t// IPv4 private ranges\n\tconst ipv4Match = hostname.match(/^(\\d+)\\.(\\d+)\\.\\d+\\.\\d+$/);\n\tif (ipv4Match) {\n\t\tconst [, first, second] = ipv4Match.map(Number);\n\t\tif (first === 10) return true; // 10.0.0.0/8\n\t\tif (first === 172 && second >= 16 && second <= 31) return true; // 172.16.0.0/12\n\t\tif (first === 192 && second === 168) return true; // 192.168.0.0/16\n\t\tif (first === 169 && second === 254) return true; // link-local 169.254.0.0/16\n\t}\n\t// IPv6 loopback, link-local, and ULA (fc00::/7)\n\tif (hostname.startsWith(\"[\")) {\n\t\tconst lh = hostname.toLowerCase();\n\t\tif (lh.includes(\"::1\") || lh.startsWith(\"[fe80:\") || lh.startsWith(\"[fc\") || lh.startsWith(\"[fd\")) return true;\n\t}\n\treturn false;\n}\n\nfunction buildResponse(response: Response): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst ct = response.headers.get(\"content-type\") || \"\";\n\tif (ct.includes(\"application/pdf\")) {\n\t\treturn response.arrayBuffer().then((buf) => ({ body: Buffer.from(buf), contentType: ct }));\n\t}\n\treturn response.text().then((text) => ({ body: text, contentType: ct }));\n}\n\nasync function httpFetch(url: string): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst originalHost = new URL(url).hostname;\n\tif (isPrivateHost(originalHost)) {\n\t\tthrow new Error(`Blocked: ${originalHost} is a private/internal address`);\n\t}\n\n\t// Manual redirect loop to enforce same-host on every hop\n\tlet currentUrl = url;\n\tconst maxRedirects = 10;\n\tfor (let i = 0; i <= maxRedirects; i++) {\n\t\tconst response = await fetch(currentUrl, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: FETCH_HEADERS,\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t\t});\n\n\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) {\n\t\t\t\tthrow new Error(`HTTP ${response.status}: redirect with no Location header`);\n\t\t\t}\n\t\t\tconst redirectUrl = new URL(location, currentUrl);\n\t\t\t// Block private IPs before revealing them in cross-host messages\n\t\t\tif (isPrivateHost(redirectUrl.hostname)) {\n\t\t\t\tthrow new Error(`Blocked: redirect to private/internal address`);\n\t\t\t}\n\t\t\tif (redirectUrl.hostname !== originalHost) {\n\t\t\t\treturn {\n\t\t\t\t\tbody: `Cross-host redirect detected.\\nOriginal: ${url}\\nRedirects to: ${redirectUrl.href}\\n\\nThe redirect target is on a different host. Fetch the new URL directly if you want to follow it.`,\n\t\t\t\t\tcontentType: \"text/plain\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tcurrentUrl = redirectUrl.href;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst errorBody = await response.text();\n\t\t\tthrow new Error(`HTTP ${response.status}: ${errorBody.slice(0, 200)}`);\n\t\t}\n\n\t\treturn buildResponse(response);\n\t}\n\tthrow new Error(`Too many redirects (${maxRedirects})`);\n}\n\n// -- PDF text extraction (basic) ---------------------------------------------\n\nfunction extractPdfText(buffer: Buffer): string {\n\t// Minimal PDF text extraction — only works on uncompressed PDFs with literal\n\t// string objects in BT/ET text blocks. Most production PDFs use FlateDecode\n\t// compression and will fall through to the failure message.\n\t// latin1 preserves raw byte values 0-255 as code points for safe regex matching.\n\tconst raw = buffer.toString(\"latin1\");\n\tconst textChunks: string[] = [];\n\n\tconst btEtRegex = /BT\\s([\\s\\S]*?)ET/g;\n\tfor (const match of raw.matchAll(btEtRegex)) {\n\t\tconst block = match[1];\n\t\tconst strRegex = /\\(([^)]*)\\)/g;\n\t\tfor (const strMatch of block.matchAll(strRegex)) {\n\t\t\tconst decoded = strMatch[1]\n\t\t\t\t.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t.replace(/\\\\r/g, \"\\r\")\n\t\t\t\t.replace(/\\\\t/g, \"\\t\")\n\t\t\t\t.replace(/\\\\\\(/g, \"(\")\n\t\t\t\t.replace(/\\\\\\)/g, \")\")\n\t\t\t\t.replace(/\\\\\\\\/g, \"\\\\\");\n\t\t\tif (decoded.trim()) {\n\t\t\t\ttextChunks.push(decoded);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (textChunks.length === 0) {\n\t\treturn \"[PDF text extraction failed — the PDF may use embedded fonts or image-based content that requires OCR]\";\n\t}\n\n\treturn textChunks.join(\" \").replace(/\\s+/g, \" \").trim();\n}\n\n// ---------------------------------------------------------------------------\n// web_search tool\n// ---------------------------------------------------------------------------\n\nconst webSearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n});\n\nexport type WebSearchToolInput = Static<typeof webSearchSchema>;\n\nexport interface WebSearchToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface SearchResult {\n\ttitle: string;\n\turl: string;\n\tsnippet: string;\n}\n\nasync function searchDuckDuckGo(query: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\t\"User-Agent\": \"dreb/1.0 (web search tool)\",\n\t\t\tAccept: \"text/html\",\n\t\t},\n\t\tredirect: \"follow\",\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`DuckDuckGo search failed: HTTP ${response.status}`);\n\t}\n\tconst html = await response.text();\n\tconst results: SearchResult[] = [];\n\n\t// Parse DuckDuckGo HTML results — split on result block class\n\tconst resultBlocks = html.split(/class=\"result results_links/);\n\tfor (const block of resultBlocks.slice(1, 11)) {\n\t\tconst titleMatch = block.match(/class=\"result__a\"[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/);\n\t\tconst snippetMatch = block.match(/class=\"result__snippet\"[^>]*>([\\s\\S]*?)<\\/(?:a|td|div)/);\n\n\t\tif (titleMatch) {\n\t\t\tlet url = titleMatch[1];\n\t\t\t// DDG wraps URLs in a redirect — extract the actual URL\n\t\t\tconst uddgMatch = url.match(/uddg=([^&]*)/);\n\t\t\tif (uddgMatch) {\n\t\t\t\turl = decodeURIComponent(uddgMatch[1]);\n\t\t\t}\n\t\t\tconst title = titleMatch[2].replace(/<[^>]+>/g, \"\").trim();\n\t\t\tconst snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, \"\").trim() : \"\";\n\t\t\tif (title && url) {\n\t\t\t\tresults.push({ title, url, snippet });\n\t\t\t}\n\t\t}\n\t}\n\tif (results.length === 0 && html.length > 1000) {\n\t\t// Got a substantial response but parsed 0 results — DDG HTML structure may have changed\n\t\tconsole.error(\"Warning: DDG returned HTML but 0 results were parsed. The HTML structure may have changed.\");\n\t}\n\treturn results;\n}\n\nasync function searchSearXNG(query: string, baseUrl: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`${baseUrl}/search?q=${encodedQuery}&format=json`, {\n\t\tmethod: \"GET\",\n\t\theaders: { Accept: \"application/json\" },\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`SearXNG search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as { results?: Array<{ title: string; url: string; content?: string }> };\n\treturn (data.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.content || \"\",\n\t}));\n}\n\nasync function searchBrave(query: string, apiKey: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"X-Subscription-Token\": apiKey,\n\t\t},\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Brave search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as {\n\t\tweb?: { results?: Array<{ title: string; url: string; description?: string }> };\n\t};\n\treturn (data.web?.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.description || \"\",\n\t}));\n}\n\nexport interface WebSearchConfig {\n\tbackend?: \"ddg\" | \"searxng\" | \"brave\";\n\tsearxngUrl?: string;\n\tbraveApiKey?: string;\n}\n\ninterface DrebConfig {\n\tsearch?: {\n\t\tbackend?: string;\n\t\tsearxng_url?: string;\n\t\tbrave_api_key?: string;\n\t};\n}\n\nconst VALID_BACKENDS = [\"ddg\", \"searxng\", \"brave\"] as const;\n\nfunction loadDrebConfig(): DrebConfig {\n\t// Config file precedence: project-local > home directory. First valid file wins.\n\tconst candidates = [\n\t\tjoin(process.cwd(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(process.cwd(), \".dreb\", \"config.json\"),\n\t\tjoin(homedir(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(homedir(), \".dreb\", \"config.json\"),\n\t];\n\tfor (const configPath of candidates) {\n\t\tif (existsSync(configPath)) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(readFileSync(configPath, \"utf-8\")) as DrebConfig;\n\t\t\t} catch (err) {\n\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\tconsole.error(`Warning: failed to parse config at ${configPath}: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nfunction getSearchConfig(): WebSearchConfig {\n\tconst fileConfig = loadDrebConfig();\n\t// Environment variables override config file\n\tconst rawBackend = process.env.DREB_SEARCH_BACKEND || fileConfig.search?.backend;\n\tlet backend: WebSearchConfig[\"backend\"] = \"ddg\";\n\tif (rawBackend) {\n\t\tif ((VALID_BACKENDS as readonly string[]).includes(rawBackend)) {\n\t\t\tbackend = rawBackend as WebSearchConfig[\"backend\"];\n\t\t} else {\n\t\t\tconsole.error(\n\t\t\t\t`Warning: unrecognized search backend \"${rawBackend}\", falling back to ddg. Valid: ${VALID_BACKENDS.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\treturn {\n\t\tbackend,\n\t\tsearxngUrl: process.env.DREB_SEARXNG_URL || fileConfig.search?.searxng_url || \"http://localhost:8888\",\n\t\tbraveApiKey: process.env.DREB_BRAVE_API_KEY || fileConfig.search?.brave_api_key,\n\t};\n}\n\nasync function executeSearch(query: string): Promise<SearchResult[]> {\n\tconst config = getSearchConfig();\n\tswitch (config.backend) {\n\t\tcase \"searxng\":\n\t\t\treturn searchSearXNG(query, config.searxngUrl!);\n\t\tcase \"brave\":\n\t\t\tif (!config.braveApiKey) throw new Error(\"DREB_BRAVE_API_KEY not set\");\n\t\t\treturn searchBrave(query, config.braveApiKey);\n\t\tdefault:\n\t\t\treturn searchDuckDuckGo(query);\n\t}\n}\n\nfunction formatSearchCall(\n\targs: { query: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst invalidArg = invalidArgText(theme);\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"web_search\")) +\n\t\t\" \" +\n\t\t(query === null ? invalidArg : theme.fg(\"accent\", `\"${query}\"`))\n\t);\n}\n\nfunction formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebSearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webSearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web. Returns titles, URLs, and snippets. Configure backend via DREB_SEARCH_BACKEND env var (ddg, searxng, brave).\",\n\t\tpromptSnippet: \"Search the web for information\",\n\t\tparameters: webSearchSchema,\n\t\tasync execute(_toolCallId, { query }: { query: string }) {\n\t\t\tlet results: SearchResult[];\n\t\t\ttry {\n\t\t\t\tresults = await executeSearch(query);\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Search failed for \"${query}\": ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `No results found for: ${query}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst formatted = results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join(\"\\n\\n\");\n\t\t\tconst output = `Search results for: ${query}\\n\\n${formatted}`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string): AgentTool<typeof webSearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd));\n}\n\nexport const webSearchToolDefinition = createWebSearchToolDefinition(process.cwd());\nexport const webSearchTool = createWebSearchTool(process.cwd());\n\n// ---------------------------------------------------------------------------\n// web_fetch tool\n// ---------------------------------------------------------------------------\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch\" }),\n});\n\nexport type WebFetchToolInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchToolDetails {\n\ttruncation?: TruncationResult;\n\ttruncatedContent?: boolean;\n}\n\nfunction formatFetchCall(\n\targs: { url: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst url = str(args?.url);\n\tconst invalidArg = invalidArgText(theme);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"web_fetch\"))} ${url === null ? invalidArg : theme.fg(\"accent\", url || \"\")}`;\n}\n\nfunction formatFetchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebFetchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 30;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst details = result.details;\n\tif (details?.truncatedContent || details?.truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (details.truncatedContent) warnings.push(`~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB content limit`);\n\t\tif (details.truncation?.truncated) warnings.push(`${formatSize(DEFAULT_MAX_BYTES)} output limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webFetchSchema, WebFetchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: `Fetch a URL and return its text content. Extracts readable text from HTML pages. Supports PDF text extraction. Content truncated to ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB. Results cached for 15 minutes.`,\n\t\tpromptSnippet: \"Fetch a URL and extract its text content\",\n\t\tparameters: webFetchSchema,\n\t\tasync execute(_toolCallId, { url }: { url: string }) {\n\t\t\t// Validate URL\n\t\t\tlet parsed: URL;\n\t\t\ttry {\n\t\t\t\tparsed = new URL(url);\n\t\t\t} catch {\n\t\t\t\t// URL constructor threw — input is not a valid URL\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Invalid URL: ${url}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!parsed.protocol.startsWith(\"http\")) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported protocol: ${parsed.protocol}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check cache (15-minute TTL, evict stale entries)\n\t\t\tconst cached = fetchCache.get(url);\n\t\t\tif (cached) {\n\t\t\t\tif (Date.now() - cached.timestamp < CACHE_TTL_MS) {\n\t\t\t\t\tconst r = cached.content;\n\t\t\t\t\tconst output = `${r.title}\\n${r.url}\\nFetched: ${r.fetchedAt} (cached)\\n\\n${r.content}`;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tfetchCache.delete(url);\n\t\t\t}\n\n\t\t\t// Fetch (with same-host redirect enforcement)\n\t\t\tlet body: string | Buffer;\n\t\t\tlet contentType: string;\n\t\t\ttry {\n\t\t\t\tconst result = await httpFetch(url);\n\t\t\t\tbody = result.body;\n\t\t\t\tcontentType = result.contentType;\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Failed to fetch ${url}: ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Extract content based on content type\n\t\t\tlet text: string;\n\t\t\tlet title: string;\n\t\t\tconst details: WebFetchToolDetails = {};\n\t\t\tconst fetchedAt = new Date().toISOString();\n\n\t\t\tif (contentType.includes(\"application/pdf\")) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = extractPdfText(body as Buffer);\n\t\t\t} else if (contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml\")) {\n\t\t\t\tconst htmlBody = body as string;\n\t\t\t\ttitle = extractTitle(htmlBody) || url;\n\t\t\t\ttext = stripHtmlToText(htmlBody);\n\t\t\t} else if (\n\t\t\t\tcontentType.includes(\"text/plain\") ||\n\t\t\t\tcontentType.includes(\"application/json\") ||\n\t\t\t\tcontentType.includes(\"text/xml\") ||\n\t\t\t\tcontentType.includes(\"application/xml\")\n\t\t\t) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = body as string;\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported content type: ${contentType}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Truncate to prevent context overflow (~100K characters)\n\t\t\tif (text.length > MAX_CONTENT_LENGTH) {\n\t\t\t\ttext = `${text.slice(0, MAX_CONTENT_LENGTH)}\\n\\n[Content truncated at ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB]`;\n\t\t\t\tdetails.truncatedContent = true;\n\t\t\t}\n\n\t\t\tconst fetchResult: WebFetchResult = { url, title, content: text, fetchedAt };\n\t\t\tfetchCache.set(url, { content: fetchResult, timestamp: Date.now() });\n\n\t\t\tconst output = `${title}\\n${url}\\nFetched: ${fetchedAt}\\n\\n${text}`;\n\t\t\tconst truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails.truncation = truncation;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: truncation.content }],\n\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string): AgentTool<typeof webFetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd));\n}\n\nexport const webFetchToolDefinition = createWebFetchToolDefinition(process.cwd());\nexport const webFetchTool = createWebFetchTool(process.cwd());\n"]}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../../src/core/tools/web.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { CONFIG_DIR_NAME } from \"../../config.js\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\nimport { WebSearchQueue } from \"./web-search-queue.js\";\n\n// ---------------------------------------------------------------------------\n// Shared: HTTP fetching and HTML extraction\n// ---------------------------------------------------------------------------\n\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_CONTENT_LENGTH = 100_000;\nconst CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes\n\nconst fetchCache = new Map<string, { content: WebFetchResult; timestamp: number }>();\n\ninterface WebFetchResult {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\tfetchedAt: string;\n}\n\nfunction stripHtmlToText(html: string): string {\n\tlet text = html;\n\t// Remove script/style/nav/footer blocks entirely\n\ttext = text.replace(/<(script|style|nav|footer|header|aside|iframe|noscript)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, \"\");\n\t// Convert block elements to newlines\n\ttext = text.replace(/<\\/(p|div|li|tr|h[1-6]|blockquote|pre|section|article)>/gi, \"\\n\");\n\ttext = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n\t// Convert links to text with URL\n\ttext = text.replace(/<a\\b[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$2 ($1)\");\n\t// Convert headings to markdown-style\n\ttext = text.replace(/<h([1-6])\\b[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_match, level, content) => {\n\t\treturn `\\n${\"#\".repeat(Number(level))} ${content.trim()}\\n`;\n\t});\n\t// Convert list items\n\ttext = text.replace(/<li\\b[^>]*>/gi, \"\\n- \");\n\t// Strip all remaining tags\n\ttext = text.replace(/<[^>]+>/g, \"\");\n\t// Decode common HTML entities\n\ttext = text.replace(/&amp;/g, \"&\");\n\ttext = text.replace(/&lt;/g, \"<\");\n\ttext = text.replace(/&gt;/g, \">\");\n\ttext = text.replace(/&quot;/g, '\"');\n\ttext = text.replace(/&#39;/g, \"'\");\n\ttext = text.replace(/&nbsp;/g, \" \");\n\t// Collapse whitespace\n\ttext = text.replace(/[ \\t]+/g, \" \");\n\ttext = text.replace(/\\n{3,}/g, \"\\n\\n\");\n\treturn text.trim();\n}\n\nfunction extractTitle(html: string): string {\n\tconst match = html.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i);\n\treturn match ? match[1].trim().replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : \"\";\n}\n\nconst FETCH_HEADERS = {\n\t\"User-Agent\": \"dreb/1.0 (web fetch tool)\",\n\tAccept: \"text/html,application/xhtml+xml,text/plain,application/pdf\",\n};\n\n// Block fetches to private/internal networks to prevent SSRF\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"0.0.0.0\"]);\n\nfunction isPrivateHost(hostname: string): boolean {\n\tif (BLOCKED_HOSTNAMES.has(hostname)) return true;\n\t// IPv4 private ranges\n\tconst ipv4Match = hostname.match(/^(\\d+)\\.(\\d+)\\.\\d+\\.\\d+$/);\n\tif (ipv4Match) {\n\t\tconst [, first, second] = ipv4Match.map(Number);\n\t\tif (first === 10) return true; // 10.0.0.0/8\n\t\tif (first === 172 && second >= 16 && second <= 31) return true; // 172.16.0.0/12\n\t\tif (first === 192 && second === 168) return true; // 192.168.0.0/16\n\t\tif (first === 169 && second === 254) return true; // link-local 169.254.0.0/16\n\t}\n\t// IPv6 loopback, link-local, and ULA (fc00::/7)\n\tif (hostname.startsWith(\"[\")) {\n\t\tconst lh = hostname.toLowerCase();\n\t\tif (lh.includes(\"::1\") || lh.startsWith(\"[fe80:\") || lh.startsWith(\"[fc\") || lh.startsWith(\"[fd\")) return true;\n\t}\n\treturn false;\n}\n\nfunction buildResponse(response: Response): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst ct = response.headers.get(\"content-type\") || \"\";\n\tif (ct.includes(\"application/pdf\")) {\n\t\treturn response.arrayBuffer().then((buf) => ({ body: Buffer.from(buf), contentType: ct }));\n\t}\n\treturn response.text().then((text) => ({ body: text, contentType: ct }));\n}\n\nasync function httpFetch(url: string): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst originalHost = new URL(url).hostname;\n\tif (isPrivateHost(originalHost)) {\n\t\tthrow new Error(`Blocked: ${originalHost} is a private/internal address`);\n\t}\n\n\t// Manual redirect loop to enforce same-host on every hop\n\tlet currentUrl = url;\n\tconst maxRedirects = 10;\n\tfor (let i = 0; i <= maxRedirects; i++) {\n\t\tconst response = await fetch(currentUrl, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: FETCH_HEADERS,\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t\t});\n\n\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) {\n\t\t\t\tthrow new Error(`HTTP ${response.status}: redirect with no Location header`);\n\t\t\t}\n\t\t\tconst redirectUrl = new URL(location, currentUrl);\n\t\t\t// Block private IPs before revealing them in cross-host messages\n\t\t\tif (isPrivateHost(redirectUrl.hostname)) {\n\t\t\t\tthrow new Error(`Blocked: redirect to private/internal address`);\n\t\t\t}\n\t\t\tif (redirectUrl.hostname !== originalHost) {\n\t\t\t\treturn {\n\t\t\t\t\tbody: `Cross-host redirect detected.\\nOriginal: ${url}\\nRedirects to: ${redirectUrl.href}\\n\\nThe redirect target is on a different host. Fetch the new URL directly if you want to follow it.`,\n\t\t\t\t\tcontentType: \"text/plain\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tcurrentUrl = redirectUrl.href;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst errorBody = await response.text();\n\t\t\tthrow new Error(`HTTP ${response.status}: ${errorBody.slice(0, 200)}`);\n\t\t}\n\n\t\treturn buildResponse(response);\n\t}\n\tthrow new Error(`Too many redirects (${maxRedirects})`);\n}\n\n// -- PDF text extraction (basic) ---------------------------------------------\n\nfunction extractPdfText(buffer: Buffer): string {\n\t// Minimal PDF text extraction — only works on uncompressed PDFs with literal\n\t// string objects in BT/ET text blocks. Most production PDFs use FlateDecode\n\t// compression and will fall through to the failure message.\n\t// latin1 preserves raw byte values 0-255 as code points for safe regex matching.\n\tconst raw = buffer.toString(\"latin1\");\n\tconst textChunks: string[] = [];\n\n\tconst btEtRegex = /BT\\s([\\s\\S]*?)ET/g;\n\tfor (const match of raw.matchAll(btEtRegex)) {\n\t\tconst block = match[1];\n\t\tconst strRegex = /\\(([^)]*)\\)/g;\n\t\tfor (const strMatch of block.matchAll(strRegex)) {\n\t\t\tconst decoded = strMatch[1]\n\t\t\t\t.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t.replace(/\\\\r/g, \"\\r\")\n\t\t\t\t.replace(/\\\\t/g, \"\\t\")\n\t\t\t\t.replace(/\\\\\\(/g, \"(\")\n\t\t\t\t.replace(/\\\\\\)/g, \")\")\n\t\t\t\t.replace(/\\\\\\\\/g, \"\\\\\");\n\t\t\tif (decoded.trim()) {\n\t\t\t\ttextChunks.push(decoded);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (textChunks.length === 0) {\n\t\treturn \"[PDF text extraction failed — the PDF may use embedded fonts or image-based content that requires OCR]\";\n\t}\n\n\treturn textChunks.join(\" \").replace(/\\s+/g, \" \").trim();\n}\n\n// ---------------------------------------------------------------------------\n// web_search tool\n// ---------------------------------------------------------------------------\n\nconst webSearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n});\n\nexport type WebSearchToolInput = Static<typeof webSearchSchema>;\n\nexport interface WebSearchToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface SearchResult {\n\ttitle: string;\n\turl: string;\n\tsnippet: string;\n}\n\nasync function searchDuckDuckGo(query: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\t\"User-Agent\": \"dreb/1.0 (web search tool)\",\n\t\t\tAccept: \"text/html\",\n\t\t},\n\t\tredirect: \"follow\",\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`DuckDuckGo search failed: HTTP ${response.status}`);\n\t}\n\tconst html = await response.text();\n\tconst results: SearchResult[] = [];\n\n\t// Parse DuckDuckGo HTML results — split on result block class\n\tconst resultBlocks = html.split(/class=\"result results_links/);\n\tfor (const block of resultBlocks.slice(1, 11)) {\n\t\tconst titleMatch = block.match(/class=\"result__a\"[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/);\n\t\tconst snippetMatch = block.match(/class=\"result__snippet\"[^>]*>([\\s\\S]*?)<\\/(?:a|td|div)/);\n\n\t\tif (titleMatch) {\n\t\t\tlet url = titleMatch[1];\n\t\t\t// DDG wraps URLs in a redirect — extract the actual URL\n\t\t\tconst uddgMatch = url.match(/uddg=([^&]*)/);\n\t\t\tif (uddgMatch) {\n\t\t\t\turl = decodeURIComponent(uddgMatch[1]);\n\t\t\t}\n\t\t\tconst title = titleMatch[2].replace(/<[^>]+>/g, \"\").trim();\n\t\t\tconst snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, \"\").trim() : \"\";\n\t\t\tif (title && url) {\n\t\t\t\tresults.push({ title, url, snippet });\n\t\t\t}\n\t\t}\n\t}\n\tif (results.length === 0 && html.length > 1000) {\n\t\t// Got a substantial response but parsed 0 results — DDG HTML structure may have changed\n\t\tconsole.error(\"Warning: DDG returned HTML but 0 results were parsed. The HTML structure may have changed.\");\n\t}\n\treturn results;\n}\n\nasync function searchSearXNG(query: string, baseUrl: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`${baseUrl}/search?q=${encodedQuery}&format=json`, {\n\t\tmethod: \"GET\",\n\t\theaders: { Accept: \"application/json\" },\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`SearXNG search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as { results?: Array<{ title: string; url: string; content?: string }> };\n\treturn (data.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.content || \"\",\n\t}));\n}\n\nasync function searchBrave(query: string, apiKey: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"X-Subscription-Token\": apiKey,\n\t\t},\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Brave search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as {\n\t\tweb?: { results?: Array<{ title: string; url: string; description?: string }> };\n\t};\n\treturn (data.web?.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.description || \"\",\n\t}));\n}\n\nexport interface WebSearchConfig {\n\tbackend?: \"ddg\" | \"searxng\" | \"brave\";\n\tsearxngUrl?: string;\n\tbraveApiKey?: string;\n\trateLimitMs?: number;\n}\n\ninterface DrebConfig {\n\tsearch?: {\n\t\tbackend?: string;\n\t\tsearxng_url?: string;\n\t\tbrave_api_key?: string;\n\t\trate_limit_ms?: number;\n\t};\n}\n\nconst VALID_BACKENDS = [\"ddg\", \"searxng\", \"brave\"] as const;\n\nfunction loadDrebConfig(): DrebConfig {\n\t// Config file precedence: project-local > home directory. First valid file wins.\n\tconst candidates = [\n\t\tjoin(process.cwd(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(process.cwd(), \".dreb\", \"config.json\"),\n\t\tjoin(homedir(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(homedir(), \".dreb\", \"config.json\"),\n\t];\n\tfor (const configPath of candidates) {\n\t\tif (existsSync(configPath)) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(readFileSync(configPath, \"utf-8\")) as DrebConfig;\n\t\t\t} catch (err) {\n\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\tconsole.error(`Warning: failed to parse config at ${configPath}: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nexport function getSearchConfig(): WebSearchConfig {\n\tconst fileConfig = loadDrebConfig();\n\t// Environment variables override config file\n\tconst rawBackend = process.env.DREB_SEARCH_BACKEND || fileConfig.search?.backend;\n\tlet backend: WebSearchConfig[\"backend\"] = \"ddg\";\n\tif (rawBackend) {\n\t\tif ((VALID_BACKENDS as readonly string[]).includes(rawBackend)) {\n\t\t\tbackend = rawBackend as WebSearchConfig[\"backend\"];\n\t\t} else {\n\t\t\tconsole.error(\n\t\t\t\t`Warning: unrecognized search backend \"${rawBackend}\", falling back to ddg. Valid: ${VALID_BACKENDS.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst rateLimitEnv = process.env.DREB_WEB_SEARCH_RATE_LIMIT_MS;\n\tlet rateLimitMs = 10_000;\n\tif (rateLimitEnv) {\n\t\tconst parsed = parseInt(rateLimitEnv, 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tconsole.error(`Warning: invalid DREB_WEB_SEARCH_RATE_LIMIT_MS \"${rateLimitEnv}\", using default`);\n\t\t}\n\t} else if (fileConfig.search?.rate_limit_ms !== undefined) {\n\t\tconst parsed = parseInt(String(fileConfig.search.rate_limit_ms), 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tconsole.error(\n\t\t\t\t`Warning: invalid search.rate_limit_ms in config file \"${fileConfig.search.rate_limit_ms}\", using default`,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn {\n\t\tbackend,\n\t\tsearxngUrl: process.env.DREB_SEARXNG_URL || fileConfig.search?.searxng_url || \"http://localhost:8888\",\n\t\tbraveApiKey: process.env.DREB_BRAVE_API_KEY || fileConfig.search?.brave_api_key,\n\t\trateLimitMs,\n\t};\n}\n\nfunction getSearchQueue(): WebSearchQueue {\n\treturn new WebSearchQueue({ rateLimitMs: getSearchConfig().rateLimitMs });\n}\n\nexport async function executeSearch(query: string): Promise<SearchResult[]> {\n\treturn getSearchQueue().enqueue(async () => {\n\t\tconst config = getSearchConfig();\n\t\tswitch (config.backend) {\n\t\t\tcase \"searxng\":\n\t\t\t\treturn searchSearXNG(query, config.searxngUrl!);\n\t\t\tcase \"brave\":\n\t\t\t\tif (!config.braveApiKey) throw new Error(\"DREB_BRAVE_API_KEY not set\");\n\t\t\t\treturn searchBrave(query, config.braveApiKey);\n\t\t\tdefault:\n\t\t\t\treturn searchDuckDuckGo(query);\n\t\t}\n\t});\n}\n\nfunction formatSearchCall(\n\targs: { query: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst invalidArg = invalidArgText(theme);\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"web_search\")) +\n\t\t\" \" +\n\t\t(query === null ? invalidArg : theme.fg(\"accent\", `\"${query}\"`))\n\t);\n}\n\nfunction formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebSearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webSearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web. Returns titles, URLs, and snippets. Configure backend via DREB_SEARCH_BACKEND env var (ddg, searxng, brave).\",\n\t\tpromptSnippet: \"Search the web for information\",\n\t\tparameters: webSearchSchema,\n\t\tasync execute(_toolCallId, { query }: { query: string }) {\n\t\t\tlet results: SearchResult[];\n\t\t\ttry {\n\t\t\t\tresults = await executeSearch(query);\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Search failed for \"${query}\": ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `No results found for: ${query}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst formatted = results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join(\"\\n\\n\");\n\t\t\tconst output = `Search results for: ${query}\\n\\n${formatted}`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string): AgentTool<typeof webSearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd));\n}\n\nexport const webSearchToolDefinition = createWebSearchToolDefinition(process.cwd());\nexport const webSearchTool = createWebSearchTool(process.cwd());\n\n// ---------------------------------------------------------------------------\n// web_fetch tool\n// ---------------------------------------------------------------------------\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch\" }),\n});\n\nexport type WebFetchToolInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchToolDetails {\n\ttruncation?: TruncationResult;\n\ttruncatedContent?: boolean;\n}\n\nfunction formatFetchCall(\n\targs: { url: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst url = str(args?.url);\n\tconst invalidArg = invalidArgText(theme);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"web_fetch\"))} ${url === null ? invalidArg : theme.fg(\"accent\", url || \"\")}`;\n}\n\nfunction formatFetchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebFetchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 30;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst details = result.details;\n\tif (details?.truncatedContent || details?.truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (details.truncatedContent) warnings.push(`~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB content limit`);\n\t\tif (details.truncation?.truncated) warnings.push(`${formatSize(DEFAULT_MAX_BYTES)} output limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webFetchSchema, WebFetchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: `Fetch a URL and return its text content. Extracts readable text from HTML pages. Supports PDF text extraction. Content truncated to ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB. Results cached for 15 minutes.`,\n\t\tpromptSnippet: \"Fetch a URL and extract its text content\",\n\t\tparameters: webFetchSchema,\n\t\tasync execute(_toolCallId, { url }: { url: string }) {\n\t\t\t// Validate URL\n\t\t\tlet parsed: URL;\n\t\t\ttry {\n\t\t\t\tparsed = new URL(url);\n\t\t\t} catch {\n\t\t\t\t// URL constructor threw — input is not a valid URL\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Invalid URL: ${url}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!parsed.protocol.startsWith(\"http\")) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported protocol: ${parsed.protocol}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check cache (15-minute TTL, evict stale entries)\n\t\t\tconst cached = fetchCache.get(url);\n\t\t\tif (cached) {\n\t\t\t\tif (Date.now() - cached.timestamp < CACHE_TTL_MS) {\n\t\t\t\t\tconst r = cached.content;\n\t\t\t\t\tconst output = `${r.title}\\n${r.url}\\nFetched: ${r.fetchedAt} (cached)\\n\\n${r.content}`;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tfetchCache.delete(url);\n\t\t\t}\n\n\t\t\t// Fetch (with same-host redirect enforcement)\n\t\t\tlet body: string | Buffer;\n\t\t\tlet contentType: string;\n\t\t\ttry {\n\t\t\t\tconst result = await httpFetch(url);\n\t\t\t\tbody = result.body;\n\t\t\t\tcontentType = result.contentType;\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Failed to fetch ${url}: ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Extract content based on content type\n\t\t\tlet text: string;\n\t\t\tlet title: string;\n\t\t\tconst details: WebFetchToolDetails = {};\n\t\t\tconst fetchedAt = new Date().toISOString();\n\n\t\t\tif (contentType.includes(\"application/pdf\")) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = extractPdfText(body as Buffer);\n\t\t\t} else if (contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml\")) {\n\t\t\t\tconst htmlBody = body as string;\n\t\t\t\ttitle = extractTitle(htmlBody) || url;\n\t\t\t\ttext = stripHtmlToText(htmlBody);\n\t\t\t} else if (\n\t\t\t\tcontentType.includes(\"text/plain\") ||\n\t\t\t\tcontentType.includes(\"application/json\") ||\n\t\t\t\tcontentType.includes(\"text/xml\") ||\n\t\t\t\tcontentType.includes(\"application/xml\")\n\t\t\t) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = body as string;\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported content type: ${contentType}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Truncate to prevent context overflow (~100K characters)\n\t\t\tif (text.length > MAX_CONTENT_LENGTH) {\n\t\t\t\ttext = `${text.slice(0, MAX_CONTENT_LENGTH)}\\n\\n[Content truncated at ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB]`;\n\t\t\t\tdetails.truncatedContent = true;\n\t\t\t}\n\n\t\t\tconst fetchResult: WebFetchResult = { url, title, content: text, fetchedAt };\n\t\t\tfetchCache.set(url, { content: fetchResult, timestamp: Date.now() });\n\n\t\t\tconst output = `${title}\\n${url}\\nFetched: ${fetchedAt}\\n\\n${text}`;\n\t\t\tconst truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails.truncation = truncation;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: truncation.content }],\n\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string): AgentTool<typeof webFetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd));\n}\n\nexport const webFetchToolDefinition = createWebFetchToolDefinition(process.cwd());\nexport const webFetchTool = createWebFetchTool(process.cwd());\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreb/coding-agent",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "drebConfig": {
@@ -18,6 +18,7 @@ This skill has two modes:
18
18
  2. **No `#N` in comment bodies** — Use "finding 3", "item 3" etc. instead.
19
19
  3. **Safe git** — Never use `git add -A` or `git add .`. Stage files by name. Never stage secrets.
20
20
  4. **Task tracking** — Use the `tasks_update` tool to show progress.
21
+ 5. **Non-interactive `gh`** — Set `GH_PAGER=cat` and `GH_EDITOR=cat` before all `gh` commands to prevent interactive prompts from hanging the agent. Use `--body-file` instead of inline `--body` for all `gh pr comment`, `gh pr create`, and `gh issue create` calls to avoid shell interpretation of backticks.
21
22
 
22
23
  ## Step 1: Parse input
23
24
 
@@ -121,6 +122,8 @@ gh pr checks <pr-number>
121
122
  gh run view <run-id> --log-failed
122
123
  ```
123
124
 
125
+ **Note:** `gh pr checks` returns exit code 8 while checks are still pending — this is expected, not a failure. Wait and re-run if needed.
126
+
124
127
  Read the failed CI logs and identify issues. Extract test failures, stack traces, error messages. If all checks pass, report this and stop.
125
128
 
126
129
  #### If finding numbers were specified:
@@ -16,6 +16,7 @@ argument-hint: "[issue-number | description]"
16
16
  4. **Safe git** — Never use `git add -A` or `git add .`. Stage files by name. Never stage secrets (.env, credentials, tokens, keys).
17
17
  5. **Task tracking** — Use the `tasks_update` tool to show progress through multi-step commands.
18
18
  6. **Project conventions** — Check for CLAUDE.md, AGENTS.md, .dreb/CONTEXT.md, and CONTRIBUTING.md before planning or implementing.
19
+ 7. **Non-interactive `gh`** — Set `GH_PAGER=cat` and `GH_EDITOR=cat` before all `gh` commands to prevent interactive prompts from hanging the agent. Use `--body-file` instead of inline `--body` for all `gh pr comment`, `gh pr create`, and `gh issue create` calls to avoid shell interpretation of backticks.
19
20
 
20
21
  ## Determine Mode
21
22
 
@@ -75,13 +76,16 @@ Present to the user:
75
76
  Post as an issue comment:
76
77
 
77
78
  ```bash
78
- gh issue comment <number> --body "<!-- mach6-assessment -->
79
+ cat > /tmp/gh-comment.md << 'MACH6_EOF'
80
+ <!-- mach6-assessment -->
79
81
  ## Issue Assessment
80
82
 
81
83
  <assessment content>
82
84
 
83
85
  ---
84
- *Automated assessment by mach6*"
86
+ *Automated assessment by mach6*
87
+ MACH6_EOF
88
+ gh issue comment <number> --body-file /tmp/gh-comment.md
85
89
  ```
86
90
 
87
91
  Update task: post → completed.
@@ -123,7 +127,10 @@ Present the draft to the user for approval.
123
127
  ### Step 3: Create the issue
124
128
 
125
129
  ```bash
126
- gh issue create --title "<title>" --body "<body>" [--label "<labels>"]
130
+ cat > /tmp/gh-body.md << 'MACH6_EOF'
131
+ <body>
132
+ MACH6_EOF
133
+ gh issue create --title "<title>" --body-file /tmp/gh-body.md [--label "<labels>"]
127
134
  ```
128
135
 
129
136
  Report the issue number and URL. Suggest next step: `/skill:mach6-plan <number>`
@@ -18,6 +18,7 @@ This command is strictly for **planning**. Do NOT implement any code changes —
18
18
  4. **Safe git** — Never use `git add -A` or `git add .`. Stage files by name. Never stage secrets.
19
19
  5. **Task tracking** — Use the `tasks_update` tool to show progress through multi-step commands.
20
20
  6. **Project conventions** — Check for CLAUDE.md, AGENTS.md, .dreb/CONTEXT.md, and CONTRIBUTING.md before planning.
21
+ 7. **Non-interactive `gh`** — Set `GH_PAGER=cat` and `GH_EDITOR=cat` before all `gh` commands to prevent interactive prompts from hanging the agent. Use `--body-file` instead of inline `--body` for all `gh pr comment`, `gh pr create`, and `gh issue create` calls to avoid shell interpretation of backticks.
21
22
 
22
23
  ## Step 1: Set up task tracking
23
24
 
@@ -97,11 +98,14 @@ git commit --allow-empty -m "chore: open PR for issue <N>"
97
98
  git push -u origin feature/issue-<N>-<slug>
98
99
 
99
100
  # Open draft PR
100
- gh pr create --draft --title "<title>" --body "Closes #<N>
101
+ cat > /tmp/gh-body.md << 'MACH6_EOF'
102
+ Closes #<N>
101
103
 
102
104
  <brief description>
103
105
 
104
- Implementation plan posted as a comment below."
106
+ Implementation plan posted as a comment below.
107
+ MACH6_EOF
108
+ gh pr create --draft --title "<title>" --body-file /tmp/gh-body.md
105
109
  ```
106
110
 
107
111
  Update task: branch → completed, post → in_progress.
@@ -109,13 +113,16 @@ Update task: branch → completed, post → in_progress.
109
113
  ## Step 7: Post plan to PR
110
114
 
111
115
  ```bash
112
- gh pr comment <pr-number> --body "<!-- mach6-plan -->
116
+ cat > /tmp/gh-comment.md << 'MACH6_EOF'
117
+ <!-- mach6-plan -->
113
118
  ## Implementation Plan
114
119
 
115
120
  <full plan content>
116
121
 
117
122
  ---
118
- *Plan created by mach6*"
123
+ *Plan created by mach6*
124
+ MACH6_EOF
125
+ gh pr comment <pr-number> --body-file /tmp/gh-comment.md
119
126
  ```
120
127
 
121
128
  Update task: post → completed.
@@ -14,6 +14,7 @@ argument-hint: "<pr-number>"
14
14
  2. **No `#N` in comment bodies** — Use "finding 3", "item 3" etc. instead.
15
15
  3. **Safe git** — Never use `git add -A` or `git add .`. Stage files by name. Never stage secrets.
16
16
  4. **Task tracking** — Use the `tasks_update` tool to show progress.
17
+ 5. **Non-interactive `gh`** — Set `GH_PAGER=cat` and `GH_EDITOR=cat` before all `gh` commands to prevent interactive prompts from hanging the agent. Use `--body-file` instead of inline `--body` for all `gh pr comment`, `gh pr create`, and `gh issue create` calls to avoid shell interpretation of backticks.
17
18
 
18
19
  ## Step 1: Set up task tracking
19
20
 
@@ -36,6 +37,8 @@ gh pr view <pr-number> --json mergeable,mergeStateStatus,statusCheckRollup,revie
36
37
  gh pr checks <pr-number>
37
38
  ```
38
39
 
40
+ **Note:** `gh pr checks` returns exit code 8 while checks are still pending — this is expected, not a failure. Wait and re-run if needed.
41
+
39
42
  Read ALL PR comments to understand the full history — plans, reviews, assessments, progress updates, and discussion.
40
43
 
41
44
  Verify:
@@ -178,7 +181,10 @@ git push --tags
178
181
 
179
182
  3. Present draft to user for approval, then create:
180
183
  ```bash
181
- gh release create v<version> --title "v<version>" --notes "<release-notes>"
184
+ cat > /tmp/gh-release-notes.md << 'MACH6_EOF'
185
+ <release-notes>
186
+ MACH6_EOF
187
+ gh release create v<version> --title "v<version>" --notes-file /tmp/gh-release-notes.md
182
188
  ```
183
189
 
184
190
  Update task: release → completed.
@@ -15,6 +15,7 @@ argument-hint: "[commit message]"
15
15
  3. **No `#N` in comment bodies** — Use "finding 3", "item 3", "stage 2" etc. instead.
16
16
  4. **Safe git** — Never use `git add -A` or `git add .`. Stage files by name. Never stage secrets (.env, credentials, tokens, keys).
17
17
  5. **Task tracking** — Use the `tasks_update` tool to show progress.
18
+ 6. **Non-interactive `gh`** — Set `GH_PAGER=cat` and `GH_EDITOR=cat` before all `gh` commands to prevent interactive prompts from hanging the agent. Use `--body-file` instead of inline `--body` for all `gh pr comment`, `gh pr create`, and `gh issue create` calls to avoid shell interpretation of backticks.
18
19
 
19
20
  ## Step 1: Set up task tracking
20
21
 
@@ -80,7 +81,8 @@ If session context points to an issue but a PR also exists on the current branch
80
81
 
81
82
  Post a progress comment:
82
83
  ```bash
83
- gh pr comment <number> --body "<!-- mach6-progress -->
84
+ cat > /tmp/gh-comment.md << 'MACH6_EOF'
85
+ <!-- mach6-progress -->
84
86
  ## Progress Update
85
87
 
86
88
  <summary of changes in this batch>
@@ -88,7 +90,9 @@ gh pr comment <number> --body "<!-- mach6-progress -->
88
90
  **Commit:** \`<hash>\`
89
91
 
90
92
  ---
91
- *Progress tracked by mach6*"
93
+ *Progress tracked by mach6*
94
+ MACH6_EOF
95
+ gh pr comment <number> --body-file /tmp/gh-comment.md
92
96
  ```
93
97
 
94
98
  Update task: comment → completed.
@@ -14,6 +14,7 @@ argument-hint: "<pr-number> [code|errors|tests|completeness|simplify]"
14
14
  2. **HTML markers** — Use `<!-- mach6-review -->` and `<!-- mach6-assessment -->` as the first line of comment bodies.
15
15
  3. **No `#N` in comment bodies** — Use "finding 3", "item 3", "stage 2" etc. instead.
16
16
  4. **Task tracking** — Use the `tasks_update` tool to show progress.
17
+ 5. **Non-interactive `gh`** — Set `GH_PAGER=cat` and `GH_EDITOR=cat` before all `gh` commands to prevent interactive prompts from hanging the agent. Use `--body-file` instead of inline `--body` for all `gh pr comment`, `gh pr create`, and `gh issue create` calls to avoid shell interpretation of backticks.
17
18
 
18
19
  **Important: Do NOT fix any issues in this session. Fixes happen via `/skill:mach6-implement`.**
19
20
 
@@ -95,7 +96,8 @@ Update task: review → completed, post-review → in_progress.
95
96
  Compile all findings from all agents into a single structured comment:
96
97
 
97
98
  ```bash
98
- gh pr comment <pr-number> --body "<!-- mach6-review -->
99
+ cat > /tmp/gh-comment.md << 'MACH6_EOF'
100
+ <!-- mach6-review -->
99
101
  ## Code Review
100
102
 
101
103
  ### Critical
@@ -113,7 +115,9 @@ gh pr comment <pr-number> --body "<!-- mach6-review -->
113
115
  **Agents run:** <list of agents>
114
116
 
115
117
  ---
116
- *Reviewed by mach6*"
118
+ *Reviewed by mach6*
119
+ MACH6_EOF
120
+ gh pr comment <pr-number> --body-file /tmp/gh-comment.md
117
121
  ```
118
122
 
119
123
  Save the review comment URL:
@@ -152,7 +156,8 @@ Update task: assess → completed, post-assess → in_progress.
152
156
  ## Step 7: Post assessment
153
157
 
154
158
  ```bash
155
- gh pr comment <pr-number> --body "<!-- mach6-assessment -->
159
+ cat > /tmp/gh-comment.md << 'MACH6_EOF'
160
+ <!-- mach6-assessment -->
156
161
  ## Review Assessment
157
162
 
158
163
  <link to review comment>
@@ -168,7 +173,9 @@ gh pr comment <pr-number> --body "<!-- mach6-assessment -->
168
173
  <numbered list of what to fix, ordered by priority>
169
174
 
170
175
  ---
171
- *Assessment by mach6*"
176
+ *Assessment by mach6*
177
+ MACH6_EOF
178
+ gh pr comment <pr-number> --body-file /tmp/gh-comment.md
172
179
  ```
173
180
 
174
181
  Update task: post-assess → completed, summary → in_progress.
@@ -182,7 +189,10 @@ Present to the user:
182
189
 
183
190
  If any findings were classified as **deferred**, ask the user if they want to create issues for them:
184
191
  ```bash
185
- gh issue create --title "<title>" --body "<body referencing PR and finding>"
192
+ cat > /tmp/gh-body.md << 'MACH6_EOF'
193
+ <body referencing PR and finding>
194
+ MACH6_EOF
195
+ gh issue create --title "<title>" --body-file /tmp/gh-body.md
186
196
  ```
187
197
 
188
198
  Update task: summary → completed.