@kolisachint/hoocode-agent 0.5.42 → 0.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/core/settings-defaults.d.ts +1 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +14 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +15 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +3 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/tools/webfetch.d.ts +9 -0
- package/dist/core/tools/webfetch.d.ts.map +1 -1
- package/dist/core/tools/webfetch.js +52 -5
- package/dist/core/tools/webfetch.js.map +1 -1
- package/dist/core/tools/websearch.d.ts.map +1 -1
- package/dist/core/tools/websearch.js +8 -0
- package/dist/core/tools/websearch.js.map +1 -1
- package/dist/core/tools/webtools-shared.d.ts +83 -2
- package/dist/core/tools/webtools-shared.d.ts.map +1 -1
- package/dist/core/tools/webtools-shared.js +60 -2
- package/dist/core/tools/webtools-shared.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +11 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +6 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +16 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/websearch-warning.d.ts +35 -0
- package/dist/modes/interactive/websearch-warning.d.ts.map +1 -0
- package/dist/modes/interactive/websearch-warning.js +40 -0
- package/dist/modes/interactive/websearch-warning.js.map +1 -0
- package/docs/settings.md +69 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -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,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAMN,KAAK,qBAAqB,EAC1B,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;AAIH,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;IACf,4FAA4F;IAC5F,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAC/B;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,CAgExE;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\tfetchStatusNote,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebFetchContentStatus,\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\ntype WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\tcontentType?: string;\n\tmedia?: string;\n\t/** Non-\"ok\" means extraction produced nothing usable; see {@link WebFetchContentStatus}. */\n\tstatus?: WebFetchContentStatus;\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\t// An empty body and a JavaScript-rendered shell look identical in the\n\t\t\t// content alone. Say which it was, so the page is not read as \"nothing\n\t\t\t// to say\" when it simply needs a browser.\n\t\t\tconst note = fetchStatusNote(result.status);\n\t\t\tconst body = note ? `${result.content}\\n\\n[webtools: ${note}]`.trimStart() : result.content;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + body }],\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\tstatus: result.status,\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,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAON,KAAK,qBAAqB,EAC1B,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;;;;;EAmBlB,CAAC;AAIH,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAC/B;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;AAyDD,wBAAgB,4BAA4B,CAC3C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,mBAAmB,GAC3B,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CAuFxE;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\tfetchStatusNote,\n\tisTruncatedContent,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebFetchContentStatus,\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\toffset: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription:\n\t\t\t\t\"Byte offset into the page's extracted text to read from, for continuing a long page. Use the offset the previous fetch reported; windows tile the document exactly, so nothing is skipped or repeated.\",\n\t\t}),\n\t),\n});\n\ntype WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\t/** The page continued past the token budget: what came back is a prefix. */\n\ttruncated?: boolean;\n\t/** The budget the cut was made at, so the TUI can say what to raise. */\n\tmaxTokens?: number;\n\t/** Estimated tokens of the whole page, when the binary reports it. */\n\ttotalTokenEstimate?: number;\n\t/** Where to resume reading, when the binary reports paging offsets. */\n\tnextOffset?: number;\n\tcontentType?: string;\n\tmedia?: string;\n\t/** Non-\"ok\" means extraction produced nothing usable; see {@link WebFetchContentStatus}. */\n\tstatus?: WebFetchContentStatus;\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\t// A cut page and a complete one cost the same at the budget, so the number\n\t\t// alone reads as \"this is the page\". Mark the ones that are a prefix.\n\t\tconst cut = result.details?.truncated\n\t\t\t? appTheme.fg(\"warning\", ` (truncated at ${result.details.maxTokens ?? tokenEstimate})`)\n\t\t\t: \"\";\n\t\ttext += `\\n${appTheme.fg(\"muted\", `~${tokenEstimate} tokens`)}${cut}`;\n\t}\n\treturn text;\n}\n\n/**\n * What to tell the model when a page did not fit.\n *\n * With paging offsets it is a position to resume at, which is the whole point:\n * the rest of the document is one call away and costs another window, not\n * another copy of the page. Without them (an older binary) the only truthful\n * advice is a larger budget.\n */\nfunction continuationNote(result: WebFetchResult, offset: number, maxTokens: number): string {\n\tconst next = result.next_offset;\n\tif (next === undefined) {\n\t\treturn `output stopped at the ${maxTokens}-token budget; the page continues past this point. Re-fetch with a larger maxTokens (up to ${MAX_TOKENS_CAP}) for more, or fetch a more specific URL or #anchor.`;\n\t}\n\tconst total = result.total_token_estimate;\n\tconst progress =\n\t\ttotal !== undefined ? `~${result.token_estimate} of ~${total} tokens` : `${result.token_estimate} tokens`;\n\treturn `showing bytes ${result.offset ?? offset}-${next} of ${result.total_bytes ?? \"?\"} (${progress}); continue with offset=${next}`;\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\t\"A fetch that reports it stopped at its token budget returned a prefix, not the page. When it names a continue offset, pass that as `offset` to read the next window; windows tile exactly, so nothing is skipped or repeated. Only keep going while the answer is genuinely further down — a more specific URL or #anchor is usually cheaper than paging a whole document.\",\n\t\t],\n\t\tparameters: webfetchSchema,\n\t\tasync execute(_toolCallId, { url, maxTokens, output, offset }: 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 effectiveOffset = Number.isFinite(offset) && offset !== undefined ? Math.max(0, Math.floor(offset)) : 0;\n\t\t\tconst cacheKey = `${format}:${effectiveMaxTokens}:${effectiveOffset}:${url}`;\n\n\t\t\tconst args = [\"--url\", url, \"--max-tokens\", String(effectiveMaxTokens), \"--output\", format];\n\t\t\t// Only sent when non-zero: an older binary rejects the unknown flag,\n\t\t\t// and a page read from the start never needs it.\n\t\t\tif (effectiveOffset > 0) args.push(\"--offset\", String(effectiveOffset));\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\t// An empty body and a JavaScript-rendered shell look identical in the\n\t\t\t// content alone. Say which it was, so the page is not read as \"nothing\n\t\t\t// to say\" when it simply needs a browser.\n\t\t\tconst note = fetchStatusNote(result.status);\n\t\t\tlet body = note ? `${result.content}\\n\\n[webtools: ${note}]`.trimStart() : result.content;\n\n\t\t\t// A cut page used to end in a bare elision marker: the model could see\n\t\t\t// that something was missing but had no way to act on it, so a long\n\t\t\t// document was a dead end rather than a first page. Say where the\n\t\t\t// window sits and how to continue past it.\n\t\t\t//\n\t\t\t// The binary's own flag is authoritative; the marker is the fallback\n\t\t\t// for binaries older than the paging fields, where the only honest\n\t\t\t// advice is a larger budget because there is no offset to resume at.\n\t\t\tconst truncated = result.truncated ?? isTruncatedContent(result.content);\n\t\t\tif (truncated) {\n\t\t\t\tbody += `\\n\\n[webtools: ${continuationNote(result, effectiveOffset, effectiveMaxTokens)}]`;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + body }],\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\ttruncated,\n\t\t\t\t\tmaxTokens: effectiveMaxTokens,\n\t\t\t\t\ttotalTokenEstimate: result.total_token_estimate,\n\t\t\t\t\tnextOffset: result.next_offset,\n\t\t\t\t\tcontentType: result.content_type,\n\t\t\t\t\tmedia: result.media,\n\t\t\t\t\tstatus: result.status,\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, fetchStatusNote, resolveWebtoolsTimeoutSecs, resolveWebtoolsTLSConfig, runWebtools, WebToolsCache, } from "./webtools-shared.js";
|
|
7
|
+
import { blockedHostForUrl, fetchStatusNote, isTruncatedContent, 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.
|
|
@@ -23,6 +23,9 @@ const webfetchSchema = Type.Object({
|
|
|
23
23
|
output: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("markdown")], {
|
|
24
24
|
description: "Output format: 'text' (default, most token-efficient, links as [N] with a trailing reference block) or 'markdown' (inline links).",
|
|
25
25
|
})),
|
|
26
|
+
offset: Type.Optional(Type.Number({
|
|
27
|
+
description: "Byte offset into the page's extracted text to read from, for continuing a long page. Use the offset the previous fetch reported; windows tile the document exactly, so nothing is skipped or repeated.",
|
|
28
|
+
})),
|
|
26
29
|
});
|
|
27
30
|
function formatWebfetchCall(args) {
|
|
28
31
|
const url = str(args?.url);
|
|
@@ -45,10 +48,32 @@ function formatWebfetchResult(result, options, showImages) {
|
|
|
45
48
|
}
|
|
46
49
|
const tokenEstimate = result.details?.tokenEstimate;
|
|
47
50
|
if (tokenEstimate !== undefined) {
|
|
48
|
-
|
|
51
|
+
// A cut page and a complete one cost the same at the budget, so the number
|
|
52
|
+
// alone reads as "this is the page". Mark the ones that are a prefix.
|
|
53
|
+
const cut = result.details?.truncated
|
|
54
|
+
? appTheme.fg("warning", ` (truncated at ${result.details.maxTokens ?? tokenEstimate})`)
|
|
55
|
+
: "";
|
|
56
|
+
text += `\n${appTheme.fg("muted", `~${tokenEstimate} tokens`)}${cut}`;
|
|
49
57
|
}
|
|
50
58
|
return text;
|
|
51
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* What to tell the model when a page did not fit.
|
|
62
|
+
*
|
|
63
|
+
* With paging offsets it is a position to resume at, which is the whole point:
|
|
64
|
+
* the rest of the document is one call away and costs another window, not
|
|
65
|
+
* another copy of the page. Without them (an older binary) the only truthful
|
|
66
|
+
* advice is a larger budget.
|
|
67
|
+
*/
|
|
68
|
+
function continuationNote(result, offset, maxTokens) {
|
|
69
|
+
const next = result.next_offset;
|
|
70
|
+
if (next === undefined) {
|
|
71
|
+
return `output stopped at the ${maxTokens}-token budget; the page continues past this point. Re-fetch with a larger maxTokens (up to ${MAX_TOKENS_CAP}) for more, or fetch a more specific URL or #anchor.`;
|
|
72
|
+
}
|
|
73
|
+
const total = result.total_token_estimate;
|
|
74
|
+
const progress = total !== undefined ? `~${result.token_estimate} of ~${total} tokens` : `${result.token_estimate} tokens`;
|
|
75
|
+
return `showing bytes ${result.offset ?? offset}-${next} of ${result.total_bytes ?? "?"} (${progress}); continue with offset=${next}`;
|
|
76
|
+
}
|
|
52
77
|
export function createWebFetchToolDefinition(cwd, options) {
|
|
53
78
|
const cache = options?.cache ?? new WebToolsCache();
|
|
54
79
|
// Resolve CA/insecure plumbing and the request timeout once (settings
|
|
@@ -62,9 +87,10 @@ export function createWebFetchToolDefinition(cwd, options) {
|
|
|
62
87
|
promptSnippet: "Fetch a URL and return clean, token-efficient page content",
|
|
63
88
|
promptGuidelines: [
|
|
64
89
|
"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.",
|
|
90
|
+
"A fetch that reports it stopped at its token budget returned a prefix, not the page. When it names a continue offset, pass that as `offset` to read the next window; windows tile exactly, so nothing is skipped or repeated. Only keep going while the answer is genuinely further down — a more specific URL or #anchor is usually cheaper than paging a whole document.",
|
|
65
91
|
],
|
|
66
92
|
parameters: webfetchSchema,
|
|
67
|
-
async execute(_toolCallId, { url, maxTokens, output }, signal) {
|
|
93
|
+
async execute(_toolCallId, { url, maxTokens, output, offset }, signal) {
|
|
68
94
|
if (signal?.aborted)
|
|
69
95
|
throw new Error("Operation aborted");
|
|
70
96
|
// Policy gate (.webtoolsignore). SSRF/private-address blocking lives in
|
|
@@ -75,21 +101,42 @@ export function createWebFetchToolDefinition(cwd, options) {
|
|
|
75
101
|
}
|
|
76
102
|
const effectiveMaxTokens = clampMaxTokens(maxTokens);
|
|
77
103
|
const format = output ?? "text";
|
|
78
|
-
const
|
|
104
|
+
const effectiveOffset = Number.isFinite(offset) && offset !== undefined ? Math.max(0, Math.floor(offset)) : 0;
|
|
105
|
+
const cacheKey = `${format}:${effectiveMaxTokens}:${effectiveOffset}:${url}`;
|
|
79
106
|
const args = ["--url", url, "--max-tokens", String(effectiveMaxTokens), "--output", format];
|
|
107
|
+
// Only sent when non-zero: an older binary rejects the unknown flag,
|
|
108
|
+
// and a page read from the start never needs it.
|
|
109
|
+
if (effectiveOffset > 0)
|
|
110
|
+
args.push("--offset", String(effectiveOffset));
|
|
80
111
|
const result = await cache.getOrCompute(cacheKey, signal, (sig) => runWebtools("fetch", args, cwd, sig, timeoutSecs, tlsConfig));
|
|
81
112
|
const header = result.title ? `${result.title}\n${result.final_url}\n\n` : `${result.final_url}\n\n`;
|
|
82
113
|
// An empty body and a JavaScript-rendered shell look identical in the
|
|
83
114
|
// content alone. Say which it was, so the page is not read as "nothing
|
|
84
115
|
// to say" when it simply needs a browser.
|
|
85
116
|
const note = fetchStatusNote(result.status);
|
|
86
|
-
|
|
117
|
+
let body = note ? `${result.content}\n\n[webtools: ${note}]`.trimStart() : result.content;
|
|
118
|
+
// A cut page used to end in a bare elision marker: the model could see
|
|
119
|
+
// that something was missing but had no way to act on it, so a long
|
|
120
|
+
// document was a dead end rather than a first page. Say where the
|
|
121
|
+
// window sits and how to continue past it.
|
|
122
|
+
//
|
|
123
|
+
// The binary's own flag is authoritative; the marker is the fallback
|
|
124
|
+
// for binaries older than the paging fields, where the only honest
|
|
125
|
+
// advice is a larger budget because there is no offset to resume at.
|
|
126
|
+
const truncated = result.truncated ?? isTruncatedContent(result.content);
|
|
127
|
+
if (truncated) {
|
|
128
|
+
body += `\n\n[webtools: ${continuationNote(result, effectiveOffset, effectiveMaxTokens)}]`;
|
|
129
|
+
}
|
|
87
130
|
return {
|
|
88
131
|
content: [{ type: "text", text: header + body }],
|
|
89
132
|
details: {
|
|
90
133
|
finalUrl: result.final_url,
|
|
91
134
|
title: result.title,
|
|
92
135
|
tokenEstimate: result.token_estimate,
|
|
136
|
+
truncated,
|
|
137
|
+
maxTokens: effectiveMaxTokens,
|
|
138
|
+
totalTokenEstimate: result.total_token_estimate,
|
|
139
|
+
nextOffset: result.next_offset,
|
|
93
140
|
contentType: result.content_type,
|
|
94
141
|
media: result.media,
|
|
95
142
|
status: result.status,
|
|
@@ -1 +1 @@
|
|
|
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\tfetchStatusNote,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebFetchContentStatus,\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\ntype WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\tcontentType?: string;\n\tmedia?: string;\n\t/** Non-\"ok\" means extraction produced nothing usable; see {@link WebFetchContentStatus}. */\n\tstatus?: WebFetchContentStatus;\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\t// An empty body and a JavaScript-rendered shell look identical in the\n\t\t\t// content alone. Say which it was, so the page is not read as \"nothing\n\t\t\t// to say\" when it simply needs a browser.\n\t\t\tconst note = fetchStatusNote(result.status);\n\t\t\tconst body = note ? `${result.content}\\n\\n[webtools: ${note}]`.trimStart() : result.content;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + body }],\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\tstatus: result.status,\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\tfetchStatusNote,\n\tisTruncatedContent,\n\tresolveWebtoolsTimeoutSecs,\n\tresolveWebtoolsTLSConfig,\n\trunWebtools,\n\ttype WebFetchContentStatus,\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\toffset: Type.Optional(\n\t\tType.Number({\n\t\t\tdescription:\n\t\t\t\t\"Byte offset into the page's extracted text to read from, for continuing a long page. Use the offset the previous fetch reported; windows tile the document exactly, so nothing is skipped or repeated.\",\n\t\t}),\n\t),\n});\n\ntype WebFetchToolInput = Static<typeof webfetchSchema>;\n\nexport interface WebFetchToolDetails {\n\tfinalUrl?: string;\n\ttitle?: string;\n\ttokenEstimate?: number;\n\t/** The page continued past the token budget: what came back is a prefix. */\n\ttruncated?: boolean;\n\t/** The budget the cut was made at, so the TUI can say what to raise. */\n\tmaxTokens?: number;\n\t/** Estimated tokens of the whole page, when the binary reports it. */\n\ttotalTokenEstimate?: number;\n\t/** Where to resume reading, when the binary reports paging offsets. */\n\tnextOffset?: number;\n\tcontentType?: string;\n\tmedia?: string;\n\t/** Non-\"ok\" means extraction produced nothing usable; see {@link WebFetchContentStatus}. */\n\tstatus?: WebFetchContentStatus;\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\t// A cut page and a complete one cost the same at the budget, so the number\n\t\t// alone reads as \"this is the page\". Mark the ones that are a prefix.\n\t\tconst cut = result.details?.truncated\n\t\t\t? appTheme.fg(\"warning\", ` (truncated at ${result.details.maxTokens ?? tokenEstimate})`)\n\t\t\t: \"\";\n\t\ttext += `\\n${appTheme.fg(\"muted\", `~${tokenEstimate} tokens`)}${cut}`;\n\t}\n\treturn text;\n}\n\n/**\n * What to tell the model when a page did not fit.\n *\n * With paging offsets it is a position to resume at, which is the whole point:\n * the rest of the document is one call away and costs another window, not\n * another copy of the page. Without them (an older binary) the only truthful\n * advice is a larger budget.\n */\nfunction continuationNote(result: WebFetchResult, offset: number, maxTokens: number): string {\n\tconst next = result.next_offset;\n\tif (next === undefined) {\n\t\treturn `output stopped at the ${maxTokens}-token budget; the page continues past this point. Re-fetch with a larger maxTokens (up to ${MAX_TOKENS_CAP}) for more, or fetch a more specific URL or #anchor.`;\n\t}\n\tconst total = result.total_token_estimate;\n\tconst progress =\n\t\ttotal !== undefined ? `~${result.token_estimate} of ~${total} tokens` : `${result.token_estimate} tokens`;\n\treturn `showing bytes ${result.offset ?? offset}-${next} of ${result.total_bytes ?? \"?\"} (${progress}); continue with offset=${next}`;\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\t\"A fetch that reports it stopped at its token budget returned a prefix, not the page. When it names a continue offset, pass that as `offset` to read the next window; windows tile exactly, so nothing is skipped or repeated. Only keep going while the answer is genuinely further down — a more specific URL or #anchor is usually cheaper than paging a whole document.\",\n\t\t],\n\t\tparameters: webfetchSchema,\n\t\tasync execute(_toolCallId, { url, maxTokens, output, offset }: 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 effectiveOffset = Number.isFinite(offset) && offset !== undefined ? Math.max(0, Math.floor(offset)) : 0;\n\t\t\tconst cacheKey = `${format}:${effectiveMaxTokens}:${effectiveOffset}:${url}`;\n\n\t\t\tconst args = [\"--url\", url, \"--max-tokens\", String(effectiveMaxTokens), \"--output\", format];\n\t\t\t// Only sent when non-zero: an older binary rejects the unknown flag,\n\t\t\t// and a page read from the start never needs it.\n\t\t\tif (effectiveOffset > 0) args.push(\"--offset\", String(effectiveOffset));\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\t// An empty body and a JavaScript-rendered shell look identical in the\n\t\t\t// content alone. Say which it was, so the page is not read as \"nothing\n\t\t\t// to say\" when it simply needs a browser.\n\t\t\tconst note = fetchStatusNote(result.status);\n\t\t\tlet body = note ? `${result.content}\\n\\n[webtools: ${note}]`.trimStart() : result.content;\n\n\t\t\t// A cut page used to end in a bare elision marker: the model could see\n\t\t\t// that something was missing but had no way to act on it, so a long\n\t\t\t// document was a dead end rather than a first page. Say where the\n\t\t\t// window sits and how to continue past it.\n\t\t\t//\n\t\t\t// The binary's own flag is authoritative; the marker is the fallback\n\t\t\t// for binaries older than the paging fields, where the only honest\n\t\t\t// advice is a larger budget because there is no offset to resume at.\n\t\t\tconst truncated = result.truncated ?? isTruncatedContent(result.content);\n\t\t\tif (truncated) {\n\t\t\t\tbody += `\\n\\n[webtools: ${continuationNote(result, effectiveOffset, effectiveMaxTokens)}]`;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: header + body }],\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\ttruncated,\n\t\t\t\t\tmaxTokens: effectiveMaxTokens,\n\t\t\t\t\ttotalTokenEstimate: result.total_token_estimate,\n\t\t\t\t\tnextOffset: result.next_offset,\n\t\t\t\t\tcontentType: result.content_type,\n\t\t\t\t\tmedia: result.media,\n\t\t\t\t\tstatus: result.status,\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 +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,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;AAIH,MAAM,WAAW,oBAAoB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;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;
|
|
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,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;AAIH,MAAM,WAAW,oBAAoB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;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;AAgED,wBAAgB,6BAA6B,CAC5C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA4D1E;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\ntype WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tresultCount?: number;\n\thiddenCount?: number;\n\ttokenEstimate?: number;\n\t/** Which backend answered — a fallback from a keyed provider is otherwise silent. */\n\tprovider?: string;\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\t// What the results cost in context, as webfetch reports for a page. Search is\n\t// the web tool with no token budget of its own — snippet length is whatever\n\t// the backend returns — so the number is the only thing that makes an\n\t// expensive query visible, and it is what `maxResults` is tuned against.\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 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 and return ranked results as titles + snippets with reference-style [N] links. Defaults to keyless DuckDuckGo; Brave, Tavily and SearXNG are used instead when credentials are configured for the webtools binary. 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\tprovider: output.provider,\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"]}
|
|
@@ -61,6 +61,14 @@ function formatWebsearchResult(result, options, showImages) {
|
|
|
61
61
|
text += `${appTheme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
// What the results cost in context, as webfetch reports for a page. Search is
|
|
65
|
+
// the web tool with no token budget of its own — snippet length is whatever
|
|
66
|
+
// the backend returns — so the number is the only thing that makes an
|
|
67
|
+
// expensive query visible, and it is what `maxResults` is tuned against.
|
|
68
|
+
const tokenEstimate = result.details?.tokenEstimate;
|
|
69
|
+
if (tokenEstimate !== undefined) {
|
|
70
|
+
text += `\n${appTheme.fg("muted", `~${tokenEstimate} tokens`)}`;
|
|
71
|
+
}
|
|
64
72
|
return text;
|
|
65
73
|
}
|
|
66
74
|
export function createWebSearchToolDefinition(cwd, options) {
|
|
@@ -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\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\ntype WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tresultCount?: number;\n\thiddenCount?: number;\n\ttokenEstimate?: number;\n\t/** Which backend answered — a fallback from a keyed provider is otherwise silent. */\n\tprovider?: string;\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 and return ranked results as titles + snippets with reference-style [N] links. Defaults to keyless DuckDuckGo; Brave, Tavily and SearXNG are used instead when credentials are configured for the webtools binary. 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\tprovider: output.provider,\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":"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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\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\ntype WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tresultCount?: number;\n\thiddenCount?: number;\n\ttokenEstimate?: number;\n\t/** Which backend answered — a fallback from a keyed provider is otherwise silent. */\n\tprovider?: string;\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\t// What the results cost in context, as webfetch reports for a page. Search is\n\t// the web tool with no token budget of its own — snippet length is whatever\n\t// the backend returns — so the number is the only thing that makes an\n\t// expensive query visible, and it is what `maxResults` is tuned against.\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 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 and return ranked results as titles + snippets with reference-style [N] links. Defaults to keyless DuckDuckGo; Brave, Tavily and SearXNG are used instead when credentials are configured for the webtools binary. 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\tprovider: output.provider,\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"]}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* This module owns:
|
|
7
7
|
* - the spawn-and-parse runner,
|
|
8
8
|
* - the locked JSON result types,
|
|
9
|
-
* - a short-lived in-process result cache,
|
|
9
|
+
* - a short-lived in-process result cache,
|
|
10
10
|
* - the `.webtoolsignore` policy matcher (gitignore semantics) used to block
|
|
11
|
-
* hosts both before a fetch and when filtering search result links
|
|
11
|
+
* hosts both before a fetch and when filtering search result links, and
|
|
12
|
+
* - the read-only check for whether `websearch` has a keyed backend configured.
|
|
12
13
|
*/
|
|
13
14
|
import ignore from "ignore";
|
|
14
15
|
type IgnoreMatcher = ReturnType<typeof ignore>;
|
|
@@ -23,6 +24,21 @@ type IgnoreMatcher = ReturnType<typeof ignore>;
|
|
|
23
24
|
export type WebFetchContentStatus = "ok" | "empty" | "needs_js" | "too_complex";
|
|
24
25
|
/** Whether the search answered (`SearchOutput.status`). See {@link WebFetchContentStatus} on optionality. */
|
|
25
26
|
export type WebSearchStatus = "ok" | "empty" | "blocked";
|
|
27
|
+
/**
|
|
28
|
+
* The elision marker the binary appends when `--max-tokens` cuts a body
|
|
29
|
+
* (`compress::TRUNCATION_MARKER`). Both output formats hoocode asks for — text
|
|
30
|
+
* and markdown — route their budgeting through `truncate_to_tokens`, so its
|
|
31
|
+
* presence is what tells us a page continued past what we were handed. The
|
|
32
|
+
* binary reports no structured flag today; when it grows one, prefer that and
|
|
33
|
+
* keep this as the fallback for older binaries.
|
|
34
|
+
*/
|
|
35
|
+
export declare const WEBTOOLS_TRUNCATION_MARKER = "\u2026[truncated]";
|
|
36
|
+
/**
|
|
37
|
+
* Whether a fetch came back cut off. Substring rather than suffix: the marker
|
|
38
|
+
* lands at the end of the *body*, and the reference block is assembled after
|
|
39
|
+
* it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isTruncatedContent(content: string | undefined): boolean;
|
|
26
42
|
/** One-line explanation for a non-`ok` fetch status, mirroring the binary's own note. */
|
|
27
43
|
export declare function fetchStatusNote(status: WebFetchContentStatus | undefined): string | undefined;
|
|
28
44
|
interface WebFetchReference {
|
|
@@ -40,6 +56,21 @@ interface WebFetchMetadata {
|
|
|
40
56
|
export interface WebFetchResult {
|
|
41
57
|
title?: string;
|
|
42
58
|
final_url: string;
|
|
59
|
+
/**
|
|
60
|
+
* Where this window sits in the extracted document. Absent on binaries
|
|
61
|
+
* older than the paging fields, which is why every consumer falls back to
|
|
62
|
+
* the elision marker (see {@link isTruncatedContent}) rather than treating
|
|
63
|
+
* a missing `next_offset` as "the page ended here".
|
|
64
|
+
*/
|
|
65
|
+
offset?: number;
|
|
66
|
+
/** Byte offset to resume at, absent when the document ended in this window. */
|
|
67
|
+
next_offset?: number;
|
|
68
|
+
/** Size of the whole extracted body, the space offsets index into. */
|
|
69
|
+
total_bytes?: number;
|
|
70
|
+
/** Estimated tokens of the whole extracted body, before budget and window. */
|
|
71
|
+
total_token_estimate?: number;
|
|
72
|
+
/** The binary's own truncation flag; authoritative when present. */
|
|
73
|
+
truncated?: boolean;
|
|
43
74
|
content: string;
|
|
44
75
|
content_type: string;
|
|
45
76
|
media: string;
|
|
@@ -98,6 +129,56 @@ export declare function resolveWebtoolsTLSConfig(overrides?: WebtoolsTLSConfig):
|
|
|
98
129
|
* clamped to [1, 120].
|
|
99
130
|
*/
|
|
100
131
|
export declare function resolveWebtoolsTimeoutSecs(override?: number): number;
|
|
132
|
+
/**
|
|
133
|
+
* The `webtools.search` block of `~/.hoocode/settings.json`.
|
|
134
|
+
*
|
|
135
|
+
* hoocode and the binary share that file: the binary reads its own `webtools`
|
|
136
|
+
* key (snake_case, per its own schema) and ignores everything else, so these
|
|
137
|
+
* keys are mirrored verbatim rather than camelCased. hoocode never writes them
|
|
138
|
+
* — it only reads them to tell whether `websearch` has a keyed backend.
|
|
139
|
+
*/
|
|
140
|
+
export interface WebtoolsSearchSettings {
|
|
141
|
+
/** Primary backend: "duckduckgo" | "brave" | "tavily" | "searxng". */
|
|
142
|
+
provider?: string;
|
|
143
|
+
/** Backend tried when the primary fails; "none" disables the fallback. */
|
|
144
|
+
fallback?: string;
|
|
145
|
+
providers?: {
|
|
146
|
+
brave?: {
|
|
147
|
+
api_key?: string;
|
|
148
|
+
};
|
|
149
|
+
tavily?: {
|
|
150
|
+
api_key?: string;
|
|
151
|
+
};
|
|
152
|
+
searxng?: {
|
|
153
|
+
base_url?: string;
|
|
154
|
+
api_key?: string;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** A search backend that answers over an API contract instead of scraped HTML. */
|
|
159
|
+
export type KeyedSearchProvider = "brave" | "tavily" | "searxng";
|
|
160
|
+
export interface WebSearchCredentialStatus {
|
|
161
|
+
/** A keyed backend is reachable, so search does not depend on scraped DuckDuckGo. */
|
|
162
|
+
configured: boolean;
|
|
163
|
+
/** Which backend the credential belongs to, when one is configured. */
|
|
164
|
+
provider?: KeyedSearchProvider;
|
|
165
|
+
/** Where the credential came from — env wins over the settings file. */
|
|
166
|
+
source?: "env" | "settings";
|
|
167
|
+
/** The user explicitly asked for the keyless backend, so nothing is missing. */
|
|
168
|
+
explicitKeyless?: boolean;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Whether `websearch` has a keyed backend configured, and where it came from.
|
|
172
|
+
*
|
|
173
|
+
* Mirrors the binary's own resolution order (env over settings file) for the
|
|
174
|
+
* three keyed backends. This is a read-only check used to decide whether to
|
|
175
|
+
* tell the user that search is running on keyless DuckDuckGo — it never
|
|
176
|
+
* returns the credential itself, so a key cannot leak into the UI or a log.
|
|
177
|
+
*
|
|
178
|
+
* A provider pinned to `duckduckgo` (env or settings) is reported as
|
|
179
|
+
* `explicitKeyless`: the user chose the scraped backend, so nothing is missing.
|
|
180
|
+
*/
|
|
181
|
+
export declare function resolveWebSearchCredentials(search?: WebtoolsSearchSettings): WebSearchCredentialStatus;
|
|
101
182
|
/**
|
|
102
183
|
* Run a `webtools` subcommand with `--json` and return parsed stdout.
|
|
103
184
|
*
|