@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 +2 -0
- package/README.md +4 -0
- package/agents/explore.md +2 -2
- package/dist/core/tools/web-search-queue.d.ts +13 -0
- package/dist/core/tools/web-search-queue.d.ts.map +1 -0
- package/dist/core/tools/web-search-queue.js +83 -0
- package/dist/core/tools/web-search-queue.js.map +1 -0
- package/dist/core/tools/web.d.ts +8 -0
- package/dist/core/tools/web.d.ts.map +1 -1
- package/dist/core/tools/web.js +40 -13
- package/dist/core/tools/web.js.map +1 -1
- package/package.json +1 -1
- package/skills/mach6-implement/SKILL.md +3 -0
- package/skills/mach6-issue/SKILL.md +10 -3
- package/skills/mach6-plan/SKILL.md +11 -4
- package/skills/mach6-publish/SKILL.md +7 -1
- package/skills/mach6-push/SKILL.md +6 -2
- package/skills/mach6-review/SKILL.md +15 -5
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"]}
|
package/dist/core/tools/web.d.ts
CHANGED
|
@@ -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(/&/g, \"&\");\n\ttext = text.replace(/</g, \"<\");\n\ttext = text.replace(/>/g, \">\");\n\ttext = text.replace(/"/g, '\"');\n\ttext = text.replace(/'/g, \"'\");\n\ttext = text.replace(/ /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(/&/g, \"&\").replace(/</g, \"<\").replace(/>/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(/&/g, \"&\");\n\ttext = text.replace(/</g, \"<\");\n\ttext = text.replace(/>/g, \">\");\n\ttext = text.replace(/"/g, '\"');\n\ttext = text.replace(/'/g, \"'\");\n\ttext = text.replace(/ /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(/&/g, \"&\").replace(/</g, \"<\").replace(/>/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/dist/core/tools/web.js
CHANGED
|
@@ -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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAe,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,wDAAwD,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAyB,YAAY,EAAE,MAAM,eAAe,CAAC;AAEnG,8EAA8E;AAC9E,4CAA4C;AAC5C,8EAA8E;AAE9E,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAChC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AACnC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,aAAa;AAElD,MAAM,UAAU,GAAG,IAAI,GAAG,EAA0D,CAAC;AASrF,SAAS,eAAe,CAAC,IAAY,EAAU;IAC9C,IAAI,IAAI,GAAG,IAAI,CAAC;IAChB,iDAAiD;IACjD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,iFAAiF,EAAE,EAAE,CAAC,CAAC;IAC3G,qCAAqC;IACrC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,2DAA2D,EAAE,IAAI,CAAC,CAAC;IACvF,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;IAC/C,iCAAiC;IACjC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,gDAAgD,EAAE,SAAS,CAAC,CAAC;IACjF,qCAAqC;IACrC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,sCAAsC,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;QACvF,OAAO,KAAK,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC;IAAA,CAC5D,CAAC,CAAC;IACH,qBAAqB;IACrB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC7C,2BAA2B;IAC3B,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACpC,8BAA8B;IAC9B,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACnC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAClC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAClC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACpC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACnC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACpC,sBAAsB;IACtB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACpC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AAAA,CACnB;AAED,SAAS,YAAY,CAAC,IAAY,EAAU;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IAC/D,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACvG;AAED,MAAM,aAAa,GAAG;IACrB,YAAY,EAAE,2BAA2B;IACzC,MAAM,EAAE,4DAA4D;CACpE,CAAC;AAEF,6DAA6D;AAC7D,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AAElF,SAAS,aAAa,CAAC,QAAgB,EAAW;IACjD,IAAI,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,sBAAsB;IACtB,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC7D,IAAI,SAAS,EAAE,CAAC;QACf,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC,CAAC,aAAa;QAC5C,IAAI,KAAK,KAAK,GAAG,IAAI,MAAM,IAAI,EAAE,IAAI,MAAM,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC,CAAC,gBAAgB;QAChF,IAAI,KAAK,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC,CAAC,iBAAiB;QACnE,IAAI,KAAK,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC,CAAC,4BAA4B;IAC/E,CAAC;IACD,gDAAgD;IAChD,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;QAClC,IAAI,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IAChH,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,SAAS,aAAa,CAAC,QAAkB,EAA2D;IACnG,MAAM,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;IACtD,IAAI,EAAE,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACpC,OAAO,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAAA,CACzE;AAED,KAAK,UAAU,SAAS,CAAC,GAAW,EAA2D;IAC9F,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IAC3C,IAAI,aAAa,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,YAAY,YAAY,gCAAgC,CAAC,CAAC;IAC3E,CAAC;IAED,yDAAyD;IACzD,IAAI,UAAU,GAAG,GAAG,CAAC;IACrB,MAAM,YAAY,GAAG,EAAE,CAAC;IACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE;YACxC,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,aAAa;YACtB,QAAQ,EAAE,QAAQ;YAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;SAC7C,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACrD,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,oCAAoC,CAAC,CAAC;YAC9E,CAAC;YACD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAClD,iEAAiE;YACjE,IAAI,aAAa,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACzC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YAClE,CAAC;YACD,IAAI,WAAW,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;gBAC3C,OAAO;oBACN,IAAI,EAAE,4CAA4C,GAAG,mBAAmB,WAAW,CAAC,IAAI,sGAAsG;oBAC9L,WAAW,EAAE,YAAY;iBACzB,CAAC;YACH,CAAC;YACD,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC;YAC9B,SAAS;QACV,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,YAAY,GAAG,CAAC,CAAC;AAAA,CACxD;AAED,+EAA+E;AAE/E,SAAS,cAAc,CAAC,MAAc,EAAU;IAC/C,+EAA6E;IAC7E,4EAA4E;IAC5E,4DAA4D;IAC5D,iFAAiF;IACjF,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,MAAM,SAAS,GAAG,mBAAmB,CAAC;IACtC,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACvB,MAAM,QAAQ,GAAG,cAAc,CAAC;QAChC,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjD,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC;iBACzB,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC;iBACrB,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC;iBACrB,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC;iBACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;iBACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;iBACrB,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACzB,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;gBACpB,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1B,CAAC;QACF,CAAC;IACF,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,0GAAwG,CAAC;IACjH,CAAC;IAED,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAAA,CACxD;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,CAAC;IACnC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACvD,CAAC,CAAC;AAcH,KAAK,UAAU,gBAAgB,CAAC,KAAa,EAA2B;IACvE,MAAM,YAAY,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,uCAAuC,YAAY,EAAE,EAAE;QACnF,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACR,YAAY,EAAE,4BAA4B;YAC1C,MAAM,EAAE,WAAW;SACnB;QACD,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAC7C,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,OAAO,GAAmB,EAAE,CAAC;IAEnC,gEAA8D;IAC9D,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAC/D,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAC/C,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;QAC5F,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAE3F,IAAI,UAAU,EAAE,CAAC;YAChB,IAAI,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YACxB,0DAAwD;YACxD,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;YAC5C,IAAI,SAAS,EAAE,CAAC;gBACf,GAAG,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YACxC,CAAC;YACD,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3D,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnF,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC;gBAClB,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YACvC,CAAC;QACF,CAAC;IACF,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAChD,0FAAwF;QACxF,OAAO,CAAC,KAAK,CAAC,4FAA4F,CAAC,CAAC;IAC7G,CAAC;IACD,OAAO,OAAO,CAAC;AAAA,CACf;AAED,KAAK,UAAU,aAAa,CAAC,KAAa,EAAE,OAAe,EAA2B;IACrF,MAAM,YAAY,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,aAAa,YAAY,cAAc,EAAE;QAC/E,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;QACvC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAC7C,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA0E,CAAC;IAC9G,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE;KACxB,CAAC,CAAC,CAAC;AAAA,CACJ;AAED,KAAK,UAAU,WAAW,CAAC,KAAa,EAAE,MAAc,EAA2B;IAClF,MAAM,YAAY,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,oDAAoD,YAAY,EAAE,EAAE;QAChG,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACR,MAAM,EAAE,kBAAkB;YAC1B,sBAAsB,EAAE,MAAM;SAC9B;QACD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAC7C,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAElC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACzD,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,OAAO,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;KAC5B,CAAC,CAAC,CAAC;AAAA,CACJ;AAgBD,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAU,CAAC;AAE5D,SAAS,cAAc,GAAe;IACrC,iFAAiF;IACjF,MAAM,UAAU,GAAG;QAClB,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,EAAE,aAAa,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC;QAC3C,IAAI,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,aAAa,CAAC;QAC/C,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC;KACvC,CAAC;IACF,KAAK,MAAM,UAAU,IAAI,UAAU,EAAE,CAAC;QACrC,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACJ,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAe,CAAC;YACpE,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACd,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC7D,OAAO,CAAC,KAAK,CAAC,sCAAsC,UAAU,KAAK,GAAG,EAAE,CAAC,CAAC;YAC3E,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,EAAE,CAAC;AAAA,CACV;AAED,SAAS,eAAe,GAAoB;IAC3C,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;IACpC,6CAA6C;IAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC;IACjF,IAAI,OAAO,GAA+B,KAAK,CAAC;IAChD,IAAI,UAAU,EAAE,CAAC;QAChB,IAAK,cAAoC,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAChE,OAAO,GAAG,UAAwC,CAAC;QACpD,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,KAAK,CACZ,yCAAyC,UAAU,kCAAkC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChH,CAAC;QACH,CAAC;IACF,CAAC;IACD,OAAO;QACN,OAAO;QACP,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,EAAE,WAAW,IAAI,uBAAuB;QACrG,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,UAAU,CAAC,MAAM,EAAE,aAAa;KAC/E,CAAC;AAAA,CACF;AAED,KAAK,UAAU,aAAa,CAAC,KAAa,EAA2B;IACpE,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,QAAQ,MAAM,CAAC,OAAO,EAAE,CAAC;QACxB,KAAK,SAAS;YACb,OAAO,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,UAAW,CAAC,CAAC;QACjD,KAAK,OAAO;YACX,IAAI,CAAC,MAAM,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;YACvE,OAAO,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;QAC/C;YACC,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;AAAA,CACD;AAED,SAAS,gBAAgB,CACxB,IAAmC,EACnC,KAAoE,EAC3D;IACT,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/B,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,CACN,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC/C,GAAG;QACH,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,GAAG,CAAC,CAAC,CAChE,CAAC;AAAA,CACF;AAED,SAAS,kBAAkB,CAC1B,MAGC,EACD,OAAgC,EAChC,KAAoE,EACpE,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,MAAM,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC;QAC1C,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACnF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,SAAS,cAAc,CAAC,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,CAAC;QAChH,CAAC;IACF,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,6BAA6B,CAC5C,IAAY,EAC+D;IAC3E,OAAO;QACN,IAAI,EAAE,YAAY;QAClB,KAAK,EAAE,YAAY;QACnB,WAAW,EACV,8HAA8H;QAC/H,aAAa,EAAE,gCAAgC;QAC/C,UAAU,EAAE,eAAe;QAC3B,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,KAAK,EAAqB,EAAE;YACxD,IAAI,OAAuB,CAAC;YAC5B,IAAI,CAAC;gBACJ,OAAO,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;YACtC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnE,OAAO;oBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,KAAK,MAAM,GAAG,EAAE,EAAE,CAAC;oBACzE,OAAO,EAAE,SAAS;iBAClB,CAAC;YACH,CAAC;YACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1B,OAAO;oBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,KAAK,EAAE,EAAE,CAAC;oBACnE,OAAO,EAAE,SAAS;iBAClB,CAAC;YACH,CAAC;YACD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC3G,MAAM,MAAM,GAAG,uBAAuB,KAAK,OAAO,SAAS,EAAE,CAAC;YAC9D,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;gBACzC,OAAO,EAAE,SAAS;aAClB,CAAC;QAAA,CACF;QACD,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;YAChC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YAC5C,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;YAC7C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAa,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YACpF,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAqC;IACnF,OAAO,kBAAkB,CAAC,6BAA6B,CAAC,GAAG,CAAC,CAAC,CAAC;AAAA,CAC9D;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAG,6BAA6B,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AACpF,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAEhE,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IAClC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACrD,CAAC,CAAC;AASH,SAAS,eAAe,CACvB,IAAiC,EACjC,KAAoE,EAC3D;IACT,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC;AAAA,CACxH;AAED,SAAS,iBAAiB,CACzB,MAGC,EACD,OAAgC,EAChC,KAAoE,EACpE,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,MAAM,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC;QAC1C,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACnF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,SAAS,cAAc,CAAC,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,CAAC;QAChH,CAAC;IACF,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,IAAI,OAAO,EAAE,gBAAgB,IAAI,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;QACjE,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,OAAO,CAAC,gBAAgB;YAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,kBAAkB,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC;QACzG,IAAI,OAAO,CAAC,UAAU,EAAE,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,iBAAiB,CAAC,eAAe,CAAC,CAAC;QAClG,IAAI,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,4BAA4B,CAC3C,IAAY,EAC6D;IACzE,OAAO;QACN,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,wIAAwI,IAAI,CAAC,KAAK,CAAC,kBAAkB,GAAG,IAAI,CAAC,oCAAoC;QAC9N,aAAa,EAAE,0CAA0C;QACzD,UAAU,EAAE,cAAc;QAC1B,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,GAAG,EAAmB,EAAE;YACpD,eAAe;YACf,IAAI,MAAW,CAAC;YAChB,IAAI,CAAC;gBACJ,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACR,qDAAmD;gBACnD,OAAO;oBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,EAAE,EAAE,CAAC;oBACxD,OAAO,EAAE,SAAS;iBAClB,CAAC;YACH,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBACzC,OAAO;oBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;oBAC7E,OAAO,EAAE,SAAS;iBAClB,CAAC;YACH,CAAC;YAED,mDAAmD;YACnD,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACnC,IAAI,MAAM,EAAE,CAAC;gBACZ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,YAAY,EAAE,CAAC;oBAClD,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC;oBACzB,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,SAAS,gBAAgB,CAAC,CAAC,OAAO,EAAE,CAAC;oBACxF,OAAO;wBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wBACzC,OAAO,EAAE,SAAS;qBAClB,CAAC;gBACH,CAAC;gBACD,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACxB,CAAC;YAED,8CAA8C;YAC9C,IAAI,IAAqB,CAAC;YAC1B,IAAI,WAAmB,CAAC;YACxB,IAAI,CAAC;gBACJ,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;gBACpC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;gBACnB,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;YAClC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnE,OAAO;oBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,KAAK,GAAG,EAAE,EAAE,CAAC;oBACnE,OAAO,EAAE,SAAS;iBAClB,CAAC;YACH,CAAC;YAED,wCAAwC;YACxC,IAAI,IAAY,CAAC;YACjB,IAAI,KAAa,CAAC;YAClB,MAAM,OAAO,GAAwB,EAAE,CAAC;YACxC,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAE3C,IAAI,WAAW,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;gBAC7C,KAAK,GAAG,GAAG,CAAC;gBACZ,IAAI,GAAG,cAAc,CAAC,IAAc,CAAC,CAAC;YACvC,CAAC;iBAAM,IAAI,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBAC3F,MAAM,QAAQ,GAAG,IAAc,CAAC;gBAChC,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC;gBACtC,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;YAClC,CAAC;iBAAM,IACN,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC;gBAClC,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC;gBACxC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAChC,WAAW,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EACtC,CAAC;gBACF,KAAK,GAAG,GAAG,CAAC;gBACZ,IAAI,GAAG,IAAc,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACP,OAAO;oBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,6BAA6B,WAAW,EAAE,EAAE,CAAC;oBAC7E,OAAO,EAAE,SAAS;iBAClB,CAAC;YACH,CAAC;YAED,0DAA0D;YAC1D,IAAI,IAAI,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;gBACtC,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC,8BAA8B,IAAI,CAAC,KAAK,CAAC,kBAAkB,GAAG,IAAI,CAAC,KAAK,CAAC;gBACpH,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;YACjC,CAAC;YAED,MAAM,WAAW,GAAmB,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YAC7E,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAErE,MAAM,MAAM,GAAG,GAAG,KAAK,KAAK,GAAG,cAAc,SAAS,OAAO,IAAI,EAAE,CAAC;YACpE,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC;YAC/E,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;gBAC1B,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC;YACjC,CAAC;YAED,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC;gBACrD,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;aAC9D,CAAC;QAAA,CACF;QACD,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;YAChC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YAC3C,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;YAC7C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,MAAa,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YACnF,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAoC;IACjF,OAAO,kBAAkB,CAAC,4BAA4B,CAAC,GAAG,CAAC,CAAC,CAAC;AAAA,CAC7D;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,4BAA4B,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAClF,MAAM,CAAC,MAAM,YAAY,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,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(/&/g, \"&\");\n\ttext = text.replace(/</g, \"<\");\n\ttext = text.replace(/>/g, \">\");\n\ttext = text.replace(/"/g, '\"');\n\ttext = text.replace(/'/g, \"'\");\n\ttext = text.replace(/ /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(/&/g, \"&\").replace(/</g, \"<\").replace(/>/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(/&/g, \"&\");\n\ttext = text.replace(/</g, \"<\");\n\ttext = text.replace(/>/g, \">\");\n\ttext = text.replace(/"/g, '\"');\n\ttext = text.replace(/'/g, \"'\");\n\ttext = text.replace(/ /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(/&/g, \"&\").replace(/</g, \"<\").replace(/>/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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|