aboutsummaryrefslogtreecommitdiff
path: root/castle-search/src/index.ts
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-06 03:33:30 +0000
committerIgor Soarez <igor@soarez.org>2026-11-06 03:33:30 +0000
commit561f0ee076c684a79fd5671af1bad0894f33e4de (patch)
tree7849677412a8b5f80835fa812f2e255f222a2c82 /castle-search/src/index.ts
parent147b3b1507ebcdf5912fbd4c474c9dfb35265148 (diff)
castle-search: resolve Google links lazily, in Chrome, not eagerly over plain HTTPHEADmaster
The Node-side resolver sent one curl-like request per result with no cookies, JS or browser; Google answers that with its 'Verifying your request' countdown after a handful, and the testing itself triggered it. A click or direct navigation in Chrome redirects at once (measured). Search now leaves Google links wrapped and prints the breadcrumb Google shows for each. castle_cdp_resolve follows one wrapped link in a Chrome tab when the agent picks it, accepts only google.com/goto?url=<token>, and reports the verification page instead of waiting it out.
Diffstat (limited to 'castle-search/src/index.ts')
-rw-r--r--castle-search/src/index.ts37
1 files changed, 36 insertions, 1 deletions
diff --git a/castle-search/src/index.ts b/castle-search/src/index.ts
index 29cdaaf..9b7c143 100644
--- a/castle-search/src/index.ts
+++ b/castle-search/src/index.ts
@@ -43,6 +43,7 @@ import {
describeConfiguredEndpoint,
describeConfiguredEngine,
disconnect,
+ resolveLink,
search,
} from "./search.ts";
@@ -202,6 +203,38 @@ export default function (pi: ExtensionAPI): void {
});
pi.registerTool({
+ name: "castle_cdp_resolve",
+ label: "Castle Resolve",
+ description:
+ "Turn one Google-wrapped result link (https://www.google.com/goto?url=…, as printed by castle_cdp_search " +
+ "with engine google) into the real destination URL, by following it in the same Chrome. Opens one tab, " +
+ "reads where it lands, closes it. It does not read the page. Resolve only the results you mean to open: " +
+ "Google answers a burst of links with a \"Verifying your request\" countdown, which this tool reports " +
+ "instead of waiting out. Other engines already return real URLs and need no resolving.",
+ promptSnippet: "Resolve one Google-wrapped result link to its real URL (castle_cdp_resolve)",
+ promptGuidelines: [
+ "castle_cdp_search on Google returns wrapped links. Read the titles, snippets and the breadcrumb Google " +
+ "shows for each, decide which result you actually need, and resolve only that one with castle_cdp_resolve. " +
+ "Do not resolve every result: that burst is what triggers Google's verification page.",
+ ],
+ parameters: Type.Object({
+ url: Type.String({
+ description: "A https://www.google.com/goto?url=… link exactly as castle_cdp_search printed it.",
+ minLength: 1,
+ }),
+ }),
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
+ const timeout = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
+ const out = await resolveLink(params.url.trim(), combined);
+ return {
+ content: [{ type: "text", text: `${out.to}\n(resolved via Chrome at ${out.endpoint})` }],
+ details: { from: out.from, to: out.to, endpoint: out.endpoint },
+ };
+ },
+ });
+
+ pi.registerTool({
name: "castle_cdp_search",
label: "Castle Search",
description:
@@ -209,7 +242,9 @@ export default function (pi: ExtensionAPI): void {
"DevTools Protocol. Returns ranked results with titles, URLs and snippets. Because it uses a real " +
"logged-in browser rather than a search API, it reaches pages that block datacentre traffic. It " +
"returns search results only — it does not fetch or read the linked pages, so follow up with a " +
- "fetch/read tool for full page content. Supports four engines with independent indexes and " +
+ "fetch/read tool for full page content. Google results come back as wrapped links with the " +
+ "breadcrumb Google shows; resolve the ones you need with castle_cdp_resolve. " +
+ "Supports four engines with independent indexes and " +
`independent rate limits (${ENGINE_IDS.join(", ")}); pass engine to switch when one is blocked. ` +
`Output is truncated at ${formatSize(DEFAULT_MAX_BYTES)} or ${DEFAULT_MAX_LINES} lines.`,
promptSnippet: