src / tools / crawlSpecificPage.ts
src / tools / crawlSpecificPage.ts
import { tool } from "@lmstudio/sdk";
import { z } from "zod";
import { crawlPage } from "../services/crawlPage";
import { normalizeUrl } from "../utils/url";
export const crawlSpecificPageTool = tool({
name: "crawlSpecificPage",
description: "Crawl a webpage. Returns optimized text content with precise contextual inline links.",
parameters: {
url: z.string().describe("The absolute URL of the webpage to crawl"),
include: z.array(z.enum(["content", "rawHtml", "links", "infoboxes", "recentUpdates"])).optional().describe("Explicit data types to extract."),
},
implementation: async (input: {
url: string;
include?: string[];
}) => {
try {
const rawUrl = input.url;
if (!rawUrl) return "Error: url is required";
const normalizedUrl = normalizeUrl(rawUrl);
// crawlPage handles blocked domains/extensions internally via isNoiseUrl — no need to pre-check
const result = await crawlPage(normalizedUrl, "");
if (!result.success) return `Crawl failed: ${result.error}`;
const include = input.include ?? ["content"];
const output: string[] = [];
// Single pass over structuredContent for infoboxes/recentUpdates filtering
let metadataSection: string | null = null;
let updateSections: string[] = [];
if (include.includes("infoboxes") || include.includes("recentUpdates")) {
for (const section of result.structuredContent) {
if (section.includes("## Page Metadata")) {
metadataSection = section;
} else if (/update|news|change|patch|version|release/i.test(section)) {
updateSections.push(section);
}
}
}
// Structured content (includes infoboxes when requested)
if ((include.includes("content") || include.includes("links")) && result.structuredContent.length > 0) {
output.push("## Structured Content");
for (const section of result.structuredContent) {
output.push(section);
}
}
// Raw text content — only as fallback when no structured sections exist
if (include.includes("content") && !output.some(s => s.startsWith("## Structured Content"))) {
const raw = result.content.slice(0, 6000).trim();
if (raw) {
output.push("## Content");
output.push(raw);
}
}
// Links
if (include.includes("links")) {
const links = result.links.map(link => {
try {
const parsed = new URL(link);
const anchor = parsed.hash ? parsed.hash.slice(1) : null;
return `- [${anchor ? `${parsed.pathname}#${anchor}` : parsed.pathname}](${link})`;
} catch {
return `- ${link}`;
}
});
if (links.length > 0) {
output.push("## Links");
output.push(links.join("\n"));
}
}
// Infoboxes (extracted in single pass above)
if (include.includes("infoboxes") && metadataSection) {
output.push(metadataSection);
}
// Recent updates (extracted in single pass above)
if (include.includes("recentUpdates") && updateSections.length > 0) {
output.push("## Recent Updates");
output.push(updateSections.join("\n\n"));
}
// rawHtml: use the already-fetched HTML from crawlPage result
if (include.includes("rawHtml") && result.rawHtml) {
output.push("## Raw HTML");
output.push(result.rawHtml.slice(0, 500_000));
}
return output.length ? output.join("\n\n") : "No content found.";
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
},
});
import { tool } from "@lmstudio/sdk";
import { z } from "zod";
import { crawlPage } from "../services/crawlPage";
import { normalizeUrl } from "../utils/url";
export const crawlSpecificPageTool = tool({
name: "crawlSpecificPage",
description: "Crawl a webpage. Returns optimized text content with precise contextual inline links.",
parameters: {
url: z.string().describe("The absolute URL of the webpage to crawl"),
include: z.array(z.enum(["content", "rawHtml", "links", "infoboxes", "recentUpdates"])).optional().describe("Explicit data types to extract."),
},
implementation: async (input: {
url: string;
include?: string[];
}) => {
try {
const rawUrl = input.url;
if (!rawUrl) return "Error: url is required";
const normalizedUrl = normalizeUrl(rawUrl);
// crawlPage handles blocked domains/extensions internally via isNoiseUrl — no need to pre-check
const result = await crawlPage(normalizedUrl, "");
if (!result.success) return `Crawl failed: ${result.error}`;
const include = input.include ?? ["content"];
const output: string[] = [];
// Single pass over structuredContent for infoboxes/recentUpdates filtering
let metadataSection: string | null = null;
let updateSections: string[] = [];
if (include.includes("infoboxes") || include.includes("recentUpdates")) {
for (const section of result.structuredContent) {
if (section.includes("## Page Metadata")) {
metadataSection = section;
} else if (/update|news|change|patch|version|release/i.test(section)) {
updateSections.push(section);
}
}
}
// Structured content (includes infoboxes when requested)
if ((include.includes("content") || include.includes("links")) && result.structuredContent.length > 0) {
output.push("## Structured Content");
for (const section of result.structuredContent) {
output.push(section);
}
}
// Raw text content — only as fallback when no structured sections exist
if (include.includes("content") && !output.some(s => s.startsWith("## Structured Content"))) {
const raw = result.content.slice(0, 6000).trim();
if (raw) {
output.push("## Content");
output.push(raw);
}
}
// Links
if (include.includes("links")) {
const links = result.links.map(link => {
try {
const parsed = new URL(link);
const anchor = parsed.hash ? parsed.hash.slice(1) : null;
return `- [${anchor ? `${parsed.pathname}#${anchor}` : parsed.pathname}](${link})`;
} catch {
return `- ${link}`;
}
});
if (links.length > 0) {
output.push("## Links");
output.push(links.join("\n"));
}
}
// Infoboxes (extracted in single pass above)
if (include.includes("infoboxes") && metadataSection) {
output.push(metadataSection);
}
// Recent updates (extracted in single pass above)
if (include.includes("recentUpdates") && updateSections.length > 0) {
output.push("## Recent Updates");
output.push(updateSections.join("\n\n"));
}
// rawHtml: use the already-fetched HTML from crawlPage result
if (include.includes("rawHtml") && result.rawHtml) {
output.push("## Raw HTML");
output.push(result.rawHtml.slice(0, 500_000));
}
return output.length ? output.join("\n\n") : "No content found.";
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Error: ${message}`;
}
},
});