@mgsoftwarebv/mg-dashboard-mcp 2.4.3 → 2.5.1
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/dist/index.js +378 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -413,6 +413,9 @@ var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
|
|
|
413
413
|
"agent_flagged",
|
|
414
414
|
"human_approved"
|
|
415
415
|
]);
|
|
416
|
+
function normalizeCompanyName(name) {
|
|
417
|
+
return name.toLowerCase().replace(/\b(b\.?v\.?|n\.?v\.?|v\.?o\.?f\.?|c\.?v\.?|holding|groep|group|nederland|netherlands)\b/gi, "").replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
|
|
418
|
+
}
|
|
416
419
|
var AGENT_TOOLS = [
|
|
417
420
|
{
|
|
418
421
|
name: "agent-report-coverage",
|
|
@@ -632,6 +635,103 @@ var AGENT_TOOLS = [
|
|
|
632
635
|
},
|
|
633
636
|
required: ["repo_slug", "results"]
|
|
634
637
|
}
|
|
638
|
+
},
|
|
639
|
+
// -- Lead Generation tools --------------------------------------------------
|
|
640
|
+
{
|
|
641
|
+
name: "agent-check-lead-exists",
|
|
642
|
+
description: "Check if a lead already exists by website URL or company name. Call this BEFORE visiting a website to avoid wasting time on duplicates.",
|
|
643
|
+
inputSchema: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: {
|
|
646
|
+
website_url: {
|
|
647
|
+
type: "string",
|
|
648
|
+
description: 'Website URL to check (e.g. "https://example.nl")'
|
|
649
|
+
},
|
|
650
|
+
company_name: {
|
|
651
|
+
type: "string",
|
|
652
|
+
description: "Company name to fuzzy-match against existing leads"
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
required: []
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "agent-save-lead",
|
|
660
|
+
description: "Save a discovered lead (company) to the database. Handles dedup on website_url + fuzzy match on company name. Returns the lead ID and whether it already existed.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
company_name: { type: "string", description: "Company name" },
|
|
665
|
+
website_url: { type: "string", description: "Company website URL" },
|
|
666
|
+
industry: { type: "string", description: "Industry sector" },
|
|
667
|
+
region: { type: "string", description: 'Geographic region (e.g. "amsterdam", "rotterdam")' },
|
|
668
|
+
description: { type: "string", description: "AI-generated summary of what the company does (max 2000 chars)" },
|
|
669
|
+
potential_fit: { type: "string", description: "Why MG Software could help this company (max 2000 chars)" },
|
|
670
|
+
fit_score: { type: "number", description: "Fit score 1-10 for MG Software partnership" },
|
|
671
|
+
estimated_company_size: { type: "string", description: 'Estimated employee count range (e.g. "1-10", "10-50", "50-200")' },
|
|
672
|
+
kvk_number: { type: "string", description: "KvK (Chamber of Commerce) number if found" },
|
|
673
|
+
contact_name: { type: "string", description: "Primary contact person name" },
|
|
674
|
+
contact_role: { type: "string", description: "Contact person role/title" },
|
|
675
|
+
contact_email: { type: "string", description: "Contact person email" },
|
|
676
|
+
contact_phone: { type: "string", description: "Contact person phone" },
|
|
677
|
+
contact_linkedin: { type: "string", description: "Contact person LinkedIn URL" },
|
|
678
|
+
general_email: { type: "string", description: "General company email (info@...)" },
|
|
679
|
+
general_phone: { type: "string", description: "General company phone number" },
|
|
680
|
+
source_url: { type: "string", description: "URL where this lead was found (Google result, directory, etc.)" },
|
|
681
|
+
target_id: { type: "string", description: "UUID of the lead_generation_target this lead was found for" }
|
|
682
|
+
},
|
|
683
|
+
required: ["company_name", "website_url"]
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: "agent-save-email-draft",
|
|
688
|
+
description: "Save a cold email draft for a lead. The email will be shown in the backoffice for manual review and copy. Do NOT use em-dashes or en-dashes in the email.",
|
|
689
|
+
inputSchema: {
|
|
690
|
+
type: "object",
|
|
691
|
+
properties: {
|
|
692
|
+
lead_id: { type: "string", description: "UUID of the lead this email is for" },
|
|
693
|
+
subject: { type: "string", description: "Email subject line (Dutch, max 200 chars)" },
|
|
694
|
+
body: { type: "string", description: "Email body text (Dutch, max 5000 chars, NO em-dashes or en-dashes)" },
|
|
695
|
+
tone: { type: "string", description: "Tone of the email (default: professional)" }
|
|
696
|
+
},
|
|
697
|
+
required: ["lead_id", "subject", "body"]
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
name: "agent-complete-target",
|
|
702
|
+
description: "Mark a lead_generation_target as completed with the number of leads found.",
|
|
703
|
+
inputSchema: {
|
|
704
|
+
type: "object",
|
|
705
|
+
properties: {
|
|
706
|
+
target_id: { type: "string", description: "UUID of the lead_generation_target" },
|
|
707
|
+
results_count: { type: "number", description: "Number of new leads saved for this target" }
|
|
708
|
+
},
|
|
709
|
+
required: ["target_id", "results_count"]
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: "web-search",
|
|
714
|
+
description: "Search the web using DuckDuckGo. Returns a list of results with title, URL, and snippet. Use this to find companies, websites, directories, etc.",
|
|
715
|
+
inputSchema: {
|
|
716
|
+
type: "object",
|
|
717
|
+
properties: {
|
|
718
|
+
query: { type: "string", description: 'Search query (e.g. "logistiek bedrijf MKB nederland")' },
|
|
719
|
+
max_results: { type: "number", description: "Max results to return (default: 15, max: 30)" }
|
|
720
|
+
},
|
|
721
|
+
required: ["query"]
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "web-fetch",
|
|
726
|
+
description: "Fetch a web page and return its text content (HTML tags stripped). Use this to read company websites, extract contact info, about pages, etc. Returns at most 15000 characters of cleaned text.",
|
|
727
|
+
inputSchema: {
|
|
728
|
+
type: "object",
|
|
729
|
+
properties: {
|
|
730
|
+
url: { type: "string", description: 'Full URL to fetch (e.g. "https://example.nl/contact")' },
|
|
731
|
+
extract_links: { type: "boolean", description: "Also extract all links from the page (default: false)" }
|
|
732
|
+
},
|
|
733
|
+
required: ["url"]
|
|
734
|
+
}
|
|
635
735
|
}
|
|
636
736
|
];
|
|
637
737
|
var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
|
|
@@ -641,7 +741,13 @@ var AGENT_TOOL_MODULE_MAP = {
|
|
|
641
741
|
"agent-save-documentation": "agent_reporting",
|
|
642
742
|
"agent-list-findings": "agent_reporting",
|
|
643
743
|
"agent-get-documentation": "agent_reporting",
|
|
644
|
-
"agent-validate-suggestions": "agent_reporting"
|
|
744
|
+
"agent-validate-suggestions": "agent_reporting",
|
|
745
|
+
"agent-check-lead-exists": "agent_reporting",
|
|
746
|
+
"agent-save-lead": "agent_reporting",
|
|
747
|
+
"agent-save-email-draft": "agent_reporting",
|
|
748
|
+
"agent-complete-target": "agent_reporting",
|
|
749
|
+
"web-search": "agent_reporting",
|
|
750
|
+
"web-fetch": "agent_reporting"
|
|
645
751
|
};
|
|
646
752
|
function clamp(val, min, max) {
|
|
647
753
|
return Math.max(min, Math.min(max, val));
|
|
@@ -649,6 +755,80 @@ function clamp(val, min, max) {
|
|
|
649
755
|
function sanitizeString(val, maxLen) {
|
|
650
756
|
return String(val ?? "").slice(0, maxLen);
|
|
651
757
|
}
|
|
758
|
+
var WEB_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
759
|
+
async function webSearch(query, maxResults) {
|
|
760
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
761
|
+
const response = await fetch(url, {
|
|
762
|
+
headers: {
|
|
763
|
+
"User-Agent": WEB_USER_AGENT,
|
|
764
|
+
"Accept": "text/html",
|
|
765
|
+
"Accept-Language": "nl,en;q=0.9"
|
|
766
|
+
},
|
|
767
|
+
redirect: "follow"
|
|
768
|
+
});
|
|
769
|
+
if (!response.ok) throw new Error(`Search failed: HTTP ${response.status}`);
|
|
770
|
+
const html = await response.text();
|
|
771
|
+
const results = [];
|
|
772
|
+
const resultBlocks = html.split(/class="result\s/);
|
|
773
|
+
for (let i = 1; i < resultBlocks.length && results.length < maxResults; i++) {
|
|
774
|
+
const block = resultBlocks[i];
|
|
775
|
+
const titleMatch = block.match(/class="result__a"[^>]*>([^<]+)</);
|
|
776
|
+
const hrefMatch = block.match(/class="result__a"\s+href="([^"]+)"/);
|
|
777
|
+
const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
|
|
778
|
+
if (!hrefMatch) continue;
|
|
779
|
+
let href = hrefMatch[1];
|
|
780
|
+
if (href.includes("uddg=")) {
|
|
781
|
+
const uddg = new URL(href, "https://duckduckgo.com").searchParams.get("uddg");
|
|
782
|
+
if (uddg) href = uddg;
|
|
783
|
+
}
|
|
784
|
+
if (!href.startsWith("http")) continue;
|
|
785
|
+
const title = titleMatch ? titleMatch[1].replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/g, '"').trim() : href;
|
|
786
|
+
const snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/g, '"').replace(/\s+/g, " ").trim() : "";
|
|
787
|
+
results.push({ title, url: href, snippet });
|
|
788
|
+
}
|
|
789
|
+
return results;
|
|
790
|
+
}
|
|
791
|
+
async function webFetch(url, extractLinks) {
|
|
792
|
+
const controller = new AbortController();
|
|
793
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
794
|
+
try {
|
|
795
|
+
const response = await fetch(url, {
|
|
796
|
+
headers: {
|
|
797
|
+
"User-Agent": WEB_USER_AGENT,
|
|
798
|
+
"Accept": "text/html,application/xhtml+xml",
|
|
799
|
+
"Accept-Language": "nl,en;q=0.9"
|
|
800
|
+
},
|
|
801
|
+
redirect: "follow",
|
|
802
|
+
signal: controller.signal
|
|
803
|
+
});
|
|
804
|
+
if (!response.ok) throw new Error(`Fetch failed: HTTP ${response.status}`);
|
|
805
|
+
const contentType = response.headers.get("content-type") || "";
|
|
806
|
+
if (!contentType.includes("text/html") && !contentType.includes("text/plain") && !contentType.includes("application/xhtml")) {
|
|
807
|
+
throw new Error(`Unsupported content type: ${contentType}. Only HTML/text pages supported.`);
|
|
808
|
+
}
|
|
809
|
+
const html = await response.text();
|
|
810
|
+
const links = [];
|
|
811
|
+
if (extractLinks) {
|
|
812
|
+
const linkRegex = /<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
813
|
+
let match;
|
|
814
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
815
|
+
const href = match[1];
|
|
816
|
+
const text2 = match[2].replace(/<[^>]+>/g, "").trim();
|
|
817
|
+
if (href && href.startsWith("http")) {
|
|
818
|
+
links.push({ href, text: text2.slice(0, 100) });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, " [NAV] ").replace(/<header[\s\S]*?<\/header>/gi, " [HEADER] ").replace(/<footer[\s\S]*?<\/footer>/gi, " [FOOTER] ").replace(/<(br|hr)\s*\/?>/gi, "\n").replace(/<\/(p|div|h[1-6]|li|tr|section|article)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/g, '"').replace(/[ \t]+/g, " ").replace(/\n\s*\n/g, "\n").trim();
|
|
823
|
+
const MAX_TEXT = 15e3;
|
|
824
|
+
if (text.length > MAX_TEXT) {
|
|
825
|
+
text = text.slice(0, MAX_TEXT) + "\n\n[... truncated at 15000 chars ...]";
|
|
826
|
+
}
|
|
827
|
+
return { text, links };
|
|
828
|
+
} finally {
|
|
829
|
+
clearTimeout(timeout);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
652
832
|
function textSimilarity(a, b) {
|
|
653
833
|
if (a === b) return 1;
|
|
654
834
|
const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
@@ -996,6 +1176,196 @@ ${output}` }]
|
|
|
996
1176
|
};
|
|
997
1177
|
}
|
|
998
1178
|
// -----------------------------------------------------------------
|
|
1179
|
+
// Lead Generation tools
|
|
1180
|
+
// -----------------------------------------------------------------
|
|
1181
|
+
case "agent-check-lead-exists": {
|
|
1182
|
+
const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
|
|
1183
|
+
const companyName = sanitizeString(args2.company_name, 500);
|
|
1184
|
+
if (!websiteUrl && !companyName) {
|
|
1185
|
+
throw new Error("At least one of website_url or company_name is required");
|
|
1186
|
+
}
|
|
1187
|
+
const checks = [];
|
|
1188
|
+
if (websiteUrl) {
|
|
1189
|
+
const { data: urlMatch } = await supabase2.from("lead").select("id, company_name, website_url").eq("website_url", websiteUrl).maybeSingle();
|
|
1190
|
+
if (urlMatch) {
|
|
1191
|
+
return {
|
|
1192
|
+
content: [{
|
|
1193
|
+
type: "text",
|
|
1194
|
+
text: `DUPLICATE: Lead already exists (URL match). ID: ${urlMatch.id}, Company: ${urlMatch.company_name}. SKIP this company.`
|
|
1195
|
+
}]
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
checks.push(`URL "${websiteUrl}" not found`);
|
|
1199
|
+
}
|
|
1200
|
+
if (companyName) {
|
|
1201
|
+
const normalized = normalizeCompanyName(companyName);
|
|
1202
|
+
if (normalized.length >= 3) {
|
|
1203
|
+
const { data: nameMatches } = await supabase2.from("lead").select("id, company_name, website_url").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
|
|
1204
|
+
if (nameMatches) {
|
|
1205
|
+
for (const match of nameMatches) {
|
|
1206
|
+
const matchNorm = normalizeCompanyName(match.company_name);
|
|
1207
|
+
if (textSimilarity(normalized, matchNorm) > 0.6) {
|
|
1208
|
+
return {
|
|
1209
|
+
content: [{
|
|
1210
|
+
type: "text",
|
|
1211
|
+
text: `DUPLICATE: Similar company found. ID: ${match.id}, Name: "${match.company_name}" (${match.website_url}). SKIP this company.`
|
|
1212
|
+
}]
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
checks.push(`Name "${companyName}" has no similar matches`);
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
content: [{
|
|
1222
|
+
type: "text",
|
|
1223
|
+
text: `NOT FOUND: No duplicate detected. ${checks.join(". ")}. Safe to proceed.`
|
|
1224
|
+
}]
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
case "agent-save-lead": {
|
|
1228
|
+
const companyName = sanitizeString(args2.company_name, 500);
|
|
1229
|
+
const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
|
|
1230
|
+
if (!companyName) throw new Error("company_name is required");
|
|
1231
|
+
if (!websiteUrl) throw new Error("website_url is required");
|
|
1232
|
+
const { data: existing } = await supabase2.from("lead").select("id, company_name").eq("website_url", websiteUrl).maybeSingle();
|
|
1233
|
+
if (existing) {
|
|
1234
|
+
return {
|
|
1235
|
+
content: [{
|
|
1236
|
+
type: "text",
|
|
1237
|
+
text: `Lead already exists (URL match). ID: ${existing.id}, Company: "${existing.company_name}". Skipped.`
|
|
1238
|
+
}]
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const normalized = normalizeCompanyName(companyName);
|
|
1242
|
+
if (normalized.length >= 3) {
|
|
1243
|
+
const { data: nameMatches } = await supabase2.from("lead").select("id, company_name").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
|
|
1244
|
+
if (nameMatches) {
|
|
1245
|
+
for (const match of nameMatches) {
|
|
1246
|
+
if (textSimilarity(normalized, normalizeCompanyName(match.company_name)) > 0.6) {
|
|
1247
|
+
return {
|
|
1248
|
+
content: [{
|
|
1249
|
+
type: "text",
|
|
1250
|
+
text: `Lead already exists (name match). ID: ${match.id}, Name: "${match.company_name}". Skipped.`
|
|
1251
|
+
}]
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const fitScore = args2.fit_score ? clamp(Number(args2.fit_score), 1, 10) : null;
|
|
1258
|
+
const { data: inserted, error } = await supabase2.from("lead").insert({
|
|
1259
|
+
company_name: companyName,
|
|
1260
|
+
website_url: websiteUrl,
|
|
1261
|
+
industry: args2.industry ? sanitizeString(args2.industry, 200) : null,
|
|
1262
|
+
region: args2.region ? sanitizeString(args2.region, 200) : null,
|
|
1263
|
+
description: args2.description ? sanitizeString(args2.description, 2e3) : null,
|
|
1264
|
+
potential_fit: args2.potential_fit ? sanitizeString(args2.potential_fit, 2e3) : null,
|
|
1265
|
+
fit_score: fitScore,
|
|
1266
|
+
estimated_company_size: args2.estimated_company_size ? sanitizeString(args2.estimated_company_size, 50) : null,
|
|
1267
|
+
kvk_number: args2.kvk_number ? sanitizeString(args2.kvk_number, 20) : null,
|
|
1268
|
+
contact_name: args2.contact_name ? sanitizeString(args2.contact_name, 200) : null,
|
|
1269
|
+
contact_role: args2.contact_role ? sanitizeString(args2.contact_role, 200) : null,
|
|
1270
|
+
contact_email: args2.contact_email ? sanitizeString(args2.contact_email, 200) : null,
|
|
1271
|
+
contact_phone: args2.contact_phone ? sanitizeString(args2.contact_phone, 50) : null,
|
|
1272
|
+
contact_linkedin: args2.contact_linkedin ? sanitizeString(args2.contact_linkedin, 500) : null,
|
|
1273
|
+
general_email: args2.general_email ? sanitizeString(args2.general_email, 200) : null,
|
|
1274
|
+
general_phone: args2.general_phone ? sanitizeString(args2.general_phone, 50) : null,
|
|
1275
|
+
source_url: args2.source_url ? sanitizeString(args2.source_url, 500) : null,
|
|
1276
|
+
target_id: args2.target_id ? sanitizeString(args2.target_id, 100) : null,
|
|
1277
|
+
status: "new"
|
|
1278
|
+
}).select("id").single();
|
|
1279
|
+
if (error) throw new Error(`Failed to save lead: ${error.message}`);
|
|
1280
|
+
return {
|
|
1281
|
+
content: [{
|
|
1282
|
+
type: "text",
|
|
1283
|
+
text: `Lead saved: "${companyName}" (${websiteUrl}). ID: ${inserted.id}. Fit score: ${fitScore ?? "N/A"}.`
|
|
1284
|
+
}]
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
case "agent-save-email-draft": {
|
|
1288
|
+
const leadId = sanitizeString(args2.lead_id, 100);
|
|
1289
|
+
const subject = sanitizeString(args2.subject, 200);
|
|
1290
|
+
let body = sanitizeString(args2.body, 5e3);
|
|
1291
|
+
const tone = sanitizeString(args2.tone, 50) || "professional";
|
|
1292
|
+
if (!leadId) throw new Error("lead_id is required");
|
|
1293
|
+
if (!subject) throw new Error("subject is required");
|
|
1294
|
+
if (!body) throw new Error("body is required");
|
|
1295
|
+
body = body.replace(/[\u2013\u2014]/g, ",");
|
|
1296
|
+
const { data: inserted, error } = await supabase2.from("lead_email_draft").insert({ lead_id: leadId, subject, body, tone, status: "draft" }).select("id").single();
|
|
1297
|
+
if (error) throw new Error(`Failed to save email draft: ${error.message}`);
|
|
1298
|
+
await supabase2.from("lead").update({ status: "email_drafted", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", leadId).eq("status", "new");
|
|
1299
|
+
return {
|
|
1300
|
+
content: [{
|
|
1301
|
+
type: "text",
|
|
1302
|
+
text: `Email draft saved for lead ${leadId}. Draft ID: ${inserted.id}. Subject: "${subject}".`
|
|
1303
|
+
}]
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
case "agent-complete-target": {
|
|
1307
|
+
const targetId = sanitizeString(args2.target_id, 100);
|
|
1308
|
+
const resultsCount = clamp(Number(args2.results_count) || 0, 0, 9999);
|
|
1309
|
+
if (!targetId) throw new Error("target_id is required");
|
|
1310
|
+
const { error } = await supabase2.from("lead_generation_target").update({
|
|
1311
|
+
status: "completed",
|
|
1312
|
+
results_count: resultsCount,
|
|
1313
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1314
|
+
}).eq("id", targetId);
|
|
1315
|
+
if (error) throw new Error(`Failed to complete target: ${error.message}`);
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{
|
|
1318
|
+
type: "text",
|
|
1319
|
+
text: `Target ${targetId} marked completed. ${resultsCount} leads found.`
|
|
1320
|
+
}]
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
// -----------------------------------------------------------------
|
|
1324
|
+
// Web Tools
|
|
1325
|
+
// -----------------------------------------------------------------
|
|
1326
|
+
case "web-search": {
|
|
1327
|
+
const query = sanitizeString(args2.query, 500);
|
|
1328
|
+
if (!query) throw new Error("query is required");
|
|
1329
|
+
const maxResults = clamp(Number(args2.max_results) || 15, 1, 30);
|
|
1330
|
+
const results = await webSearch(query, maxResults);
|
|
1331
|
+
if (results.length === 0) {
|
|
1332
|
+
return { content: [{ type: "text", text: `No search results found for: "${query}"` }] };
|
|
1333
|
+
}
|
|
1334
|
+
const formatted = results.map(
|
|
1335
|
+
(r, i) => `${i + 1}. ${r.title}
|
|
1336
|
+
URL: ${r.url}
|
|
1337
|
+
${r.snippet}`
|
|
1338
|
+
).join("\n\n");
|
|
1339
|
+
return {
|
|
1340
|
+
content: [{
|
|
1341
|
+
type: "text",
|
|
1342
|
+
text: `Search results for "${query}" (${results.length} results):
|
|
1343
|
+
|
|
1344
|
+
${formatted}`
|
|
1345
|
+
}]
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
case "web-fetch": {
|
|
1349
|
+
const url = sanitizeString(args2.url, 2e3);
|
|
1350
|
+
if (!url) throw new Error("url is required");
|
|
1351
|
+
if (!/^https?:\/\//i.test(url)) throw new Error("url must start with http:// or https://");
|
|
1352
|
+
const extractLinks = Boolean(args2.extract_links);
|
|
1353
|
+
const result = await webFetch(url, extractLinks);
|
|
1354
|
+
let text = `Content from ${url} (${result.text.length} chars):
|
|
1355
|
+
|
|
1356
|
+
${result.text}`;
|
|
1357
|
+
if (extractLinks && result.links.length > 0) {
|
|
1358
|
+
const linkList = result.links.slice(0, 50).map(
|
|
1359
|
+
(l) => ` ${l.text ? l.text + ": " : ""}${l.href}`
|
|
1360
|
+
).join("\n");
|
|
1361
|
+
text += `
|
|
1362
|
+
|
|
1363
|
+
--- Links (${result.links.length} found, showing max 50) ---
|
|
1364
|
+
${linkList}`;
|
|
1365
|
+
}
|
|
1366
|
+
return { content: [{ type: "text", text }] };
|
|
1367
|
+
}
|
|
1368
|
+
// -----------------------------------------------------------------
|
|
999
1369
|
default:
|
|
1000
1370
|
return { content: [{ type: "text", text: `Unknown agent tool: ${name}` }] };
|
|
1001
1371
|
}
|
|
@@ -2787,7 +3157,13 @@ async function main() {
|
|
|
2787
3157
|
"/api/save-documentation": "agent-save-documentation",
|
|
2788
3158
|
"/api/list-findings": "agent-list-findings",
|
|
2789
3159
|
"/api/get-documentation": "agent-get-documentation",
|
|
2790
|
-
"/api/validate-suggestions": "agent-validate-suggestions"
|
|
3160
|
+
"/api/validate-suggestions": "agent-validate-suggestions",
|
|
3161
|
+
"/api/check-lead-exists": "agent-check-lead-exists",
|
|
3162
|
+
"/api/save-lead": "agent-save-lead",
|
|
3163
|
+
"/api/save-email-draft": "agent-save-email-draft",
|
|
3164
|
+
"/api/complete-target": "agent-complete-target",
|
|
3165
|
+
"/api/web-search": "web-search",
|
|
3166
|
+
"/api/web-fetch": "web-fetch"
|
|
2791
3167
|
};
|
|
2792
3168
|
const httpServer = createServer(async (req, res) => {
|
|
2793
3169
|
const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);
|