@kolisachint/hoocode-agent 0.4.152 → 0.4.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +3 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/embsearch/embsearch-progress.d.ts +33 -0
  6. package/dist/core/embsearch/embsearch-progress.d.ts.map +1 -0
  7. package/dist/core/embsearch/embsearch-progress.js +86 -0
  8. package/dist/core/embsearch/embsearch-progress.js.map +1 -0
  9. package/dist/core/embsearch/embsearch-service.d.ts +4 -0
  10. package/dist/core/embsearch/embsearch-service.d.ts.map +1 -1
  11. package/dist/core/embsearch/embsearch-service.js +6 -1
  12. package/dist/core/embsearch/embsearch-service.js.map +1 -1
  13. package/dist/core/settings-defaults.d.ts +6 -0
  14. package/dist/core/settings-defaults.d.ts.map +1 -1
  15. package/dist/core/settings-defaults.js +6 -0
  16. package/dist/core/settings-defaults.js.map +1 -1
  17. package/dist/core/settings-manager.d.ts +12 -1
  18. package/dist/core/settings-manager.d.ts.map +1 -1
  19. package/dist/core/settings-manager.js +44 -0
  20. package/dist/core/settings-manager.js.map +1 -1
  21. package/dist/core/settings-types.d.ts +8 -0
  22. package/dist/core/settings-types.d.ts.map +1 -1
  23. package/dist/core/settings-types.js.map +1 -1
  24. package/dist/core/startup-progress.d.ts +48 -0
  25. package/dist/core/startup-progress.d.ts.map +1 -0
  26. package/dist/core/startup-progress.js +50 -0
  27. package/dist/core/startup-progress.js.map +1 -0
  28. package/dist/core/tools/webfetch.d.ts +2 -0
  29. package/dist/core/tools/webfetch.d.ts.map +1 -1
  30. package/dist/core/tools/webfetch.js +5 -4
  31. package/dist/core/tools/webfetch.js.map +1 -1
  32. package/dist/core/tools/websearch.d.ts +2 -0
  33. package/dist/core/tools/websearch.d.ts.map +1 -1
  34. package/dist/core/tools/websearch.js +5 -4
  35. package/dist/core/tools/websearch.js.map +1 -1
  36. package/dist/core/tools/webtools-shared.d.ts +9 -0
  37. package/dist/core/tools/webtools-shared.d.ts.map +1 -1
  38. package/dist/core/tools/webtools-shared.js +28 -0
  39. package/dist/core/tools/webtools-shared.js.map +1 -1
  40. package/dist/main.d.ts.map +1 -1
  41. package/dist/main.js +4 -48
  42. package/dist/main.js.map +1 -1
  43. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  44. package/dist/modes/interactive/components/footer.js +44 -0
  45. package/dist/modes/interactive/components/footer.js.map +1 -1
  46. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  47. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/settings-selector.js +27 -1
  49. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  50. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  51. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  52. package/dist/modes/interactive/interactive-mode.js +65 -5
  53. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  54. package/dist/modes/interactive/voice/voice-controller.d.ts +18 -0
  55. package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -1
  56. package/dist/modes/interactive/voice/voice-controller.js +16 -10
  57. package/dist/modes/interactive/voice/voice-controller.js.map +1 -1
  58. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  59. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  60. package/examples/extensions/sandbox/package.json +1 -1
  61. package/examples/extensions/with-deps/package.json +1 -1
  62. package/package.json +4 -4
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Process-wide store for transient startup progress: first-run downloads of the
3
+ * external tool binaries (fd, rg, the semantic-index engine) and the semantic
4
+ * index build itself.
5
+ *
6
+ * Modeled on `taskStore` — a singleton the footer reads directly and re-renders
7
+ * on change — but deliberately separate from it: this is short-lived startup
8
+ * status, not agent work, so it must never render as a task-panel plan row.
9
+ * Entries are keyed so several concurrent downloads each own a stable footer
10
+ * line; a caller removes its key when the work settles and the line disappears.
11
+ */
12
+ class StartupProgressStore {
13
+ // Map preserves insertion order, so lines stay in the order work started
14
+ // (fd, rg, then the index) instead of jumping as byte counts update.
15
+ entries = new Map();
16
+ listeners = new Set();
17
+ /** Insert or replace the entry for its key. */
18
+ set(entry) {
19
+ this.entries.set(entry.key, entry);
20
+ this.emit();
21
+ }
22
+ /** Drop a key's line once its work settled (download done, index ready/skipped). */
23
+ remove(key) {
24
+ if (this.entries.delete(key))
25
+ this.emit();
26
+ }
27
+ /** Wipe everything — called when the first user turn starts (transient startup status). */
28
+ clear() {
29
+ if (this.entries.size === 0)
30
+ return;
31
+ this.entries.clear();
32
+ this.emit();
33
+ }
34
+ list() {
35
+ return [...this.entries.values()];
36
+ }
37
+ subscribe(listener) {
38
+ this.listeners.add(listener);
39
+ return () => {
40
+ this.listeners.delete(listener);
41
+ };
42
+ }
43
+ emit() {
44
+ for (const listener of this.listeners)
45
+ listener();
46
+ }
47
+ }
48
+ /** Shared, process-wide startup-progress store. */
49
+ export const startupProgress = new StartupProgressStore();
50
+ //# sourceMappingURL=startup-progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup-progress.js","sourceRoot":"","sources":["../../src/core/startup-progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,MAAM,oBAAoB;IACzB,yEAAyE;IACzE,qEAAqE;IACpD,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IAC7C,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;IAEjD,+CAA+C;IAC/C,GAAG,CAAC,KAAsB,EAAQ;QACjC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,oFAAoF;IACpF,MAAM,CAAC,GAAW,EAAQ;QACzB,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CAC1C;IAED,6FAA2F;IAC3F,KAAK,GAAS;QACb,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QACpC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,IAAI,GAA+B;QAClC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAAA,CAClC;IAED,SAAS,CAAC,QAAkB,EAAc;QACzC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC;YACZ,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAA,CAChC,CAAC;IAAA,CACF;IAEO,IAAI,GAAS;QACpB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS;YAAE,QAAQ,EAAE,CAAC;IAAA,CAClD;CACD;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,oBAAoB,EAAE,CAAC","sourcesContent":["/**\n * Process-wide store for transient startup progress: first-run downloads of the\n * external tool binaries (fd, rg, the semantic-index engine) and the semantic\n * index build itself.\n *\n * Modeled on `taskStore` — a singleton the footer reads directly and re-renders\n * on change — but deliberately separate from it: this is short-lived startup\n * status, not agent work, so it must never render as a task-panel plan row.\n * Entries are keyed so several concurrent downloads each own a stable footer\n * line; a caller removes its key when the work settles and the line disappears.\n */\n\nexport type StartupProgress =\n\t| { readonly key: string; kind: \"download\"; label: string; receivedBytes: number; totalBytes: number | null }\n\t| { readonly key: string; kind: \"work\"; label: string; done: number; total: number; unit: string }\n\t| { readonly key: string; kind: \"error\"; label: string; message: string };\n\ntype Listener = () => void;\n\nclass StartupProgressStore {\n\t// Map preserves insertion order, so lines stay in the order work started\n\t// (fd, rg, then the index) instead of jumping as byte counts update.\n\tprivate readonly entries = new Map<string, StartupProgress>();\n\tprivate readonly listeners = new Set<Listener>();\n\n\t/** Insert or replace the entry for its key. */\n\tset(entry: StartupProgress): void {\n\t\tthis.entries.set(entry.key, entry);\n\t\tthis.emit();\n\t}\n\n\t/** Drop a key's line once its work settled (download done, index ready/skipped). */\n\tremove(key: string): void {\n\t\tif (this.entries.delete(key)) this.emit();\n\t}\n\n\t/** Wipe everything — called when the first user turn starts (transient startup status). */\n\tclear(): void {\n\t\tif (this.entries.size === 0) return;\n\t\tthis.entries.clear();\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly StartupProgress[] {\n\t\treturn [...this.entries.values()];\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) listener();\n\t}\n}\n\n/** Shared, process-wide startup-progress store. */\nexport const startupProgress = new StartupProgressStore();\n"]}
@@ -20,6 +20,8 @@ export interface WebFetchToolDetails {
20
20
  export interface WebFetchToolOptions extends WebtoolsTLSConfig {
21
21
  /** Override the result cache (mainly for tests). */
22
22
  cache?: WebToolsCache<WebFetchResult>;
23
+ /** Effective per-request timeout (seconds); falls back to env/default when unset. */
24
+ timeoutSecs?: number;
23
25
  }
24
26
  export declare function createWebFetchToolDefinition(cwd: string, options?: WebFetchToolOptions): ToolDefinition<typeof webfetchSchema, WebFetchToolDetails | undefined>;
25
27
  export declare function createWebFetchTool(cwd: string, options?: WebFetchToolOptions): AgentTool<typeof webfetchSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"webfetch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/webfetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAKN,KAAK,cAAc,EACnB,aAAa,EACb,KAAK,iBAAiB,EACtB,MAAM,sBAAsB,CAAC;AAO9B,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,QAAA,MAAM,cAAc;;;;EAalB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC7D,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;CACtC;AAiCD,wBAAgB,4BAA4B,CAC3C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,mBAAmB,GAC3B,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CAyDxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC,OAAO,cAAc,CAAC,CAE/G","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\tblockedHostForUrl,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\tWEBTOOLS_DEFAULT_TIMEOUT_SECS,\n\ttype WebFetchResult,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_TOKENS = 4000;\n// Hard ceiling so a single fetch can never flood the context window, regardless\n// of what the model requests. The binary still applies its own soft cap.\nconst MAX_TOKENS_CAP = 25000;\n\n/** Clamp a requested token budget into `(0, MAX_TOKENS_CAP]`, defaulting when unset. */\nexport function clampMaxTokens(requested?: number): number {\n\tif (!requested || requested <= 0) return DEFAULT_MAX_TOKENS;\n\treturn Math.min(requested, MAX_TOKENS_CAP);\n}\n\nconst webfetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch (http or https)\" }),\n\tmaxTokens: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Soft cap on returned output size in estimated tokens (default: ${DEFAULT_MAX_TOKENS}, max: ${MAX_TOKENS_CAP})`,\n\t\t}),\n\t),\n\toutput: Type.Optional(\n\t\tType.Union([Type.Literal(\"text\"), Type.Literal(\"markdown\")], {\n\t\t\tdescription:\n\t\t\t\t\"Output format: 'text' (default, most token-efficient, links as [N] with a trailing reference block) or 'markdown' (inline links).\",\n\t\t}),\n\t),\n});\n\nexport type WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\tcontentType?: string;\n\tmedia?: string;\n}\n\nexport interface WebFetchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebFetchResult>;\n}\n\nfunction formatWebfetchCall(args: { url?: string; output?: string } | undefined): string {\n\tconst url = str(args?.url);\n\tconst urlDisplay = url === null ? invalidArgText(appTheme) : url ? url : appTheme.fg(\"toolOutput\", \"...\");\n\tconst format = args?.output === \"markdown\" ? appTheme.fg(\"muted\", \" (markdown)\") : \"\";\n\treturn appTheme.fg(\"toolTitle\", appTheme.bold(\"webfetch \")) + appTheme.fg(\"accent\", urlDisplay) + format;\n}\n\nfunction formatWebfetchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebFetchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst tokenEstimate = result.details?.tokenEstimate;\n\tif (tokenEstimate !== undefined) {\n\t\ttext += `\\n${appTheme.fg(\"muted\", `~${tokenEstimate} tokens`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\tcwd: string,\n\toptions?: WebFetchToolOptions,\n): ToolDefinition<typeof webfetchSchema, WebFetchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebFetchResult>();\n\t// Resolve CA/insecure plumbing once (settings overrides, else env) and thread\n\t// it into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\treturn {\n\t\tname: \"webfetch\",\n\t\tlabel: \"webfetch\",\n\t\tdescription:\n\t\t\t\"Fetch a web page (or JSON/text resource) and return token-efficient, reference-style content. HTML is extracted to clean text; links become inline [N] markers with full URLs in a trailing reference block. Returns title, final URL (after redirects), and an estimated token count. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Fetch a URL and return clean, token-efficient page content\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use webfetch to read a known URL instead of bash curl/wget; it returns clean extracted text with reference-style [N] links, not raw HTML.\",\n\t\t],\n\t\tparameters: webfetchSchema,\n\t\tasync execute(_toolCallId, { url, maxTokens, output }: WebFetchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\t// Policy gate (.webtoolsignore). SSRF/private-address blocking lives in\n\t\t\t// the binary; this is host-level allow/deny policy only.\n\t\t\tconst blockedHost = blockedHostForUrl(cwd, url);\n\t\t\tif (blockedHost) {\n\t\t\t\tthrow new Error(`Blocked by .webtoolsignore policy: ${blockedHost}`);\n\t\t\t}\n\n\t\t\tconst effectiveMaxTokens = clampMaxTokens(maxTokens);\n\t\t\tconst format = output ?? \"text\";\n\t\t\tconst cacheKey = `${format}:${effectiveMaxTokens}:${url}`;\n\n\t\t\tconst args = [\"--url\", url, \"--max-tokens\", String(effectiveMaxTokens), \"--output\", format];\n\t\t\tconst result = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebFetchResult>(\"fetch\", args, cwd, sig, WEBTOOLS_DEFAULT_TIMEOUT_SECS, tlsConfig),\n\t\t\t);\n\n\t\t\tconst header = result.title ? `${result.title}\\n${result.final_url}\\n\\n` : `${result.final_url}\\n\\n`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + result.content }],\n\t\t\t\tdetails: {\n\t\t\t\t\tfinalUrl: result.final_url,\n\t\t\t\t\ttitle: result.title,\n\t\t\t\t\ttokenEstimate: result.token_estimate,\n\t\t\t\t\tcontentType: result.content_type,\n\t\t\t\t\tmedia: result.media,\n\t\t\t\t},\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(formatWebfetchCall(args));\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(formatWebfetchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string, options?: WebFetchToolOptions): AgentTool<typeof webfetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"webfetch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/webfetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAKN,KAAK,cAAc,EACnB,aAAa,EACb,KAAK,iBAAiB,EACtB,MAAM,sBAAsB,CAAC;AAO9B,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,QAAA,MAAM,cAAc;;;;EAalB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC7D,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACtC,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAiCD,wBAAgB,4BAA4B,CAC3C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,mBAAmB,GAC3B,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CA0DxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC,OAAO,cAAc,CAAC,CAE/G","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\tblockedHostForUrl,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebFetchResult,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_TOKENS = 4000;\n// Hard ceiling so a single fetch can never flood the context window, regardless\n// of what the model requests. The binary still applies its own soft cap.\nconst MAX_TOKENS_CAP = 25000;\n\n/** Clamp a requested token budget into `(0, MAX_TOKENS_CAP]`, defaulting when unset. */\nexport function clampMaxTokens(requested?: number): number {\n\tif (!requested || requested <= 0) return DEFAULT_MAX_TOKENS;\n\treturn Math.min(requested, MAX_TOKENS_CAP);\n}\n\nconst webfetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch (http or https)\" }),\n\tmaxTokens: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Soft cap on returned output size in estimated tokens (default: ${DEFAULT_MAX_TOKENS}, max: ${MAX_TOKENS_CAP})`,\n\t\t}),\n\t),\n\toutput: Type.Optional(\n\t\tType.Union([Type.Literal(\"text\"), Type.Literal(\"markdown\")], {\n\t\t\tdescription:\n\t\t\t\t\"Output format: 'text' (default, most token-efficient, links as [N] with a trailing reference block) or 'markdown' (inline links).\",\n\t\t}),\n\t),\n});\n\nexport type WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\tcontentType?: string;\n\tmedia?: string;\n}\n\nexport interface WebFetchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebFetchResult>;\n\t/** Effective per-request timeout (seconds); falls back to env/default when unset. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatWebfetchCall(args: { url?: string; output?: string } | undefined): string {\n\tconst url = str(args?.url);\n\tconst urlDisplay = url === null ? invalidArgText(appTheme) : url ? url : appTheme.fg(\"toolOutput\", \"...\");\n\tconst format = args?.output === \"markdown\" ? appTheme.fg(\"muted\", \" (markdown)\") : \"\";\n\treturn appTheme.fg(\"toolTitle\", appTheme.bold(\"webfetch \")) + appTheme.fg(\"accent\", urlDisplay) + format;\n}\n\nfunction formatWebfetchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebFetchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst tokenEstimate = result.details?.tokenEstimate;\n\tif (tokenEstimate !== undefined) {\n\t\ttext += `\\n${appTheme.fg(\"muted\", `~${tokenEstimate} tokens`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\tcwd: string,\n\toptions?: WebFetchToolOptions,\n): ToolDefinition<typeof webfetchSchema, WebFetchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebFetchResult>();\n\t// Resolve CA/insecure plumbing and the request timeout once (settings\n\t// overrides, else env) and thread them into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\tconst timeoutSecs = resolveWebtoolsTimeoutSecs(options?.timeoutSecs);\n\treturn {\n\t\tname: \"webfetch\",\n\t\tlabel: \"webfetch\",\n\t\tdescription:\n\t\t\t\"Fetch a web page (or JSON/text resource) and return token-efficient, reference-style content. HTML is extracted to clean text; links become inline [N] markers with full URLs in a trailing reference block. Returns title, final URL (after redirects), and an estimated token count. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Fetch a URL and return clean, token-efficient page content\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use webfetch to read a known URL instead of bash curl/wget; it returns clean extracted text with reference-style [N] links, not raw HTML.\",\n\t\t],\n\t\tparameters: webfetchSchema,\n\t\tasync execute(_toolCallId, { url, maxTokens, output }: WebFetchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\t// Policy gate (.webtoolsignore). SSRF/private-address blocking lives in\n\t\t\t// the binary; this is host-level allow/deny policy only.\n\t\t\tconst blockedHost = blockedHostForUrl(cwd, url);\n\t\t\tif (blockedHost) {\n\t\t\t\tthrow new Error(`Blocked by .webtoolsignore policy: ${blockedHost}`);\n\t\t\t}\n\n\t\t\tconst effectiveMaxTokens = clampMaxTokens(maxTokens);\n\t\t\tconst format = output ?? \"text\";\n\t\t\tconst cacheKey = `${format}:${effectiveMaxTokens}:${url}`;\n\n\t\t\tconst args = [\"--url\", url, \"--max-tokens\", String(effectiveMaxTokens), \"--output\", format];\n\t\t\tconst result = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebFetchResult>(\"fetch\", args, cwd, sig, timeoutSecs, tlsConfig),\n\t\t\t);\n\n\t\t\tconst header = result.title ? `${result.title}\\n${result.final_url}\\n\\n` : `${result.final_url}\\n\\n`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + result.content }],\n\t\t\t\tdetails: {\n\t\t\t\t\tfinalUrl: result.final_url,\n\t\t\t\t\ttitle: result.title,\n\t\t\t\t\ttokenEstimate: result.token_estimate,\n\t\t\t\t\tcontentType: result.content_type,\n\t\t\t\t\tmedia: result.media,\n\t\t\t\t},\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(formatWebfetchCall(args));\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(formatWebfetchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string, options?: WebFetchToolOptions): AgentTool<typeof webfetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd, options));\n}\n"]}
@@ -4,7 +4,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
4
4
  import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
5
5
  import { getTextOutput, invalidArgText, str } from "./render-utils.js";
6
6
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
7
- import { blockedHostForUrl, resolveWebtoolsTLSConfig, runWebtools, WEBTOOLS_DEFAULT_TIMEOUT_SECS, WebToolsCache, } from "./webtools-shared.js";
7
+ import { blockedHostForUrl, resolveWebtoolsTimeoutSecs, resolveWebtoolsTLSConfig, runWebtools, WebToolsCache, } from "./webtools-shared.js";
8
8
  const DEFAULT_MAX_TOKENS = 4000;
9
9
  // Hard ceiling so a single fetch can never flood the context window, regardless
10
10
  // of what the model requests. The binary still applies its own soft cap.
@@ -51,9 +51,10 @@ function formatWebfetchResult(result, options, showImages) {
51
51
  }
52
52
  export function createWebFetchToolDefinition(cwd, options) {
53
53
  const cache = options?.cache ?? new WebToolsCache();
54
- // Resolve CA/insecure plumbing once (settings overrides, else env) and thread
55
- // it into every spawn; not hardcoded.
54
+ // Resolve CA/insecure plumbing and the request timeout once (settings
55
+ // overrides, else env) and thread them into every spawn; not hardcoded.
56
56
  const tlsConfig = resolveWebtoolsTLSConfig(options);
57
+ const timeoutSecs = resolveWebtoolsTimeoutSecs(options?.timeoutSecs);
57
58
  return {
58
59
  name: "webfetch",
59
60
  label: "webfetch",
@@ -76,7 +77,7 @@ export function createWebFetchToolDefinition(cwd, options) {
76
77
  const format = output ?? "text";
77
78
  const cacheKey = `${format}:${effectiveMaxTokens}:${url}`;
78
79
  const args = ["--url", url, "--max-tokens", String(effectiveMaxTokens), "--output", format];
79
- const result = await cache.getOrCompute(cacheKey, signal, (sig) => runWebtools("fetch", args, cwd, sig, WEBTOOLS_DEFAULT_TIMEOUT_SECS, tlsConfig));
80
+ const result = await cache.getOrCompute(cacheKey, signal, (sig) => runWebtools("fetch", args, cwd, sig, timeoutSecs, tlsConfig));
80
81
  const header = result.title ? `${result.title}\n${result.final_url}\n\n` : `${result.final_url}\n\n`;
81
82
  return {
82
83
  content: [{ type: "text", text: header + result.content }],
@@ -1 +1 @@
1
- {"version":3,"file":"webfetch.js","sourceRoot":"","sources":["../../../src/core/tools/webfetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wDAAwD,CAAC;AACjF,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAE3E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EACN,iBAAiB,EACjB,wBAAwB,EACxB,WAAW,EACX,6BAA6B,EAE7B,aAAa,GAEb,MAAM,sBAAsB,CAAC;AAE9B,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAChC,gFAAgF;AAChF,yEAAyE;AACzE,MAAM,cAAc,GAAG,KAAK,CAAC;AAE7B,wFAAwF;AACxF,MAAM,UAAU,cAAc,CAAC,SAAkB,EAAU;IAC1D,IAAI,CAAC,SAAS,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,kBAAkB,CAAC;IAC5D,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;AAAA,CAC3C;AAED,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IAClC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;IACrE,SAAS,EAAE,IAAI,CAAC,QAAQ,CACvB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EAAE,kEAAkE,kBAAkB,UAAU,cAAc,GAAG;KAC5H,CAAC,CACF;IACD,MAAM,EAAE,IAAI,CAAC,QAAQ,CACpB,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE;QAC5D,WAAW,EACV,mIAAmI;KACpI,CAAC,CACF;CACD,CAAC,CAAC;AAiBH,SAAS,kBAAkB,CAAC,IAAmD,EAAU;IACxF,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,MAAM,UAAU,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IAC1G,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACtF,OAAO,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,GAAG,MAAM,CAAC;AAAA,CACzG;AAED,SAAS,oBAAoB,CAC5B,MAA0F,EAC1F,OAAgC,EAChC,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAa,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/D,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,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,SAAS,cAAc,CAAC,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,CAAC;QACnH,CAAC;IACF,CAAC;IACD,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC;IACpD,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,aAAa,SAAS,CAAC,EAAE,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,4BAA4B,CAC3C,GAAW,EACX,OAA6B,EAC4C;IACzE,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,IAAI,aAAa,EAAkB,CAAC;IACpE,8EAA8E;IAC9E,sCAAsC;IACtC,MAAM,SAAS,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;IACpD,OAAO;QACN,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,UAAU;QACjB,WAAW,EACV,wUAAwU;QACzU,aAAa,EAAE,4DAA4D;QAC3E,gBAAgB,EAAE;YACjB,2IAA2I;SAC3I;QACD,UAAU,EAAE,cAAc;QAC1B,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAqB,EAAE,MAAoB,EAAE;YAC/F,IAAI,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAE1D,wEAAwE;YACxE,yDAAyD;YACzD,MAAM,WAAW,GAAG,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAChD,IAAI,WAAW,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,sCAAsC,WAAW,EAAE,CAAC,CAAC;YACtE,CAAC;YAED,MAAM,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,MAAM,GAAG,MAAM,IAAI,MAAM,CAAC;YAChC,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,kBAAkB,IAAI,GAAG,EAAE,CAAC;YAE1D,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,CAAC,kBAAkB,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;YAC5F,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACjE,WAAW,CAAiB,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,6BAA6B,EAAE,SAAS,CAAC,CAC9F,CAAC;YAEF,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,MAAM,CAAC;YACrG,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnE,OAAO,EAAE;oBACR,QAAQ,EAAE,MAAM,CAAC,SAAS;oBAC1B,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,aAAa,EAAE,MAAM,CAAC,cAAc;oBACpC,WAAW,EAAE,MAAM,CAAC,YAAY;oBAChC,KAAK,EAAE,MAAM,CAAC,KAAK;iBACnB;aACD,CAAC;QAAA,CACF;QACD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;YACjC,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,IAAI,CAAC,CAAC,CAAC;YACvC,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;YAC9C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,MAAa,EAAE,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YAC/E,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,OAA6B,EAAoC;IAChH,OAAO,kBAAkB,CAAC,4BAA4B,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CACtE","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\tblockedHostForUrl,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\tWEBTOOLS_DEFAULT_TIMEOUT_SECS,\n\ttype WebFetchResult,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_TOKENS = 4000;\n// Hard ceiling so a single fetch can never flood the context window, regardless\n// of what the model requests. The binary still applies its own soft cap.\nconst MAX_TOKENS_CAP = 25000;\n\n/** Clamp a requested token budget into `(0, MAX_TOKENS_CAP]`, defaulting when unset. */\nexport function clampMaxTokens(requested?: number): number {\n\tif (!requested || requested <= 0) return DEFAULT_MAX_TOKENS;\n\treturn Math.min(requested, MAX_TOKENS_CAP);\n}\n\nconst webfetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch (http or https)\" }),\n\tmaxTokens: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Soft cap on returned output size in estimated tokens (default: ${DEFAULT_MAX_TOKENS}, max: ${MAX_TOKENS_CAP})`,\n\t\t}),\n\t),\n\toutput: Type.Optional(\n\t\tType.Union([Type.Literal(\"text\"), Type.Literal(\"markdown\")], {\n\t\t\tdescription:\n\t\t\t\t\"Output format: 'text' (default, most token-efficient, links as [N] with a trailing reference block) or 'markdown' (inline links).\",\n\t\t}),\n\t),\n});\n\nexport type WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\tcontentType?: string;\n\tmedia?: string;\n}\n\nexport interface WebFetchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebFetchResult>;\n}\n\nfunction formatWebfetchCall(args: { url?: string; output?: string } | undefined): string {\n\tconst url = str(args?.url);\n\tconst urlDisplay = url === null ? invalidArgText(appTheme) : url ? url : appTheme.fg(\"toolOutput\", \"...\");\n\tconst format = args?.output === \"markdown\" ? appTheme.fg(\"muted\", \" (markdown)\") : \"\";\n\treturn appTheme.fg(\"toolTitle\", appTheme.bold(\"webfetch \")) + appTheme.fg(\"accent\", urlDisplay) + format;\n}\n\nfunction formatWebfetchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebFetchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst tokenEstimate = result.details?.tokenEstimate;\n\tif (tokenEstimate !== undefined) {\n\t\ttext += `\\n${appTheme.fg(\"muted\", `~${tokenEstimate} tokens`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\tcwd: string,\n\toptions?: WebFetchToolOptions,\n): ToolDefinition<typeof webfetchSchema, WebFetchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebFetchResult>();\n\t// Resolve CA/insecure plumbing once (settings overrides, else env) and thread\n\t// it into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\treturn {\n\t\tname: \"webfetch\",\n\t\tlabel: \"webfetch\",\n\t\tdescription:\n\t\t\t\"Fetch a web page (or JSON/text resource) and return token-efficient, reference-style content. HTML is extracted to clean text; links become inline [N] markers with full URLs in a trailing reference block. Returns title, final URL (after redirects), and an estimated token count. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Fetch a URL and return clean, token-efficient page content\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use webfetch to read a known URL instead of bash curl/wget; it returns clean extracted text with reference-style [N] links, not raw HTML.\",\n\t\t],\n\t\tparameters: webfetchSchema,\n\t\tasync execute(_toolCallId, { url, maxTokens, output }: WebFetchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\t// Policy gate (.webtoolsignore). SSRF/private-address blocking lives in\n\t\t\t// the binary; this is host-level allow/deny policy only.\n\t\t\tconst blockedHost = blockedHostForUrl(cwd, url);\n\t\t\tif (blockedHost) {\n\t\t\t\tthrow new Error(`Blocked by .webtoolsignore policy: ${blockedHost}`);\n\t\t\t}\n\n\t\t\tconst effectiveMaxTokens = clampMaxTokens(maxTokens);\n\t\t\tconst format = output ?? \"text\";\n\t\t\tconst cacheKey = `${format}:${effectiveMaxTokens}:${url}`;\n\n\t\t\tconst args = [\"--url\", url, \"--max-tokens\", String(effectiveMaxTokens), \"--output\", format];\n\t\t\tconst result = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebFetchResult>(\"fetch\", args, cwd, sig, WEBTOOLS_DEFAULT_TIMEOUT_SECS, tlsConfig),\n\t\t\t);\n\n\t\t\tconst header = result.title ? `${result.title}\\n${result.final_url}\\n\\n` : `${result.final_url}\\n\\n`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + result.content }],\n\t\t\t\tdetails: {\n\t\t\t\t\tfinalUrl: result.final_url,\n\t\t\t\t\ttitle: result.title,\n\t\t\t\t\ttokenEstimate: result.token_estimate,\n\t\t\t\t\tcontentType: result.content_type,\n\t\t\t\t\tmedia: result.media,\n\t\t\t\t},\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(formatWebfetchCall(args));\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(formatWebfetchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string, options?: WebFetchToolOptions): AgentTool<typeof webfetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"webfetch.js","sourceRoot":"","sources":["../../../src/core/tools/webfetch.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\tblockedHostForUrl,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebFetchResult,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_TOKENS = 4000;\n// Hard ceiling so a single fetch can never flood the context window, regardless\n// of what the model requests. The binary still applies its own soft cap.\nconst MAX_TOKENS_CAP = 25000;\n\n/** Clamp a requested token budget into `(0, MAX_TOKENS_CAP]`, defaulting when unset. */\nexport function clampMaxTokens(requested?: number): number {\n\tif (!requested || requested <= 0) return DEFAULT_MAX_TOKENS;\n\treturn Math.min(requested, MAX_TOKENS_CAP);\n}\n\nconst webfetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch (http or https)\" }),\n\tmaxTokens: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Soft cap on returned output size in estimated tokens (default: ${DEFAULT_MAX_TOKENS}, max: ${MAX_TOKENS_CAP})`,\n\t\t}),\n\t),\n\toutput: Type.Optional(\n\t\tType.Union([Type.Literal(\"text\"), Type.Literal(\"markdown\")], {\n\t\t\tdescription:\n\t\t\t\t\"Output format: 'text' (default, most token-efficient, links as [N] with a trailing reference block) or 'markdown' (inline links).\",\n\t\t}),\n\t),\n});\n\nexport type WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\tcontentType?: string;\n\tmedia?: string;\n}\n\nexport interface WebFetchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebFetchResult>;\n\t/** Effective per-request timeout (seconds); falls back to env/default when unset. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatWebfetchCall(args: { url?: string; output?: string } | undefined): string {\n\tconst url = str(args?.url);\n\tconst urlDisplay = url === null ? invalidArgText(appTheme) : url ? url : appTheme.fg(\"toolOutput\", \"...\");\n\tconst format = args?.output === \"markdown\" ? appTheme.fg(\"muted\", \" (markdown)\") : \"\";\n\treturn appTheme.fg(\"toolTitle\", appTheme.bold(\"webfetch \")) + appTheme.fg(\"accent\", urlDisplay) + format;\n}\n\nfunction formatWebfetchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebFetchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst tokenEstimate = result.details?.tokenEstimate;\n\tif (tokenEstimate !== undefined) {\n\t\ttext += `\\n${appTheme.fg(\"muted\", `~${tokenEstimate} tokens`)}`;\n\t}\n\treturn text;\n}\n\nexport function createWebFetchToolDefinition(\n\tcwd: string,\n\toptions?: WebFetchToolOptions,\n): ToolDefinition<typeof webfetchSchema, WebFetchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebFetchResult>();\n\t// Resolve CA/insecure plumbing and the request timeout once (settings\n\t// overrides, else env) and thread them into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\tconst timeoutSecs = resolveWebtoolsTimeoutSecs(options?.timeoutSecs);\n\treturn {\n\t\tname: \"webfetch\",\n\t\tlabel: \"webfetch\",\n\t\tdescription:\n\t\t\t\"Fetch a web page (or JSON/text resource) and return token-efficient, reference-style content. HTML is extracted to clean text; links become inline [N] markers with full URLs in a trailing reference block. Returns title, final URL (after redirects), and an estimated token count. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Fetch a URL and return clean, token-efficient page content\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use webfetch to read a known URL instead of bash curl/wget; it returns clean extracted text with reference-style [N] links, not raw HTML.\",\n\t\t],\n\t\tparameters: webfetchSchema,\n\t\tasync execute(_toolCallId, { url, maxTokens, output }: WebFetchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\t// Policy gate (.webtoolsignore). SSRF/private-address blocking lives in\n\t\t\t// the binary; this is host-level allow/deny policy only.\n\t\t\tconst blockedHost = blockedHostForUrl(cwd, url);\n\t\t\tif (blockedHost) {\n\t\t\t\tthrow new Error(`Blocked by .webtoolsignore policy: ${blockedHost}`);\n\t\t\t}\n\n\t\t\tconst effectiveMaxTokens = clampMaxTokens(maxTokens);\n\t\t\tconst format = output ?? \"text\";\n\t\t\tconst cacheKey = `${format}:${effectiveMaxTokens}:${url}`;\n\n\t\t\tconst args = [\"--url\", url, \"--max-tokens\", String(effectiveMaxTokens), \"--output\", format];\n\t\t\tconst result = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebFetchResult>(\"fetch\", args, cwd, sig, timeoutSecs, tlsConfig),\n\t\t\t);\n\n\t\t\tconst header = result.title ? `${result.title}\\n${result.final_url}\\n\\n` : `${result.final_url}\\n\\n`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + result.content }],\n\t\t\t\tdetails: {\n\t\t\t\t\tfinalUrl: result.final_url,\n\t\t\t\t\ttitle: result.title,\n\t\t\t\t\ttokenEstimate: result.token_estimate,\n\t\t\t\t\tcontentType: result.content_type,\n\t\t\t\t\tmedia: result.media,\n\t\t\t\t},\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(formatWebfetchCall(args));\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(formatWebfetchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string, options?: WebFetchToolOptions): AgentTool<typeof webfetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd, options));\n}\n"]}
@@ -16,6 +16,8 @@ export interface WebSearchToolDetails {
16
16
  export interface WebSearchToolOptions extends WebtoolsTLSConfig {
17
17
  /** Override the result cache (mainly for tests). */
18
18
  cache?: WebToolsCache<WebSearchOutput>;
19
+ /** Effective per-request timeout (seconds); falls back to env/default when unset. */
20
+ timeoutSecs?: number;
19
21
  }
20
22
  export declare function createWebSearchToolDefinition(cwd: string, options?: WebSearchToolOptions): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined>;
21
23
  export declare function createWebSearchTool(cwd: string, options?: WebSearchToolOptions): AgentTool<typeof websearchSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"websearch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAON,KAAK,eAAe,EAEpB,aAAa,EACb,KAAK,iBAAiB,EACtB,MAAM,sBAAsB,CAAC;AAK9B,QAAA,MAAM,eAAe;;;;EAYnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC9D,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACvC;AAwDD,wBAAgB,6BAA6B,CAC5C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA0D1E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC,OAAO,eAAe,CAAC,CAElH","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\thostnameOf,\n\tisHostBlocked,\n\tloadWebtoolsIgnore,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\tWEBTOOLS_DEFAULT_TIMEOUT_SECS,\n\ttype WebSearchOutput,\n\ttype WebSearchResultItem,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_RESULTS = 5;\nconst MAX_RESULTS_CAP = 10;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n\tmaxResults: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS}, max: ${MAX_RESULTS_CAP})`,\n\t\t}),\n\t),\n\tsafeSearch: Type.Optional(\n\t\tType.Union([Type.Literal(\"on\"), Type.Literal(\"off\")], {\n\t\t\tdescription: \"Safe search filter. Omit to use the search engine's default.\",\n\t\t}),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tresultCount?: number;\n\thiddenCount?: number;\n\ttokenEstimate?: number;\n}\n\nexport interface WebSearchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebSearchOutput>;\n}\n\n/**\n * Render the kept results in reference style: title + snippet with a trailing\n * [N] marker, then a `References:` block. Indices are renumbered after filtering\n * so the visible list stays contiguous.\n */\nfunction renderSearchText(query: string, kept: WebSearchResultItem[], hiddenCount: number): string {\n\tif (kept.length === 0) {\n\t\tconst suffix = hiddenCount > 0 ? ` (${hiddenCount} blocked by .webtoolsignore policy)` : \"\";\n\t\treturn `No results for \"${query}\"${suffix}`;\n\t}\n\tconst blocks: string[] = [];\n\tconst refs: string[] = [];\n\tkept.forEach((item, i) => {\n\t\tconst n = i + 1;\n\t\tblocks.push(`${item.title} [${n}]\\n${item.snippet}`);\n\t\trefs.push(`[${n}] ${item.url}`);\n\t});\n\tlet text = `${blocks.join(\"\\n\\n\")}\\n\\nReferences:\\n${refs.join(\"\\n\")}`;\n\tif (hiddenCount > 0) {\n\t\ttext += `\\n\\n[${hiddenCount} result${hiddenCount === 1 ? \"\" : \"s\"} hidden by .webtoolsignore policy]`;\n\t}\n\treturn text;\n}\n\nfunction formatWebsearchCall(args: { query?: string; maxResults?: number } | undefined): string {\n\tconst query = str(args?.query);\n\tconst queryDisplay =\n\t\tquery === null ? invalidArgText(appTheme) : query ? `\"${query}\"` : appTheme.fg(\"toolOutput\", \"...\");\n\tconst limit = args?.maxResults;\n\tlet text = appTheme.fg(\"toolTitle\", appTheme.bold(\"websearch \")) + appTheme.fg(\"accent\", queryDisplay);\n\tif (limit !== undefined) text += appTheme.fg(\"muted\", ` (${limit})`);\n\treturn text;\n}\n\nfunction formatWebsearchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebSearchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.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\tcwd: string,\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebSearchOutput>();\n\t// Resolve CA/insecure plumbing once (settings overrides, else env) and thread\n\t// it into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web (DuckDuckGo, no API key) and return ranked results as titles + snippets with reference-style [N] links. Use to discover URLs, then webfetch to read a result in full. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Search the web and return ranked results with links\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use websearch to discover URLs when you do not already have one, then webfetch the most relevant result to read it in full.\",\n\t\t],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, maxResults, safeSearch }: WebSearchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst effectiveMax = Math.min(MAX_RESULTS_CAP, Math.max(1, maxResults ?? DEFAULT_MAX_RESULTS));\n\t\t\tconst cacheKey = `${effectiveMax}:${safeSearch ?? \"default\"}:${query}`;\n\n\t\t\tconst args = [\"--query\", query, \"--max-results\", String(effectiveMax)];\n\t\t\tif (safeSearch) args.push(\"--safe-search\", safeSearch);\n\t\t\tconst output = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebSearchOutput>(\"search\", args, cwd, sig, WEBTOOLS_DEFAULT_TIMEOUT_SECS, tlsConfig),\n\t\t\t);\n\n\t\t\t// Filter result links through .webtoolsignore policy.\n\t\t\tconst matcher = loadWebtoolsIgnore(cwd);\n\t\t\tconst allResults = output.results ?? [];\n\t\t\tconst kept = matcher\n\t\t\t\t? allResults.filter((item) => {\n\t\t\t\t\t\tconst host = hostnameOf(item.url);\n\t\t\t\t\t\treturn host ? !isHostBlocked(matcher, host) : true;\n\t\t\t\t\t})\n\t\t\t\t: allResults;\n\t\t\tconst hiddenCount = allResults.length - kept.length;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderSearchText(query, kept, hiddenCount) }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: kept.length,\n\t\t\t\t\thiddenCount,\n\t\t\t\t\ttokenEstimate: output.token_estimate,\n\t\t\t\t},\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(formatWebsearchCall(args));\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(formatWebsearchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string, options?: WebSearchToolOptions): AgentTool<typeof websearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"websearch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAON,KAAK,eAAe,EAEpB,aAAa,EACb,KAAK,iBAAiB,EACtB,MAAM,sBAAsB,CAAC;AAK9B,QAAA,MAAM,eAAe;;;;EAYnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC9D,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IACvC,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAwDD,wBAAgB,6BAA6B,CAC5C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA2D1E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC,OAAO,eAAe,CAAC,CAElH","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\thostnameOf,\n\tisHostBlocked,\n\tloadWebtoolsIgnore,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebSearchOutput,\n\ttype WebSearchResultItem,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_RESULTS = 5;\nconst MAX_RESULTS_CAP = 10;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n\tmaxResults: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS}, max: ${MAX_RESULTS_CAP})`,\n\t\t}),\n\t),\n\tsafeSearch: Type.Optional(\n\t\tType.Union([Type.Literal(\"on\"), Type.Literal(\"off\")], {\n\t\t\tdescription: \"Safe search filter. Omit to use the search engine's default.\",\n\t\t}),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tresultCount?: number;\n\thiddenCount?: number;\n\ttokenEstimate?: number;\n}\n\nexport interface WebSearchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebSearchOutput>;\n\t/** Effective per-request timeout (seconds); falls back to env/default when unset. */\n\ttimeoutSecs?: number;\n}\n\n/**\n * Render the kept results in reference style: title + snippet with a trailing\n * [N] marker, then a `References:` block. Indices are renumbered after filtering\n * so the visible list stays contiguous.\n */\nfunction renderSearchText(query: string, kept: WebSearchResultItem[], hiddenCount: number): string {\n\tif (kept.length === 0) {\n\t\tconst suffix = hiddenCount > 0 ? ` (${hiddenCount} blocked by .webtoolsignore policy)` : \"\";\n\t\treturn `No results for \"${query}\"${suffix}`;\n\t}\n\tconst blocks: string[] = [];\n\tconst refs: string[] = [];\n\tkept.forEach((item, i) => {\n\t\tconst n = i + 1;\n\t\tblocks.push(`${item.title} [${n}]\\n${item.snippet}`);\n\t\trefs.push(`[${n}] ${item.url}`);\n\t});\n\tlet text = `${blocks.join(\"\\n\\n\")}\\n\\nReferences:\\n${refs.join(\"\\n\")}`;\n\tif (hiddenCount > 0) {\n\t\ttext += `\\n\\n[${hiddenCount} result${hiddenCount === 1 ? \"\" : \"s\"} hidden by .webtoolsignore policy]`;\n\t}\n\treturn text;\n}\n\nfunction formatWebsearchCall(args: { query?: string; maxResults?: number } | undefined): string {\n\tconst query = str(args?.query);\n\tconst queryDisplay =\n\t\tquery === null ? invalidArgText(appTheme) : query ? `\"${query}\"` : appTheme.fg(\"toolOutput\", \"...\");\n\tconst limit = args?.maxResults;\n\tlet text = appTheme.fg(\"toolTitle\", appTheme.bold(\"websearch \")) + appTheme.fg(\"accent\", queryDisplay);\n\tif (limit !== undefined) text += appTheme.fg(\"muted\", ` (${limit})`);\n\treturn text;\n}\n\nfunction formatWebsearchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebSearchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.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\tcwd: string,\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebSearchOutput>();\n\t// Resolve CA/insecure plumbing and the request timeout once (settings\n\t// overrides, else env) and thread them into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\tconst timeoutSecs = resolveWebtoolsTimeoutSecs(options?.timeoutSecs);\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web (DuckDuckGo, no API key) and return ranked results as titles + snippets with reference-style [N] links. Use to discover URLs, then webfetch to read a result in full. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Search the web and return ranked results with links\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use websearch to discover URLs when you do not already have one, then webfetch the most relevant result to read it in full.\",\n\t\t],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, maxResults, safeSearch }: WebSearchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst effectiveMax = Math.min(MAX_RESULTS_CAP, Math.max(1, maxResults ?? DEFAULT_MAX_RESULTS));\n\t\t\tconst cacheKey = `${effectiveMax}:${safeSearch ?? \"default\"}:${query}`;\n\n\t\t\tconst args = [\"--query\", query, \"--max-results\", String(effectiveMax)];\n\t\t\tif (safeSearch) args.push(\"--safe-search\", safeSearch);\n\t\t\tconst output = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebSearchOutput>(\"search\", args, cwd, sig, timeoutSecs, tlsConfig),\n\t\t\t);\n\n\t\t\t// Filter result links through .webtoolsignore policy.\n\t\t\tconst matcher = loadWebtoolsIgnore(cwd);\n\t\t\tconst allResults = output.results ?? [];\n\t\t\tconst kept = matcher\n\t\t\t\t? allResults.filter((item) => {\n\t\t\t\t\t\tconst host = hostnameOf(item.url);\n\t\t\t\t\t\treturn host ? !isHostBlocked(matcher, host) : true;\n\t\t\t\t\t})\n\t\t\t\t: allResults;\n\t\t\tconst hiddenCount = allResults.length - kept.length;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderSearchText(query, kept, hiddenCount) }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: kept.length,\n\t\t\t\t\thiddenCount,\n\t\t\t\t\ttokenEstimate: output.token_estimate,\n\t\t\t\t},\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(formatWebsearchCall(args));\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(formatWebsearchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string, options?: WebSearchToolOptions): AgentTool<typeof websearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd, options));\n}\n"]}
@@ -4,7 +4,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
4
4
  import { theme as appTheme } from "../../modes/interactive/theme/theme.js";
5
5
  import { getTextOutput, invalidArgText, str } from "./render-utils.js";
6
6
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
7
- import { hostnameOf, isHostBlocked, loadWebtoolsIgnore, resolveWebtoolsTLSConfig, runWebtools, WEBTOOLS_DEFAULT_TIMEOUT_SECS, WebToolsCache, } from "./webtools-shared.js";
7
+ import { hostnameOf, isHostBlocked, loadWebtoolsIgnore, resolveWebtoolsTimeoutSecs, resolveWebtoolsTLSConfig, runWebtools, WebToolsCache, } from "./webtools-shared.js";
8
8
  const DEFAULT_MAX_RESULTS = 5;
9
9
  const MAX_RESULTS_CAP = 10;
10
10
  const websearchSchema = Type.Object({
@@ -65,9 +65,10 @@ function formatWebsearchResult(result, options, showImages) {
65
65
  }
66
66
  export function createWebSearchToolDefinition(cwd, options) {
67
67
  const cache = options?.cache ?? new WebToolsCache();
68
- // Resolve CA/insecure plumbing once (settings overrides, else env) and thread
69
- // it into every spawn; not hardcoded.
68
+ // Resolve CA/insecure plumbing and the request timeout once (settings
69
+ // overrides, else env) and thread them into every spawn; not hardcoded.
70
70
  const tlsConfig = resolveWebtoolsTLSConfig(options);
71
+ const timeoutSecs = resolveWebtoolsTimeoutSecs(options?.timeoutSecs);
71
72
  return {
72
73
  name: "websearch",
73
74
  label: "websearch",
@@ -85,7 +86,7 @@ export function createWebSearchToolDefinition(cwd, options) {
85
86
  const args = ["--query", query, "--max-results", String(effectiveMax)];
86
87
  if (safeSearch)
87
88
  args.push("--safe-search", safeSearch);
88
- const output = await cache.getOrCompute(cacheKey, signal, (sig) => runWebtools("search", args, cwd, sig, WEBTOOLS_DEFAULT_TIMEOUT_SECS, tlsConfig));
89
+ const output = await cache.getOrCompute(cacheKey, signal, (sig) => runWebtools("search", args, cwd, sig, timeoutSecs, tlsConfig));
89
90
  // Filter result links through .webtoolsignore policy.
90
91
  const matcher = loadWebtoolsIgnore(cwd);
91
92
  const allResults = output.results ?? [];
@@ -1 +1 @@
1
- {"version":3,"file":"websearch.js","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\thostnameOf,\n\tisHostBlocked,\n\tloadWebtoolsIgnore,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\tWEBTOOLS_DEFAULT_TIMEOUT_SECS,\n\ttype WebSearchOutput,\n\ttype WebSearchResultItem,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_RESULTS = 5;\nconst MAX_RESULTS_CAP = 10;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n\tmaxResults: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS}, max: ${MAX_RESULTS_CAP})`,\n\t\t}),\n\t),\n\tsafeSearch: Type.Optional(\n\t\tType.Union([Type.Literal(\"on\"), Type.Literal(\"off\")], {\n\t\t\tdescription: \"Safe search filter. Omit to use the search engine's default.\",\n\t\t}),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tresultCount?: number;\n\thiddenCount?: number;\n\ttokenEstimate?: number;\n}\n\nexport interface WebSearchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebSearchOutput>;\n}\n\n/**\n * Render the kept results in reference style: title + snippet with a trailing\n * [N] marker, then a `References:` block. Indices are renumbered after filtering\n * so the visible list stays contiguous.\n */\nfunction renderSearchText(query: string, kept: WebSearchResultItem[], hiddenCount: number): string {\n\tif (kept.length === 0) {\n\t\tconst suffix = hiddenCount > 0 ? ` (${hiddenCount} blocked by .webtoolsignore policy)` : \"\";\n\t\treturn `No results for \"${query}\"${suffix}`;\n\t}\n\tconst blocks: string[] = [];\n\tconst refs: string[] = [];\n\tkept.forEach((item, i) => {\n\t\tconst n = i + 1;\n\t\tblocks.push(`${item.title} [${n}]\\n${item.snippet}`);\n\t\trefs.push(`[${n}] ${item.url}`);\n\t});\n\tlet text = `${blocks.join(\"\\n\\n\")}\\n\\nReferences:\\n${refs.join(\"\\n\")}`;\n\tif (hiddenCount > 0) {\n\t\ttext += `\\n\\n[${hiddenCount} result${hiddenCount === 1 ? \"\" : \"s\"} hidden by .webtoolsignore policy]`;\n\t}\n\treturn text;\n}\n\nfunction formatWebsearchCall(args: { query?: string; maxResults?: number } | undefined): string {\n\tconst query = str(args?.query);\n\tconst queryDisplay =\n\t\tquery === null ? invalidArgText(appTheme) : query ? `\"${query}\"` : appTheme.fg(\"toolOutput\", \"...\");\n\tconst limit = args?.maxResults;\n\tlet text = appTheme.fg(\"toolTitle\", appTheme.bold(\"websearch \")) + appTheme.fg(\"accent\", queryDisplay);\n\tif (limit !== undefined) text += appTheme.fg(\"muted\", ` (${limit})`);\n\treturn text;\n}\n\nfunction formatWebsearchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebSearchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.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\tcwd: string,\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebSearchOutput>();\n\t// Resolve CA/insecure plumbing once (settings overrides, else env) and thread\n\t// it into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web (DuckDuckGo, no API key) and return ranked results as titles + snippets with reference-style [N] links. Use to discover URLs, then webfetch to read a result in full. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Search the web and return ranked results with links\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use websearch to discover URLs when you do not already have one, then webfetch the most relevant result to read it in full.\",\n\t\t],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, maxResults, safeSearch }: WebSearchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst effectiveMax = Math.min(MAX_RESULTS_CAP, Math.max(1, maxResults ?? DEFAULT_MAX_RESULTS));\n\t\t\tconst cacheKey = `${effectiveMax}:${safeSearch ?? \"default\"}:${query}`;\n\n\t\t\tconst args = [\"--query\", query, \"--max-results\", String(effectiveMax)];\n\t\t\tif (safeSearch) args.push(\"--safe-search\", safeSearch);\n\t\t\tconst output = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebSearchOutput>(\"search\", args, cwd, sig, WEBTOOLS_DEFAULT_TIMEOUT_SECS, tlsConfig),\n\t\t\t);\n\n\t\t\t// Filter result links through .webtoolsignore policy.\n\t\t\tconst matcher = loadWebtoolsIgnore(cwd);\n\t\t\tconst allResults = output.results ?? [];\n\t\t\tconst kept = matcher\n\t\t\t\t? allResults.filter((item) => {\n\t\t\t\t\t\tconst host = hostnameOf(item.url);\n\t\t\t\t\t\treturn host ? !isHostBlocked(matcher, host) : true;\n\t\t\t\t\t})\n\t\t\t\t: allResults;\n\t\t\tconst hiddenCount = allResults.length - kept.length;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderSearchText(query, kept, hiddenCount) }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: kept.length,\n\t\t\t\t\thiddenCount,\n\t\t\t\t\ttokenEstimate: output.token_estimate,\n\t\t\t\t},\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(formatWebsearchCall(args));\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(formatWebsearchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string, options?: WebSearchToolOptions): AgentTool<typeof websearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"websearch.js","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAChD,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wDAAwD,CAAC;AACjF,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAE3E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EACN,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,0BAA0B,EAC1B,wBAAwB,EACxB,WAAW,EAGX,aAAa,GAEb,MAAM,sBAAsB,CAAC;AAE9B,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAC9B,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,CAAC;IACnC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;IACvD,UAAU,EAAE,IAAI,CAAC,QAAQ,CACxB,IAAI,CAAC,MAAM,CAAC;QACX,WAAW,EAAE,iDAAiD,mBAAmB,UAAU,eAAe,GAAG;KAC7G,CAAC,CACF;IACD,UAAU,EAAE,IAAI,CAAC,QAAQ,CACxB,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE;QACrD,WAAW,EAAE,8DAA8D;KAC3E,CAAC,CACF;CACD,CAAC,CAAC;AAiBH;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,KAAa,EAAE,IAA2B,EAAE,WAAmB,EAAU;IAClG,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,WAAW,qCAAqC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5F,OAAO,mBAAmB,KAAK,IAAI,MAAM,EAAE,CAAC;IAC7C,CAAC;IACD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,KAAK,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAAA,CAChC,CAAC,CAAC;IACH,IAAI,IAAI,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,oBAAoB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACvE,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;QACrB,IAAI,IAAI,QAAQ,WAAW,UAAU,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,oCAAoC,CAAC;IACvG,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,mBAAmB,CAAC,IAAyD,EAAU;IAC/F,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/B,MAAM,YAAY,GACjB,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACrG,MAAM,KAAK,GAAG,IAAI,EAAE,UAAU,CAAC;IAC/B,IAAI,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACvG,IAAI,KAAK,KAAK,SAAS;QAAE,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,KAAK,GAAG,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,qBAAqB,CAC7B,MAA2F,EAC3F,OAAgC,EAChC,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAa,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/D,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,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,SAAS,cAAc,CAAC,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,CAAC;QACnH,CAAC;IACF,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,6BAA6B,CAC5C,GAAW,EACX,OAA8B,EAC6C;IAC3E,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,IAAI,aAAa,EAAmB,CAAC;IACrE,sEAAsE;IACtE,wEAAwE;IACxE,MAAM,SAAS,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,WAAW,GAAG,0BAA0B,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACrE,OAAO;QACN,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,WAAW;QAClB,WAAW,EACV,sOAAsO;QACvO,aAAa,EAAE,qDAAqD;QACpE,gBAAgB,EAAE;YACjB,6HAA6H;SAC7H;QACD,UAAU,EAAE,eAAe;QAC3B,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAsB,EAAE,MAAoB,EAAE;YACvG,IAAI,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAE1D,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,IAAI,mBAAmB,CAAC,CAAC,CAAC;YAC/F,MAAM,QAAQ,GAAG,GAAG,YAAY,IAAI,UAAU,IAAI,SAAS,IAAI,KAAK,EAAE,CAAC;YAEvE,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;YACvE,IAAI,UAAU;gBAAE,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;YACvD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACjE,WAAW,CAAkB,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,CAAC,CAC9E,CAAC;YAEF,sDAAsD;YACtD,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;YACxC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,OAAO;gBACnB,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;oBAC5B,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAClC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAA,CACnD,CAAC;gBACH,CAAC,CAAC,UAAU,CAAC;YACd,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YAEpD,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC;gBACtF,OAAO,EAAE;oBACR,WAAW,EAAE,IAAI,CAAC,MAAM;oBACxB,WAAW;oBACX,aAAa,EAAE,MAAM,CAAC,cAAc;iBACpC;aACD,CAAC;QAAA,CACF;QACD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;YACjC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;YACxC,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;YAC9C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,qBAAqB,CAAC,MAAa,EAAE,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YAChF,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAE,OAA8B,EAAqC;IACnH,OAAO,kBAAkB,CAAC,6BAA6B,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CACvE","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.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 {\n\thostnameOf,\n\tisHostBlocked,\n\tloadWebtoolsIgnore,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebSearchOutput,\n\ttype WebSearchResultItem,\n\tWebToolsCache,\n\ttype WebtoolsTLSConfig,\n} from \"./webtools-shared.js\";\n\nconst DEFAULT_MAX_RESULTS = 5;\nconst MAX_RESULTS_CAP = 10;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n\tmaxResults: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS}, max: ${MAX_RESULTS_CAP})`,\n\t\t}),\n\t),\n\tsafeSearch: Type.Optional(\n\t\tType.Union([Type.Literal(\"on\"), Type.Literal(\"off\")], {\n\t\t\tdescription: \"Safe search filter. Omit to use the search engine's default.\",\n\t\t}),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tresultCount?: number;\n\thiddenCount?: number;\n\ttokenEstimate?: number;\n}\n\nexport interface WebSearchToolOptions extends WebtoolsTLSConfig {\n\t/** Override the result cache (mainly for tests). */\n\tcache?: WebToolsCache<WebSearchOutput>;\n\t/** Effective per-request timeout (seconds); falls back to env/default when unset. */\n\ttimeoutSecs?: number;\n}\n\n/**\n * Render the kept results in reference style: title + snippet with a trailing\n * [N] marker, then a `References:` block. Indices are renumbered after filtering\n * so the visible list stays contiguous.\n */\nfunction renderSearchText(query: string, kept: WebSearchResultItem[], hiddenCount: number): string {\n\tif (kept.length === 0) {\n\t\tconst suffix = hiddenCount > 0 ? ` (${hiddenCount} blocked by .webtoolsignore policy)` : \"\";\n\t\treturn `No results for \"${query}\"${suffix}`;\n\t}\n\tconst blocks: string[] = [];\n\tconst refs: string[] = [];\n\tkept.forEach((item, i) => {\n\t\tconst n = i + 1;\n\t\tblocks.push(`${item.title} [${n}]\\n${item.snippet}`);\n\t\trefs.push(`[${n}] ${item.url}`);\n\t});\n\tlet text = `${blocks.join(\"\\n\\n\")}\\n\\nReferences:\\n${refs.join(\"\\n\")}`;\n\tif (hiddenCount > 0) {\n\t\ttext += `\\n\\n[${hiddenCount} result${hiddenCount === 1 ? \"\" : \"s\"} hidden by .webtoolsignore policy]`;\n\t}\n\treturn text;\n}\n\nfunction formatWebsearchCall(args: { query?: string; maxResults?: number } | undefined): string {\n\tconst query = str(args?.query);\n\tconst queryDisplay =\n\t\tquery === null ? invalidArgText(appTheme) : query ? `\"${query}\"` : appTheme.fg(\"toolOutput\", \"...\");\n\tconst limit = args?.maxResults;\n\tlet text = appTheme.fg(\"toolTitle\", appTheme.bold(\"websearch \")) + appTheme.fg(\"accent\", queryDisplay);\n\tif (limit !== undefined) text += appTheme.fg(\"muted\", ` (${limit})`);\n\treturn text;\n}\n\nfunction formatWebsearchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WebSearchToolDetails },\n\toptions: ToolRenderResultOptions,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, 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) => appTheme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${appTheme.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\tcwd: string,\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\tconst cache = options?.cache ?? new WebToolsCache<WebSearchOutput>();\n\t// Resolve CA/insecure plumbing and the request timeout once (settings\n\t// overrides, else env) and thread them into every spawn; not hardcoded.\n\tconst tlsConfig = resolveWebtoolsTLSConfig(options);\n\tconst timeoutSecs = resolveWebtoolsTimeoutSecs(options?.timeoutSecs);\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web (DuckDuckGo, no API key) and return ranked results as titles + snippets with reference-style [N] links. Use to discover URLs, then webfetch to read a result in full. Off by default; enabled with --enable-webtools.\",\n\t\tpromptSnippet: \"Search the web and return ranked results with links\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use websearch to discover URLs when you do not already have one, then webfetch the most relevant result to read it in full.\",\n\t\t],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, maxResults, safeSearch }: WebSearchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst effectiveMax = Math.min(MAX_RESULTS_CAP, Math.max(1, maxResults ?? DEFAULT_MAX_RESULTS));\n\t\t\tconst cacheKey = `${effectiveMax}:${safeSearch ?? \"default\"}:${query}`;\n\n\t\t\tconst args = [\"--query\", query, \"--max-results\", String(effectiveMax)];\n\t\t\tif (safeSearch) args.push(\"--safe-search\", safeSearch);\n\t\t\tconst output = await cache.getOrCompute(cacheKey, signal, (sig) =>\n\t\t\t\trunWebtools<WebSearchOutput>(\"search\", args, cwd, sig, timeoutSecs, tlsConfig),\n\t\t\t);\n\n\t\t\t// Filter result links through .webtoolsignore policy.\n\t\t\tconst matcher = loadWebtoolsIgnore(cwd);\n\t\t\tconst allResults = output.results ?? [];\n\t\t\tconst kept = matcher\n\t\t\t\t? allResults.filter((item) => {\n\t\t\t\t\t\tconst host = hostnameOf(item.url);\n\t\t\t\t\t\treturn host ? !isHostBlocked(matcher, host) : true;\n\t\t\t\t\t})\n\t\t\t\t: allResults;\n\t\t\tconst hiddenCount = allResults.length - kept.length;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: renderSearchText(query, kept, hiddenCount) }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: kept.length,\n\t\t\t\t\thiddenCount,\n\t\t\t\t\ttokenEstimate: output.token_estimate,\n\t\t\t\t},\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(formatWebsearchCall(args));\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(formatWebsearchResult(result as any, options, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string, options?: WebSearchToolOptions): AgentTool<typeof websearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd, options));\n}\n"]}
@@ -71,6 +71,15 @@ export interface WebtoolsTLSConfig {
71
71
  * (`HOOCODE_WEBTOOLS_CA_CERT`, `HOOCODE_WEBTOOLS_INSECURE`). Never hardcoded.
72
72
  */
73
73
  export declare function resolveWebtoolsTLSConfig(overrides?: WebtoolsTLSConfig): WebtoolsTLSConfig;
74
+ /**
75
+ * Resolve the effective webtools request timeout (seconds) from an explicit
76
+ * override (e.g. settings.json passed down from the tool factories) falling back
77
+ * to the environment (`HOOCODE_WEBTOOLS_TIMEOUT`) and finally the default. Mirrors
78
+ * {@link resolveWebtoolsTLSConfig}: resolve once, thread in, never hardcode. A
79
+ * malformed or out-of-range env value falls back to the default; every result is
80
+ * clamped to [1, 120].
81
+ */
82
+ export declare function resolveWebtoolsTimeoutSecs(override?: number): number;
74
83
  /**
75
84
  * Run a `webtools` subcommand with `--json` and return parsed stdout.
76
85
  *
@@ -1 +1 @@
1
- {"version":3,"file":"webtools-shared.d.ts","sourceRoot":"","sources":["../../../src/core/tools/webtools-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared plumbing for the `webfetch` and `websearch` tools.\n *\n * Both tools shell out to the `webtools` binary (fetch / search subcommands,\n * resolved/downloaded via {@link ensureTool}) and parse its `--json` output.\n * This module owns:\n * - the spawn-and-parse runner,\n * - the locked JSON result types,\n * - a short-lived in-process result cache, and\n * - the `.webtoolsignore` policy matcher (gitignore semantics) used to block\n * hosts both before a fetch and when filtering search result links.\n */\n\nimport { accessSync, constants, existsSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport ignore from \"ignore\";\nimport { getAgentDir } from \"../../config.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { execCommand } from \"../exec.js\";\n\ntype IgnoreMatcher = ReturnType<typeof ignore>;\n\n/** Default request timeout (seconds) passed to the binary. */\nexport const WEBTOOLS_DEFAULT_TIMEOUT_SECS = 15;\n\n/** How long a successful result stays cached, mirroring the documented 15-min TTL. */\nconst CACHE_TTL_MS = 15 * 60 * 1000;\n\n// ============================================================================\n// Result types (locked against `webtools <cmd> --json`)\n// ============================================================================\n\nexport interface WebFetchReference {\n\tindex: number;\n\turl: string;\n\ttext?: string;\n}\n\nexport interface WebFetchMetadata {\n\tdescription?: string;\n\tauthor?: string;\n\tpublished?: string;\n\tlang?: string;\n\tsite_name?: string;\n}\n\nexport interface WebFetchResult {\n\ttitle?: string;\n\tfinal_url: string;\n\tcontent: string;\n\tcontent_type: string;\n\tmedia: string;\n\ttoken_estimate: number;\n\treferences: WebFetchReference[];\n\tmetadata?: WebFetchMetadata;\n\tsource: string;\n}\n\nexport interface WebSearchResultItem {\n\ttitle: string;\n\tsnippet: string;\n\turl: string;\n\tref_index: number;\n}\n\nexport interface WebSearchReference {\n\tindex: number;\n\turl: string;\n}\n\nexport interface WebSearchOutput {\n\tquery: string;\n\tresults: WebSearchResultItem[];\n\treferences: WebSearchReference[];\n\ttoken_estimate: number;\n\tresult_count: number;\n}\n\n// ============================================================================\n// Binary runner\n// ============================================================================\n\nconst BINARY_MISSING_MESSAGE =\n\t\"webtools binary unavailable and could not be downloaded — web tools require the `webtools` CLI on PATH or a published release for this platform\";\n\n/**\n * TLS plumbing forwarded to the `webtools` binary for `webfetch`/`websearch`.\n * Kept separate from hoocode's own app-level TLS trust (utils/tls-ca.ts): the\n * binary has its own TLS stack, so it needs the CA / insecure flag passed in.\n */\nexport interface WebtoolsTLSConfig {\n\t/** Path to a PEM CA bundle forwarded as `--ca-cert <path>` (validated readable). */\n\tcaCertPath?: string;\n\t/** Forward `--insecure` (disables TLS verification in the binary). Strictly opt-in. */\n\tinsecure?: boolean;\n}\n\nfunction isTruthyEnv(value: string | undefined): boolean {\n\tif (!value) return false;\n\tconst normalized = value.trim().toLowerCase();\n\treturn normalized === \"1\" || normalized === \"true\" || normalized === \"yes\";\n}\n\n/**\n * Resolve the webtools TLS config from explicit overrides (e.g. settings.json\n * passed down from the tool factories) falling back to the environment\n * (`HOOCODE_WEBTOOLS_CA_CERT`, `HOOCODE_WEBTOOLS_INSECURE`). Never hardcoded.\n */\nexport function resolveWebtoolsTLSConfig(overrides?: WebtoolsTLSConfig): WebtoolsTLSConfig {\n\tconst envCaCert = process.env.HOOCODE_WEBTOOLS_CA_CERT?.trim();\n\tconst caCertPath = overrides?.caCertPath ?? (envCaCert && envCaCert.length > 0 ? envCaCert : undefined);\n\tconst insecure = overrides?.insecure ?? isTruthyEnv(process.env.HOOCODE_WEBTOOLS_INSECURE);\n\treturn { caCertPath, insecure };\n}\n\n// Warn at most once per distinct message for the life of the process.\nconst warnedWebtoolsMessages = new Set<string>();\nfunction warnOnce(message: string): void {\n\tif (warnedWebtoolsMessages.has(message)) return;\n\twarnedWebtoolsMessages.add(message);\n\tconsole.warn(chalk.yellow(`[webtools] ${message}`));\n}\n\n/** True only when `path` is a readable regular file; warns once and returns false otherwise. */\nfunction isReadableFile(path: string): boolean {\n\ttry {\n\t\tif (!statSync(path).isFile()) {\n\t\t\twarnOnce(`--ca-cert path is not a regular file, ignoring: ${path}`);\n\t\t\treturn false;\n\t\t}\n\t\taccessSync(path, constants.R_OK);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\twarnOnce(`--ca-cert path is not readable, ignoring: ${path} (${reason})`);\n\t\treturn false;\n\t}\n}\n\n/** Build the TLS-related argv flags forwarded to the binary (argv array, no shell). */\nfunction buildTLSArgs(config: WebtoolsTLSConfig | undefined): string[] {\n\tconst flags: string[] = [];\n\tif (!config) return flags;\n\tif (config.caCertPath && isReadableFile(config.caCertPath)) {\n\t\tflags.push(\"--ca-cert\", config.caCertPath);\n\t}\n\tif (config.insecure) {\n\t\twarnOnce(\n\t\t\t\"webtools running with --insecure: TLS verification is DISABLED for webfetch/websearch. \" +\n\t\t\t\t\"Prefer HOOCODE_WEBTOOLS_CA_CERT to trust your proxy's CA with verification kept on.\",\n\t\t);\n\t\tflags.push(\"--insecure\");\n\t}\n\treturn flags;\n}\n\n/**\n * Run a `webtools` subcommand with `--json` and return parsed stdout.\n *\n * Throws on missing binary, non-zero exit (surfacing the binary's stderr), or\n * unparseable output. Callers convert thrown errors into tool error results.\n */\nexport async function runWebtools<T>(\n\tsubcommand: \"fetch\" | \"search\",\n\targs: string[],\n\tcwd: string,\n\tsignal?: AbortSignal,\n\ttimeoutSecs: number = WEBTOOLS_DEFAULT_TIMEOUT_SECS,\n\ttlsConfig?: WebtoolsTLSConfig,\n): Promise<T> {\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\tconst binaryPath = await ensureTool(\"webtools\", true);\n\tif (!binaryPath) throw new Error(BINARY_MISSING_MESSAGE);\n\n\t// Give the spawn a little headroom over the binary's own request timeout so\n\t// the binary reports the timeout itself rather than being killed mid-flight.\n\tconst spawnTimeoutMs = (timeoutSecs + 5) * 1000;\n\tconst tlsArgs = buildTLSArgs(tlsConfig);\n\tconst result = await execCommand(binaryPath, [subcommand, ...args, ...tlsArgs, \"--json\"], cwd, {\n\t\tsignal,\n\t\ttimeout: spawnTimeoutMs,\n\t});\n\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\tif (result.killed) throw new Error(`webtools ${subcommand} timed out after ${timeoutSecs}s`);\n\tif (result.code !== 0) {\n\t\tconst stderr = result.stderr.trim();\n\t\tthrow new Error(stderr || `webtools ${subcommand} exited with code ${result.code}`);\n\t}\n\n\ttry {\n\t\treturn JSON.parse(result.stdout) as T;\n\t} catch {\n\t\tthrow new Error(`webtools ${subcommand} returned malformed JSON`);\n\t}\n}\n\n// ============================================================================\n// Result cache (per-process, short TTL)\n// ============================================================================\n\ninterface CacheEntry<T> {\n\tvalue: T;\n\texpiresAt: number;\n}\n\n/**\n * A computation shared by every caller that requested the same key while it was\n * still running. The subprocess is only aborted once *all* joined callers have\n * aborted, tracked by {@link refCount} against a shared {@link controller}.\n */\ninterface InFlightEntry<T> {\n\tpromise: Promise<T>;\n\tcontroller: AbortController;\n\trefCount: number;\n}\n\nexport class WebToolsCache<T> {\n\tprivate readonly entries = new Map<string, CacheEntry<T>>();\n\tprivate readonly inflight = new Map<string, InFlightEntry<T>>();\n\n\tget(key: string): T | undefined {\n\t\tconst entry = this.entries.get(key);\n\t\tif (!entry) return undefined;\n\t\tif (Date.now() >= entry.expiresAt) {\n\t\t\tthis.entries.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\t\treturn entry.value;\n\t}\n\n\tset(key: string, value: T): void {\n\t\tthis.entries.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });\n\t}\n\n\t/**\n\t * Return a cached value, join an identical in-flight computation, or start a\n\t * new one — collapsing concurrent duplicate fetch/search calls onto a single\n\t * subprocess. Successful results are cached; failures are not.\n\t *\n\t * Cancellation is shared safely: a caller whose own `signal` aborts rejects\n\t * promptly and releases its reference, but the underlying work keeps running\n\t * for the remaining callers and is only cancelled once none are left.\n\t */\n\tasync getOrCompute(\n\t\tkey: string,\n\t\tsignal: AbortSignal | undefined,\n\t\tcompute: (signal: AbortSignal) => Promise<T>,\n\t): Promise<T> {\n\t\tconst cached = this.get(key);\n\t\tif (cached !== undefined) return cached;\n\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\tlet entry = this.inflight.get(key);\n\t\tif (!entry) {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst promise = (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst value = await compute(controller.signal);\n\t\t\t\t\tthis.set(key, value);\n\t\t\t\t\treturn value;\n\t\t\t\t} finally {\n\t\t\t\t\tthis.inflight.delete(key);\n\t\t\t\t}\n\t\t\t})();\n\t\t\tentry = { promise, controller, refCount: 0 };\n\t\t\tthis.inflight.set(key, entry);\n\t\t}\n\n\t\tconst joined = entry;\n\t\t// Every joined caller (signalled or not) holds a reference; the shared work\n\t\t// is cancelled only when an abort drops the count back to zero.\n\t\tjoined.refCount++;\n\n\t\tif (!signal) {\n\t\t\treturn joined.promise;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tif (joined.refCount > 0) joined.refCount--;\n\t\t\tif (joined.refCount === 0) joined.controller.abort();\n\t\t};\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\ttry {\n\t\t\treturn await Promise.race([\n\t\t\t\tjoined.promise,\n\t\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\t\tsignal.addEventListener(\"abort\", () => reject(new Error(\"Operation aborted\")), { once: true });\n\t\t\t\t}),\n\t\t\t]);\n\t\t} finally {\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t}\n\t}\n}\n\n// ============================================================================\n// .webtoolsignore policy matcher\n// ============================================================================\n\n/**\n * Memoize the parsed matcher per cwd. The policy is consulted on every webfetch\n * (twice: permission gate + tool execute) and every websearch, so re-reading and\n * re-parsing three files each time is wasted sync I/O on the hot path. The cache\n * is invalidated by a cheap stat signature (existence + mtime + size) so an\n * edited `.webtoolsignore` still takes effect immediately — correctness matters\n * here because this gate enforces host policy.\n */\ninterface IgnoreCacheEntry {\n\tsignature: string;\n\tmatcher: IgnoreMatcher | undefined;\n}\nconst ignoreCacheByCwd = new Map<string, IgnoreCacheEntry>();\n\nfunction ignoreSignature(files: string[]): string {\n\treturn files\n\t\t.map((file) => {\n\t\t\ttry {\n\t\t\t\tconst st = statSync(file);\n\t\t\t\treturn `${file}:${st.mtimeMs}:${st.size}`;\n\t\t\t} catch {\n\t\t\t\treturn `${file}:absent`;\n\t\t\t}\n\t\t})\n\t\t.join(\"|\");\n}\n\n/**\n * Build an {@link Ignore} matcher from `.webtoolsignore` policy files.\n *\n * Precedence is project-after-user so a project file can re-allow (`!host`)\n * something the user blocked, matching gitignore layering. Returns undefined\n * when no policy files exist (the common case: everything allowed).\n *\n * Hosts are matched as single path components, so subdomains need an explicit\n * wildcard (`*.example.com`), exactly like gitignore directory matching.\n */\nexport function loadWebtoolsIgnore(cwd: string): IgnoreMatcher | undefined {\n\tconst files = [\n\t\tjoin(getAgentDir(), \"webtoolsignore\"),\n\t\tjoin(homedir(), \".webtoolsignore\"),\n\t\tjoin(cwd, \".webtoolsignore\"),\n\t];\n\n\tconst signature = ignoreSignature(files);\n\tconst cached = ignoreCacheByCwd.get(cwd);\n\tif (cached && cached.signature === signature) {\n\t\treturn cached.matcher;\n\t}\n\n\tlet found = false;\n\tconst ig = ignore();\n\tfor (const file of files) {\n\t\tif (!existsSync(file)) continue;\n\t\ttry {\n\t\t\tig.add(readFileSync(file, \"utf8\"));\n\t\t\tfound = true;\n\t\t} catch {\n\t\t\t// Unreadable policy file: ignore it rather than failing the tool call.\n\t\t}\n\t}\n\tconst matcher = found ? ig : undefined;\n\tignoreCacheByCwd.set(cwd, { signature, matcher });\n\treturn matcher;\n}\n\n/** Extract the lowercased hostname from a URL, or undefined if it cannot be parsed. */\nexport function hostnameOf(url: string): string | undefined {\n\ttry {\n\t\tconst host = new URL(url).hostname.toLowerCase();\n\t\treturn host || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Whether a host is blocked by policy. A matcher is required; with no policy\n * files present callers treat every host as allowed.\n */\nexport function isHostBlocked(matcher: IgnoreMatcher, host: string): boolean {\n\tif (!host) return false;\n\treturn matcher.ignores(host);\n}\n\n/**\n * Convenience used by the permission gate: returns the blocked host for a URL,\n * or undefined when the URL is allowed (or there is no policy / unparseable URL).\n */\nexport function blockedHostForUrl(cwd: string, url: string): string | undefined {\n\tconst matcher = loadWebtoolsIgnore(cwd);\n\tif (!matcher) return undefined;\n\tconst host = hostnameOf(url);\n\tif (!host) return undefined;\n\treturn isHostBlocked(matcher, host) ? host : undefined;\n}\n"]}
1
+ {"version":3,"file":"webtools-shared.d.ts","sourceRoot":"","sources":["../../../src/core/tools/webtools-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared plumbing for the `webfetch` and `websearch` tools.\n *\n * Both tools shell out to the `webtools` binary (fetch / search subcommands,\n * resolved/downloaded via {@link ensureTool}) and parse its `--json` output.\n * This module owns:\n * - the spawn-and-parse runner,\n * - the locked JSON result types,\n * - a short-lived in-process result cache, and\n * - the `.webtoolsignore` policy matcher (gitignore semantics) used to block\n * hosts both before a fetch and when filtering search result links.\n */\n\nimport { accessSync, constants, existsSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport chalk from \"chalk\";\nimport ignore from \"ignore\";\nimport { getAgentDir } from \"../../config.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { execCommand } from \"../exec.js\";\n\ntype IgnoreMatcher = ReturnType<typeof ignore>;\n\n/** Default request timeout (seconds) passed to the binary. */\nexport const WEBTOOLS_DEFAULT_TIMEOUT_SECS = 15;\n\n/** Lower/upper bounds on the effective request timeout (seconds). */\nconst WEBTOOLS_MIN_TIMEOUT_SECS = 1;\nconst WEBTOOLS_MAX_TIMEOUT_SECS = 120;\n\n/** How long a successful result stays cached, mirroring the documented 15-min TTL. */\nconst CACHE_TTL_MS = 15 * 60 * 1000;\n\n// ============================================================================\n// Result types (locked against `webtools <cmd> --json`)\n// ============================================================================\n\nexport interface WebFetchReference {\n\tindex: number;\n\turl: string;\n\ttext?: string;\n}\n\nexport interface WebFetchMetadata {\n\tdescription?: string;\n\tauthor?: string;\n\tpublished?: string;\n\tlang?: string;\n\tsite_name?: string;\n}\n\nexport interface WebFetchResult {\n\ttitle?: string;\n\tfinal_url: string;\n\tcontent: string;\n\tcontent_type: string;\n\tmedia: string;\n\ttoken_estimate: number;\n\treferences: WebFetchReference[];\n\tmetadata?: WebFetchMetadata;\n\tsource: string;\n}\n\nexport interface WebSearchResultItem {\n\ttitle: string;\n\tsnippet: string;\n\turl: string;\n\tref_index: number;\n}\n\nexport interface WebSearchReference {\n\tindex: number;\n\turl: string;\n}\n\nexport interface WebSearchOutput {\n\tquery: string;\n\tresults: WebSearchResultItem[];\n\treferences: WebSearchReference[];\n\ttoken_estimate: number;\n\tresult_count: number;\n}\n\n// ============================================================================\n// Binary runner\n// ============================================================================\n\nconst BINARY_MISSING_MESSAGE =\n\t\"webtools binary unavailable and could not be downloaded — web tools require the `webtools` CLI on PATH or a published release for this platform\";\n\n/**\n * TLS plumbing forwarded to the `webtools` binary for `webfetch`/`websearch`.\n * Kept separate from hoocode's own app-level TLS trust (utils/tls-ca.ts): the\n * binary has its own TLS stack, so it needs the CA / insecure flag passed in.\n */\nexport interface WebtoolsTLSConfig {\n\t/** Path to a PEM CA bundle forwarded as `--ca-cert <path>` (validated readable). */\n\tcaCertPath?: string;\n\t/** Forward `--insecure` (disables TLS verification in the binary). Strictly opt-in. */\n\tinsecure?: boolean;\n}\n\nfunction isTruthyEnv(value: string | undefined): boolean {\n\tif (!value) return false;\n\tconst normalized = value.trim().toLowerCase();\n\treturn normalized === \"1\" || normalized === \"true\" || normalized === \"yes\";\n}\n\n/**\n * Resolve the webtools TLS config from explicit overrides (e.g. settings.json\n * passed down from the tool factories) falling back to the environment\n * (`HOOCODE_WEBTOOLS_CA_CERT`, `HOOCODE_WEBTOOLS_INSECURE`). Never hardcoded.\n */\nexport function resolveWebtoolsTLSConfig(overrides?: WebtoolsTLSConfig): WebtoolsTLSConfig {\n\tconst envCaCert = process.env.HOOCODE_WEBTOOLS_CA_CERT?.trim();\n\tconst caCertPath = overrides?.caCertPath ?? (envCaCert && envCaCert.length > 0 ? envCaCert : undefined);\n\tconst insecure = overrides?.insecure ?? isTruthyEnv(process.env.HOOCODE_WEBTOOLS_INSECURE);\n\treturn { caCertPath, insecure };\n}\n\n/** Clamp a request timeout to the supported range, flooring to whole seconds. */\nfunction clampTimeoutSecs(secs: number): number {\n\treturn Math.min(WEBTOOLS_MAX_TIMEOUT_SECS, Math.max(WEBTOOLS_MIN_TIMEOUT_SECS, Math.floor(secs)));\n}\n\n/**\n * Resolve the effective webtools request timeout (seconds) from an explicit\n * override (e.g. settings.json passed down from the tool factories) falling back\n * to the environment (`HOOCODE_WEBTOOLS_TIMEOUT`) and finally the default. Mirrors\n * {@link resolveWebtoolsTLSConfig}: resolve once, thread in, never hardcode. A\n * malformed or out-of-range env value falls back to the default; every result is\n * clamped to [1, 120].\n */\nexport function resolveWebtoolsTimeoutSecs(override?: number): number {\n\tif (override !== undefined && Number.isFinite(override)) {\n\t\treturn clampTimeoutSecs(override);\n\t}\n\tconst envRaw = process.env.HOOCODE_WEBTOOLS_TIMEOUT?.trim();\n\tif (envRaw) {\n\t\tconst envValue = Number(envRaw);\n\t\tif (Number.isFinite(envValue) && envValue > 0) {\n\t\t\treturn clampTimeoutSecs(envValue);\n\t\t}\n\t}\n\treturn WEBTOOLS_DEFAULT_TIMEOUT_SECS;\n}\n\n// Warn at most once per distinct message for the life of the process.\nconst warnedWebtoolsMessages = new Set<string>();\nfunction warnOnce(message: string): void {\n\tif (warnedWebtoolsMessages.has(message)) return;\n\twarnedWebtoolsMessages.add(message);\n\tconsole.warn(chalk.yellow(`[webtools] ${message}`));\n}\n\n/** True only when `path` is a readable regular file; warns once and returns false otherwise. */\nfunction isReadableFile(path: string): boolean {\n\ttry {\n\t\tif (!statSync(path).isFile()) {\n\t\t\twarnOnce(`--ca-cert path is not a regular file, ignoring: ${path}`);\n\t\t\treturn false;\n\t\t}\n\t\taccessSync(path, constants.R_OK);\n\t\treturn true;\n\t} catch (error) {\n\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\twarnOnce(`--ca-cert path is not readable, ignoring: ${path} (${reason})`);\n\t\treturn false;\n\t}\n}\n\n/** Build the TLS-related argv flags forwarded to the binary (argv array, no shell). */\nfunction buildTLSArgs(config: WebtoolsTLSConfig | undefined): string[] {\n\tconst flags: string[] = [];\n\tif (!config) return flags;\n\tif (config.caCertPath && isReadableFile(config.caCertPath)) {\n\t\tflags.push(\"--ca-cert\", config.caCertPath);\n\t}\n\tif (config.insecure) {\n\t\twarnOnce(\n\t\t\t\"webtools running with --insecure: TLS verification is DISABLED for webfetch/websearch. \" +\n\t\t\t\t\"Prefer HOOCODE_WEBTOOLS_CA_CERT to trust your proxy's CA with verification kept on.\",\n\t\t);\n\t\tflags.push(\"--insecure\");\n\t}\n\treturn flags;\n}\n\n/**\n * Run a `webtools` subcommand with `--json` and return parsed stdout.\n *\n * Throws on missing binary, non-zero exit (surfacing the binary's stderr), or\n * unparseable output. Callers convert thrown errors into tool error results.\n */\nexport async function runWebtools<T>(\n\tsubcommand: \"fetch\" | \"search\",\n\targs: string[],\n\tcwd: string,\n\tsignal?: AbortSignal,\n\ttimeoutSecs: number = WEBTOOLS_DEFAULT_TIMEOUT_SECS,\n\ttlsConfig?: WebtoolsTLSConfig,\n): Promise<T> {\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\tconst binaryPath = await ensureTool(\"webtools\", true);\n\tif (!binaryPath) throw new Error(BINARY_MISSING_MESSAGE);\n\n\t// Give the spawn a little headroom over the binary's own request timeout so\n\t// the binary reports the timeout itself rather than being killed mid-flight.\n\tconst spawnTimeoutMs = (timeoutSecs + 5) * 1000;\n\tconst tlsArgs = buildTLSArgs(tlsConfig);\n\tconst result = await execCommand(binaryPath, [subcommand, ...args, ...tlsArgs, \"--json\"], cwd, {\n\t\tsignal,\n\t\ttimeout: spawnTimeoutMs,\n\t});\n\n\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\tif (result.killed) throw new Error(`webtools ${subcommand} timed out after ${timeoutSecs}s`);\n\tif (result.code !== 0) {\n\t\tconst stderr = result.stderr.trim();\n\t\tthrow new Error(stderr || `webtools ${subcommand} exited with code ${result.code}`);\n\t}\n\n\ttry {\n\t\treturn JSON.parse(result.stdout) as T;\n\t} catch {\n\t\tthrow new Error(`webtools ${subcommand} returned malformed JSON`);\n\t}\n}\n\n// ============================================================================\n// Result cache (per-process, short TTL)\n// ============================================================================\n\ninterface CacheEntry<T> {\n\tvalue: T;\n\texpiresAt: number;\n}\n\n/**\n * A computation shared by every caller that requested the same key while it was\n * still running. The subprocess is only aborted once *all* joined callers have\n * aborted, tracked by {@link refCount} against a shared {@link controller}.\n */\ninterface InFlightEntry<T> {\n\tpromise: Promise<T>;\n\tcontroller: AbortController;\n\trefCount: number;\n}\n\nexport class WebToolsCache<T> {\n\tprivate readonly entries = new Map<string, CacheEntry<T>>();\n\tprivate readonly inflight = new Map<string, InFlightEntry<T>>();\n\n\tget(key: string): T | undefined {\n\t\tconst entry = this.entries.get(key);\n\t\tif (!entry) return undefined;\n\t\tif (Date.now() >= entry.expiresAt) {\n\t\t\tthis.entries.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\t\treturn entry.value;\n\t}\n\n\tset(key: string, value: T): void {\n\t\tthis.entries.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });\n\t}\n\n\t/**\n\t * Return a cached value, join an identical in-flight computation, or start a\n\t * new one — collapsing concurrent duplicate fetch/search calls onto a single\n\t * subprocess. Successful results are cached; failures are not.\n\t *\n\t * Cancellation is shared safely: a caller whose own `signal` aborts rejects\n\t * promptly and releases its reference, but the underlying work keeps running\n\t * for the remaining callers and is only cancelled once none are left.\n\t */\n\tasync getOrCompute(\n\t\tkey: string,\n\t\tsignal: AbortSignal | undefined,\n\t\tcompute: (signal: AbortSignal) => Promise<T>,\n\t): Promise<T> {\n\t\tconst cached = this.get(key);\n\t\tif (cached !== undefined) return cached;\n\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\tlet entry = this.inflight.get(key);\n\t\tif (!entry) {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst promise = (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst value = await compute(controller.signal);\n\t\t\t\t\tthis.set(key, value);\n\t\t\t\t\treturn value;\n\t\t\t\t} finally {\n\t\t\t\t\tthis.inflight.delete(key);\n\t\t\t\t}\n\t\t\t})();\n\t\t\tentry = { promise, controller, refCount: 0 };\n\t\t\tthis.inflight.set(key, entry);\n\t\t}\n\n\t\tconst joined = entry;\n\t\t// Every joined caller (signalled or not) holds a reference; the shared work\n\t\t// is cancelled only when an abort drops the count back to zero.\n\t\tjoined.refCount++;\n\n\t\tif (!signal) {\n\t\t\treturn joined.promise;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tif (joined.refCount > 0) joined.refCount--;\n\t\t\tif (joined.refCount === 0) joined.controller.abort();\n\t\t};\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\ttry {\n\t\t\treturn await Promise.race([\n\t\t\t\tjoined.promise,\n\t\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\t\tsignal.addEventListener(\"abort\", () => reject(new Error(\"Operation aborted\")), { once: true });\n\t\t\t\t}),\n\t\t\t]);\n\t\t} finally {\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t}\n\t}\n}\n\n// ============================================================================\n// .webtoolsignore policy matcher\n// ============================================================================\n\n/**\n * Memoize the parsed matcher per cwd. The policy is consulted on every webfetch\n * (twice: permission gate + tool execute) and every websearch, so re-reading and\n * re-parsing three files each time is wasted sync I/O on the hot path. The cache\n * is invalidated by a cheap stat signature (existence + mtime + size) so an\n * edited `.webtoolsignore` still takes effect immediately — correctness matters\n * here because this gate enforces host policy.\n */\ninterface IgnoreCacheEntry {\n\tsignature: string;\n\tmatcher: IgnoreMatcher | undefined;\n}\nconst ignoreCacheByCwd = new Map<string, IgnoreCacheEntry>();\n\nfunction ignoreSignature(files: string[]): string {\n\treturn files\n\t\t.map((file) => {\n\t\t\ttry {\n\t\t\t\tconst st = statSync(file);\n\t\t\t\treturn `${file}:${st.mtimeMs}:${st.size}`;\n\t\t\t} catch {\n\t\t\t\treturn `${file}:absent`;\n\t\t\t}\n\t\t})\n\t\t.join(\"|\");\n}\n\n/**\n * Build an {@link Ignore} matcher from `.webtoolsignore` policy files.\n *\n * Precedence is project-after-user so a project file can re-allow (`!host`)\n * something the user blocked, matching gitignore layering. Returns undefined\n * when no policy files exist (the common case: everything allowed).\n *\n * Hosts are matched as single path components, so subdomains need an explicit\n * wildcard (`*.example.com`), exactly like gitignore directory matching.\n */\nexport function loadWebtoolsIgnore(cwd: string): IgnoreMatcher | undefined {\n\tconst files = [\n\t\tjoin(getAgentDir(), \"webtoolsignore\"),\n\t\tjoin(homedir(), \".webtoolsignore\"),\n\t\tjoin(cwd, \".webtoolsignore\"),\n\t];\n\n\tconst signature = ignoreSignature(files);\n\tconst cached = ignoreCacheByCwd.get(cwd);\n\tif (cached && cached.signature === signature) {\n\t\treturn cached.matcher;\n\t}\n\n\tlet found = false;\n\tconst ig = ignore();\n\tfor (const file of files) {\n\t\tif (!existsSync(file)) continue;\n\t\ttry {\n\t\t\tig.add(readFileSync(file, \"utf8\"));\n\t\t\tfound = true;\n\t\t} catch {\n\t\t\t// Unreadable policy file: ignore it rather than failing the tool call.\n\t\t}\n\t}\n\tconst matcher = found ? ig : undefined;\n\tignoreCacheByCwd.set(cwd, { signature, matcher });\n\treturn matcher;\n}\n\n/** Extract the lowercased hostname from a URL, or undefined if it cannot be parsed. */\nexport function hostnameOf(url: string): string | undefined {\n\ttry {\n\t\tconst host = new URL(url).hostname.toLowerCase();\n\t\treturn host || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Whether a host is blocked by policy. A matcher is required; with no policy\n * files present callers treat every host as allowed.\n */\nexport function isHostBlocked(matcher: IgnoreMatcher, host: string): boolean {\n\tif (!host) return false;\n\treturn matcher.ignores(host);\n}\n\n/**\n * Convenience used by the permission gate: returns the blocked host for a URL,\n * or undefined when the URL is allowed (or there is no policy / unparseable URL).\n */\nexport function blockedHostForUrl(cwd: string, url: string): string | undefined {\n\tconst matcher = loadWebtoolsIgnore(cwd);\n\tif (!matcher) return undefined;\n\tconst host = hostnameOf(url);\n\tif (!host) return undefined;\n\treturn isHostBlocked(matcher, host) ? host : undefined;\n}\n"]}
@@ -20,6 +20,9 @@ import { ensureTool } from "../../utils/tools-manager.js";
20
20
  import { execCommand } from "../exec.js";
21
21
  /** Default request timeout (seconds) passed to the binary. */
22
22
  export const WEBTOOLS_DEFAULT_TIMEOUT_SECS = 15;
23
+ /** Lower/upper bounds on the effective request timeout (seconds). */
24
+ const WEBTOOLS_MIN_TIMEOUT_SECS = 1;
25
+ const WEBTOOLS_MAX_TIMEOUT_SECS = 120;
23
26
  /** How long a successful result stays cached, mirroring the documented 15-min TTL. */
24
27
  const CACHE_TTL_MS = 15 * 60 * 1000;
25
28
  // ============================================================================
@@ -43,6 +46,31 @@ export function resolveWebtoolsTLSConfig(overrides) {
43
46
  const insecure = overrides?.insecure ?? isTruthyEnv(process.env.HOOCODE_WEBTOOLS_INSECURE);
44
47
  return { caCertPath, insecure };
45
48
  }
49
+ /** Clamp a request timeout to the supported range, flooring to whole seconds. */
50
+ function clampTimeoutSecs(secs) {
51
+ return Math.min(WEBTOOLS_MAX_TIMEOUT_SECS, Math.max(WEBTOOLS_MIN_TIMEOUT_SECS, Math.floor(secs)));
52
+ }
53
+ /**
54
+ * Resolve the effective webtools request timeout (seconds) from an explicit
55
+ * override (e.g. settings.json passed down from the tool factories) falling back
56
+ * to the environment (`HOOCODE_WEBTOOLS_TIMEOUT`) and finally the default. Mirrors
57
+ * {@link resolveWebtoolsTLSConfig}: resolve once, thread in, never hardcode. A
58
+ * malformed or out-of-range env value falls back to the default; every result is
59
+ * clamped to [1, 120].
60
+ */
61
+ export function resolveWebtoolsTimeoutSecs(override) {
62
+ if (override !== undefined && Number.isFinite(override)) {
63
+ return clampTimeoutSecs(override);
64
+ }
65
+ const envRaw = process.env.HOOCODE_WEBTOOLS_TIMEOUT?.trim();
66
+ if (envRaw) {
67
+ const envValue = Number(envRaw);
68
+ if (Number.isFinite(envValue) && envValue > 0) {
69
+ return clampTimeoutSecs(envValue);
70
+ }
71
+ }
72
+ return WEBTOOLS_DEFAULT_TIMEOUT_SECS;
73
+ }
46
74
  // Warn at most once per distinct message for the life of the process.
47
75
  const warnedWebtoolsMessages = new Set();
48
76
  function warnOnce(message) {