@mgsoftwarebv/mg-dashboard-mcp 2.6.2 → 2.7.0
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 +268 -1028
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -376,339 +376,13 @@ ${rawJson.substring(0, 500)}`
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
// src/agent-tools.ts
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
"
|
|
384
|
-
"improvement",
|
|
385
|
-
"n_plus_1_query",
|
|
386
|
-
"bundle_size",
|
|
387
|
-
"unnecessary_rerender",
|
|
388
|
-
"slow_endpoint",
|
|
389
|
-
"memory_leak",
|
|
390
|
-
"missing_memoization",
|
|
391
|
-
"large_dependency",
|
|
392
|
-
"dead_code",
|
|
393
|
-
"unused_dependency",
|
|
394
|
-
"env_leak",
|
|
395
|
-
"i18n_missing_key",
|
|
396
|
-
"i18n_unused_key"
|
|
397
|
-
]);
|
|
398
|
-
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["info", "warning", "critical"]);
|
|
399
|
-
var VALID_SCOPES = /* @__PURE__ */ new Set([
|
|
400
|
-
"architecture",
|
|
401
|
-
"module",
|
|
402
|
-
"component",
|
|
403
|
-
"api",
|
|
404
|
-
"package",
|
|
405
|
-
"security",
|
|
406
|
-
"server_audit",
|
|
407
|
-
"changelog",
|
|
408
|
-
"api-reference"
|
|
409
|
-
]);
|
|
410
|
-
var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
|
|
411
|
-
"pending",
|
|
412
|
-
"agent_approved",
|
|
413
|
-
"agent_flagged",
|
|
414
|
-
"human_approved"
|
|
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();
|
|
379
|
+
function clamp(val, min, max) {
|
|
380
|
+
return Math.max(min, Math.min(max, val));
|
|
381
|
+
}
|
|
382
|
+
function sanitizeString(val, maxLen) {
|
|
383
|
+
return String(val ?? "").slice(0, maxLen);
|
|
418
384
|
}
|
|
419
385
|
var AGENT_TOOLS = [
|
|
420
|
-
{
|
|
421
|
-
name: "agent-report-coverage",
|
|
422
|
-
description: "Report documentation coverage data for a repository. Upserts records into doc_coverage. Call once per logical unit (package, module, theme, plugin directory). Provide all entries in a single call for efficiency.",
|
|
423
|
-
inputSchema: {
|
|
424
|
-
type: "object",
|
|
425
|
-
properties: {
|
|
426
|
-
repo_slug: {
|
|
427
|
-
type: "string",
|
|
428
|
-
description: 'Repository slug (e.g. "mg-dashboard", "bna-wordpress")'
|
|
429
|
-
},
|
|
430
|
-
refront_project_id: {
|
|
431
|
-
type: "string",
|
|
432
|
-
description: "Refront project UUID linked to this repository"
|
|
433
|
-
},
|
|
434
|
-
entries: {
|
|
435
|
-
type: "array",
|
|
436
|
-
description: "Array of coverage entries, one per logical unit",
|
|
437
|
-
items: {
|
|
438
|
-
type: "object",
|
|
439
|
-
properties: {
|
|
440
|
-
path: { type: "string", description: 'Logical unit path (e.g. "packages/ui", "wp-content/themes/bna")' },
|
|
441
|
-
total_functions: { type: "number", description: "Total public functions/methods" },
|
|
442
|
-
documented_functions: { type: "number", description: "Functions with doc comments" },
|
|
443
|
-
total_types: { type: "number", description: "Total classes/interfaces/types" },
|
|
444
|
-
documented_types: { type: "number", description: "Types with doc comments" },
|
|
445
|
-
total_endpoints: { type: "number", description: "Total API/REST/hook endpoints" },
|
|
446
|
-
documented_endpoints: { type: "number", description: "Endpoints with documentation" }
|
|
447
|
-
},
|
|
448
|
-
required: ["path", "total_functions", "documented_functions"]
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
},
|
|
452
|
-
required: ["repo_slug", "entries"]
|
|
453
|
-
}
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
name: "agent-report-finding",
|
|
457
|
-
description: "Report documentation or performance findings for a repository. Inserts records into doc_suggestion. Auto-creates a parent documentation record if one does not exist yet. Batch multiple findings in one call.",
|
|
458
|
-
inputSchema: {
|
|
459
|
-
type: "object",
|
|
460
|
-
properties: {
|
|
461
|
-
repo_slug: {
|
|
462
|
-
type: "string",
|
|
463
|
-
description: "Repository slug"
|
|
464
|
-
},
|
|
465
|
-
refront_project_id: {
|
|
466
|
-
type: "string",
|
|
467
|
-
description: "Refront project UUID linked to this repository"
|
|
468
|
-
},
|
|
469
|
-
category: {
|
|
470
|
-
type: "string",
|
|
471
|
-
enum: ["scan_findings", "perf_audit"],
|
|
472
|
-
description: 'Finding category: "scan_findings" for doc issues, "perf_audit" for performance issues'
|
|
473
|
-
},
|
|
474
|
-
findings: {
|
|
475
|
-
type: "array",
|
|
476
|
-
description: "Array of findings to report",
|
|
477
|
-
items: {
|
|
478
|
-
type: "object",
|
|
479
|
-
properties: {
|
|
480
|
-
type: {
|
|
481
|
-
type: "string",
|
|
482
|
-
enum: [...VALID_FINDING_TYPES],
|
|
483
|
-
description: "Finding type"
|
|
484
|
-
},
|
|
485
|
-
severity: {
|
|
486
|
-
type: "string",
|
|
487
|
-
enum: ["info", "warning", "critical"],
|
|
488
|
-
description: "Severity level"
|
|
489
|
-
},
|
|
490
|
-
description: {
|
|
491
|
-
type: "string",
|
|
492
|
-
description: "Clear description of the issue (max 2000 chars)"
|
|
493
|
-
},
|
|
494
|
-
file_path: {
|
|
495
|
-
type: "string",
|
|
496
|
-
description: "Relative file path where the issue was found"
|
|
497
|
-
},
|
|
498
|
-
suggested_fix: {
|
|
499
|
-
type: "string",
|
|
500
|
-
description: "Suggested fix with code example (max 5000 chars)"
|
|
501
|
-
}
|
|
502
|
-
},
|
|
503
|
-
required: ["type", "severity", "description"]
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
},
|
|
507
|
-
required: ["repo_slug", "category", "findings"]
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
name: "agent-save-documentation",
|
|
512
|
-
description: "Save or update a documentation record for a repository. Upserts by repo_slug + scope + path combination.",
|
|
513
|
-
inputSchema: {
|
|
514
|
-
type: "object",
|
|
515
|
-
properties: {
|
|
516
|
-
repo_slug: { type: "string", description: "Repository slug" },
|
|
517
|
-
refront_project_id: { type: "string", description: "Refront project UUID (optional for standalone repos)" },
|
|
518
|
-
scope: {
|
|
519
|
-
type: "string",
|
|
520
|
-
enum: [...VALID_SCOPES],
|
|
521
|
-
description: "Documentation scope"
|
|
522
|
-
},
|
|
523
|
-
path: {
|
|
524
|
-
type: "string",
|
|
525
|
-
description: 'Path within the repo (e.g. "packages/ui")'
|
|
526
|
-
},
|
|
527
|
-
title: { type: "string", description: "Document title" },
|
|
528
|
-
content: { type: "string", description: "Documentation content (Typst markup)" },
|
|
529
|
-
review_status: {
|
|
530
|
-
type: "string",
|
|
531
|
-
enum: [...VALID_REVIEW_STATUSES],
|
|
532
|
-
description: 'Review status (default: "pending")'
|
|
533
|
-
}
|
|
534
|
-
},
|
|
535
|
-
required: ["repo_slug", "scope", "path", "title", "content"]
|
|
536
|
-
}
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
name: "agent-list-findings",
|
|
540
|
-
description: "List existing findings (doc_suggestion records) for a repository. Use this to check what has already been reported before submitting new findings.",
|
|
541
|
-
inputSchema: {
|
|
542
|
-
type: "object",
|
|
543
|
-
properties: {
|
|
544
|
-
repo_slug: { type: "string", description: "Repository slug" },
|
|
545
|
-
type: {
|
|
546
|
-
type: "string",
|
|
547
|
-
enum: [...VALID_FINDING_TYPES],
|
|
548
|
-
description: "Filter by finding type"
|
|
549
|
-
},
|
|
550
|
-
severity: {
|
|
551
|
-
type: "string",
|
|
552
|
-
enum: ["info", "warning", "critical"],
|
|
553
|
-
description: "Filter by severity"
|
|
554
|
-
},
|
|
555
|
-
status: {
|
|
556
|
-
type: "string",
|
|
557
|
-
enum: ["open", "ticket_created", "resolved", "dismissed"],
|
|
558
|
-
description: "Filter by status (default: all)"
|
|
559
|
-
},
|
|
560
|
-
limit: {
|
|
561
|
-
type: "number",
|
|
562
|
-
description: "Max results to return (default: 50, max: 200)"
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
|
-
required: ["repo_slug"]
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
name: "agent-get-documentation",
|
|
570
|
-
description: "Retrieve existing documentation records for a repository. Use this to read current docs before generating or reviewing.",
|
|
571
|
-
inputSchema: {
|
|
572
|
-
type: "object",
|
|
573
|
-
properties: {
|
|
574
|
-
repo_slug: { type: "string", description: "Repository slug" },
|
|
575
|
-
scope: {
|
|
576
|
-
type: "string",
|
|
577
|
-
enum: [...VALID_SCOPES],
|
|
578
|
-
description: "Filter by scope"
|
|
579
|
-
},
|
|
580
|
-
path: { type: "string", description: "Filter by exact path" },
|
|
581
|
-
limit: {
|
|
582
|
-
type: "number",
|
|
583
|
-
description: "Max results to return (default: 20, max: 100)"
|
|
584
|
-
}
|
|
585
|
-
},
|
|
586
|
-
required: ["repo_slug"]
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
name: "agent-validate-suggestions",
|
|
591
|
-
description: "Validate existing open suggestions against the actual codebase. For each suggestion, report whether it is resolved (fixed), valid (still open), invalid (should be dismissed), or needs adjustment.",
|
|
592
|
-
inputSchema: {
|
|
593
|
-
type: "object",
|
|
594
|
-
properties: {
|
|
595
|
-
repo_slug: {
|
|
596
|
-
type: "string",
|
|
597
|
-
description: "Repository slug being validated"
|
|
598
|
-
},
|
|
599
|
-
results: {
|
|
600
|
-
type: "array",
|
|
601
|
-
description: "Validation results per suggestion",
|
|
602
|
-
items: {
|
|
603
|
-
type: "object",
|
|
604
|
-
properties: {
|
|
605
|
-
suggestion_id: {
|
|
606
|
-
type: "string",
|
|
607
|
-
description: "UUID of the doc_suggestion being validated"
|
|
608
|
-
},
|
|
609
|
-
verdict: {
|
|
610
|
-
type: "string",
|
|
611
|
-
enum: ["valid", "invalid", "adjusted", "resolved"],
|
|
612
|
-
description: "resolved = fix applied (sets status resolved), valid = still relevant (keep open), invalid = dismiss, adjusted = update fields"
|
|
613
|
-
},
|
|
614
|
-
reason: {
|
|
615
|
-
type: "string",
|
|
616
|
-
description: "Explanation of why this suggestion is valid/invalid/adjusted (max 2000 chars)"
|
|
617
|
-
},
|
|
618
|
-
adjusted_description: {
|
|
619
|
-
type: "string",
|
|
620
|
-
description: "Updated description (only for verdict=adjusted, max 2000 chars)"
|
|
621
|
-
},
|
|
622
|
-
adjusted_severity: {
|
|
623
|
-
type: "string",
|
|
624
|
-
enum: ["info", "warning", "critical"],
|
|
625
|
-
description: "Updated severity (only for verdict=adjusted)"
|
|
626
|
-
},
|
|
627
|
-
adjusted_suggested_fix: {
|
|
628
|
-
type: "string",
|
|
629
|
-
description: "Updated suggested fix (only for verdict=adjusted, max 5000 chars)"
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
required: ["suggestion_id", "verdict", "reason"]
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
},
|
|
636
|
-
required: ["repo_slug", "results"]
|
|
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
386
|
{
|
|
713
387
|
name: "web-search",
|
|
714
388
|
description: "Search the web using DuckDuckGo. Returns a list of results with title, URL, and snippet. Use this to find companies, websites, directories, etc.",
|
|
@@ -758,26 +432,10 @@ var AGENT_TOOLS = [
|
|
|
758
432
|
];
|
|
759
433
|
var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
|
|
760
434
|
var AGENT_TOOL_MODULE_MAP = {
|
|
761
|
-
"agent-report-coverage": "agent_reporting",
|
|
762
|
-
"agent-report-finding": "agent_reporting",
|
|
763
|
-
"agent-save-documentation": "agent_reporting",
|
|
764
|
-
"agent-list-findings": "agent_reporting",
|
|
765
|
-
"agent-get-documentation": "agent_reporting",
|
|
766
|
-
"agent-validate-suggestions": "agent_reporting",
|
|
767
|
-
"agent-check-lead-exists": "agent_reporting",
|
|
768
|
-
"agent-save-lead": "agent_reporting",
|
|
769
|
-
"agent-save-email-draft": "agent_reporting",
|
|
770
|
-
"agent-complete-target": "agent_reporting",
|
|
771
435
|
"web-search": "agent_reporting",
|
|
772
436
|
"web-fetch": "agent_reporting",
|
|
773
437
|
"web-find-contacts": "agent_reporting"
|
|
774
438
|
};
|
|
775
|
-
function clamp(val, min, max) {
|
|
776
|
-
return Math.max(min, Math.min(max, val));
|
|
777
|
-
}
|
|
778
|
-
function sanitizeString(val, maxLen) {
|
|
779
|
-
return String(val ?? "").slice(0, maxLen);
|
|
780
|
-
}
|
|
781
439
|
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";
|
|
782
440
|
async function webSearch(query, maxResults) {
|
|
783
441
|
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
@@ -852,7 +510,19 @@ var NOISE_EMAIL_PATTERNS = [
|
|
|
852
510
|
/smith@/i,
|
|
853
511
|
/doe@/i,
|
|
854
512
|
/demo@/i,
|
|
855
|
-
/sample@/i
|
|
513
|
+
/sample@/i,
|
|
514
|
+
/naam@/i,
|
|
515
|
+
/voorbeeld/i,
|
|
516
|
+
/your-?email/i,
|
|
517
|
+
/email@/i,
|
|
518
|
+
/@domein\./i,
|
|
519
|
+
/@bedrijf\./i,
|
|
520
|
+
/@domain\./i,
|
|
521
|
+
/@sentry/i,
|
|
522
|
+
/@wixpress/i,
|
|
523
|
+
/@lieferkassen/i,
|
|
524
|
+
/john@/i,
|
|
525
|
+
/jane@/i
|
|
856
526
|
];
|
|
857
527
|
var CONTACT_PATH_KEYWORDS = [
|
|
858
528
|
"contact",
|
|
@@ -974,6 +644,21 @@ function guessCommonEmails(domain) {
|
|
|
974
644
|
const d = domain.replace(/^www\./, "");
|
|
975
645
|
return [`info@${d}`, `contact@${d}`, `hello@${d}`, `administratie@${d}`, `verkoop@${d}`];
|
|
976
646
|
}
|
|
647
|
+
var BOT_CHALLENGE_INDICATORS = [
|
|
648
|
+
"sgcaptcha",
|
|
649
|
+
"challenge-platform",
|
|
650
|
+
"cf-browser-verification",
|
|
651
|
+
"Just a moment",
|
|
652
|
+
"Checking your browser",
|
|
653
|
+
"Enable JavaScript and cookies",
|
|
654
|
+
"Attention Required",
|
|
655
|
+
"DDoS protection by"
|
|
656
|
+
];
|
|
657
|
+
function isBotChallengePage(html) {
|
|
658
|
+
if (html.length > 2e3) return false;
|
|
659
|
+
const lower = html.toLowerCase();
|
|
660
|
+
return BOT_CHALLENGE_INDICATORS.some((ind) => lower.includes(ind.toLowerCase()));
|
|
661
|
+
}
|
|
977
662
|
async function fetchRawHtml(url, timeoutMs = 1e4) {
|
|
978
663
|
const controller = new AbortController();
|
|
979
664
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -987,10 +672,33 @@ async function fetchRawHtml(url, timeoutMs = 1e4) {
|
|
|
987
672
|
redirect: "follow",
|
|
988
673
|
signal: controller.signal
|
|
989
674
|
});
|
|
990
|
-
if (!res.ok) return null;
|
|
991
675
|
const ct = res.headers.get("content-type") || "";
|
|
992
676
|
if (!ct.includes("text/html") && !ct.includes("text/plain") && !ct.includes("xhtml")) return null;
|
|
993
|
-
|
|
677
|
+
if (!res.ok && res.status !== 403) return null;
|
|
678
|
+
const html = await res.text();
|
|
679
|
+
if (isBotChallengePage(html)) return null;
|
|
680
|
+
return html;
|
|
681
|
+
} catch {
|
|
682
|
+
return null;
|
|
683
|
+
} finally {
|
|
684
|
+
clearTimeout(timer);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function fetchWaybackHtml(url, timeoutMs = 15e3) {
|
|
688
|
+
const cleanUrl = url.replace(/^https?:\/\//, "");
|
|
689
|
+
const wbUrl = `https://web.archive.org/web/2024/${cleanUrl}`;
|
|
690
|
+
const controller = new AbortController();
|
|
691
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
692
|
+
try {
|
|
693
|
+
const res = await fetch(wbUrl, {
|
|
694
|
+
headers: { "User-Agent": WEB_USER_AGENT },
|
|
695
|
+
redirect: "follow",
|
|
696
|
+
signal: controller.signal
|
|
697
|
+
});
|
|
698
|
+
if (!res.ok) return null;
|
|
699
|
+
const html = await res.text();
|
|
700
|
+
if (html.length < 500) return null;
|
|
701
|
+
return html;
|
|
994
702
|
} catch {
|
|
995
703
|
return null;
|
|
996
704
|
} finally {
|
|
@@ -1038,499 +746,8 @@ async function webFetch(url, extractLinks) {
|
|
|
1038
746
|
clearTimeout(timeout);
|
|
1039
747
|
}
|
|
1040
748
|
}
|
|
1041
|
-
function
|
|
1042
|
-
if (a === b) return 1;
|
|
1043
|
-
const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1044
|
-
const na = norm(a);
|
|
1045
|
-
const nb = norm(b);
|
|
1046
|
-
if (na === nb) return 1;
|
|
1047
|
-
if (na.length < 3 || nb.length < 3) return na === nb ? 1 : 0;
|
|
1048
|
-
const trigrams = (s) => {
|
|
1049
|
-
const set = /* @__PURE__ */ new Set();
|
|
1050
|
-
for (let i = 0; i <= s.length - 3; i++) set.add(s.slice(i, i + 3));
|
|
1051
|
-
return set;
|
|
1052
|
-
};
|
|
1053
|
-
const setA = trigrams(na);
|
|
1054
|
-
const setB = trigrams(nb);
|
|
1055
|
-
let intersection = 0;
|
|
1056
|
-
for (const t of setA) if (setB.has(t)) intersection++;
|
|
1057
|
-
const union = setA.size + setB.size - intersection;
|
|
1058
|
-
return union === 0 ? 0 : intersection / union;
|
|
1059
|
-
}
|
|
1060
|
-
async function handleAgentTool(name, args2, deps) {
|
|
1061
|
-
const { supabase: supabase2 } = deps;
|
|
749
|
+
async function handleAgentTool(name, args2) {
|
|
1062
750
|
switch (name) {
|
|
1063
|
-
// -----------------------------------------------------------------
|
|
1064
|
-
case "agent-report-coverage": {
|
|
1065
|
-
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1066
|
-
const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
|
|
1067
|
-
const entries = Array.isArray(args2.entries) ? args2.entries : [];
|
|
1068
|
-
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1069
|
-
if (entries.length === 0) throw new Error("entries array must not be empty");
|
|
1070
|
-
const wsId = deps.workspaceId;
|
|
1071
|
-
const scanCommit = wsId ? `agent-scan-${wsId.slice(0, 8)}` : `agent-scan-${Date.now().toString(36)}`;
|
|
1072
|
-
let upserted = 0;
|
|
1073
|
-
let errors = 0;
|
|
1074
|
-
for (const entry of entries) {
|
|
1075
|
-
const { error } = await supabase2.from("doc_coverage").upsert(
|
|
1076
|
-
{
|
|
1077
|
-
refront_project_id: refrontProjectId,
|
|
1078
|
-
repo_slug: repoSlug,
|
|
1079
|
-
path: sanitizeString(entry.path, 500),
|
|
1080
|
-
total_functions: clamp(Number(entry.total_functions) || 0, 0, 99999),
|
|
1081
|
-
documented_functions: clamp(Number(entry.documented_functions) || 0, 0, 99999),
|
|
1082
|
-
total_types: clamp(Number(entry.total_types) || 0, 0, 99999),
|
|
1083
|
-
documented_types: clamp(Number(entry.documented_types) || 0, 0, 99999),
|
|
1084
|
-
total_endpoints: clamp(Number(entry.total_endpoints) || 0, 0, 99999),
|
|
1085
|
-
documented_endpoints: clamp(Number(entry.documented_endpoints) || 0, 0, 99999),
|
|
1086
|
-
scan_commit: scanCommit,
|
|
1087
|
-
scanned_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1088
|
-
},
|
|
1089
|
-
{ onConflict: "repo_slug,path" }
|
|
1090
|
-
);
|
|
1091
|
-
if (error) errors++;
|
|
1092
|
-
else upserted++;
|
|
1093
|
-
}
|
|
1094
|
-
return {
|
|
1095
|
-
content: [{
|
|
1096
|
-
type: "text",
|
|
1097
|
-
text: `Coverage reported: ${upserted} entries upserted${errors > 0 ? `, ${errors} errors` : ""}`
|
|
1098
|
-
}]
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
// -----------------------------------------------------------------
|
|
1102
|
-
case "agent-report-finding": {
|
|
1103
|
-
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1104
|
-
const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
|
|
1105
|
-
const category = args2.category;
|
|
1106
|
-
const findings = Array.isArray(args2.findings) ? args2.findings : [];
|
|
1107
|
-
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1108
|
-
if (!category || !["scan_findings", "perf_audit"].includes(category)) {
|
|
1109
|
-
throw new Error('category must be "scan_findings" or "perf_audit"');
|
|
1110
|
-
}
|
|
1111
|
-
if (findings.length === 0) throw new Error("findings array must not be empty");
|
|
1112
|
-
const SIMILARITY_THRESHOLD = 0.55;
|
|
1113
|
-
const MAX_OPEN_PER_TYPE = 30;
|
|
1114
|
-
const { data: existingFindings } = await supabase2.from("doc_suggestion").select("id, type, description, file_path, severity, category, status").eq("repo_slug", repoSlug).in("status", ["open", "dismissed", "resolved", "ticket_created"]).limit(500);
|
|
1115
|
-
const existing = existingFindings ?? [];
|
|
1116
|
-
const typeCountMap = /* @__PURE__ */ new Map();
|
|
1117
|
-
for (const e of existing) {
|
|
1118
|
-
if (e.status !== "open") continue;
|
|
1119
|
-
const key = `${e.category}:${e.type}`;
|
|
1120
|
-
typeCountMap.set(key, (typeCountMap.get(key) ?? 0) + 1);
|
|
1121
|
-
}
|
|
1122
|
-
let inserted = 0;
|
|
1123
|
-
let deduplicated = 0;
|
|
1124
|
-
let grouped = 0;
|
|
1125
|
-
let errors = 0;
|
|
1126
|
-
const overflowBucket = /* @__PURE__ */ new Map();
|
|
1127
|
-
for (const f of findings) {
|
|
1128
|
-
const findingType = VALID_FINDING_TYPES.has(f.type) ? f.type : "improvement";
|
|
1129
|
-
const severity = VALID_SEVERITIES.has(f.severity) ? f.severity : "info";
|
|
1130
|
-
const description = sanitizeString(f.description, 2e3);
|
|
1131
|
-
const filePath = f.file_path ? sanitizeString(f.file_path, 500) : null;
|
|
1132
|
-
if (!description) continue;
|
|
1133
|
-
const isDuplicate = existing.some((e) => {
|
|
1134
|
-
if (e.type !== findingType) return false;
|
|
1135
|
-
if (filePath && e.file_path === filePath && ["dismissed", "resolved"].includes(e.status)) {
|
|
1136
|
-
return true;
|
|
1137
|
-
}
|
|
1138
|
-
if (filePath && e.file_path === filePath) {
|
|
1139
|
-
return textSimilarity(e.description ?? "", description) > 0.4;
|
|
1140
|
-
}
|
|
1141
|
-
return textSimilarity(e.description ?? "", description) > SIMILARITY_THRESHOLD;
|
|
1142
|
-
});
|
|
1143
|
-
if (isDuplicate) {
|
|
1144
|
-
deduplicated++;
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
const typeKey = `${category}:${findingType}`;
|
|
1148
|
-
const currentCount = typeCountMap.get(typeKey) ?? 0;
|
|
1149
|
-
if (currentCount >= MAX_OPEN_PER_TYPE) {
|
|
1150
|
-
const bucket = overflowBucket.get(typeKey) ?? [];
|
|
1151
|
-
bucket.push(`${filePath ? `${filePath}: ` : ""}${description.slice(0, 120)}`);
|
|
1152
|
-
overflowBucket.set(typeKey, bucket);
|
|
1153
|
-
grouped++;
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
const { error } = await supabase2.from("doc_suggestion").insert({
|
|
1157
|
-
repo_slug: repoSlug,
|
|
1158
|
-
refront_project_id: refrontProjectId,
|
|
1159
|
-
category,
|
|
1160
|
-
type: findingType,
|
|
1161
|
-
severity,
|
|
1162
|
-
description,
|
|
1163
|
-
file_path: filePath,
|
|
1164
|
-
suggested_fix: f.suggested_fix ? sanitizeString(f.suggested_fix, 5e3) : null,
|
|
1165
|
-
status: "open"
|
|
1166
|
-
});
|
|
1167
|
-
if (error) {
|
|
1168
|
-
errors++;
|
|
1169
|
-
} else {
|
|
1170
|
-
inserted++;
|
|
1171
|
-
typeCountMap.set(typeKey, currentCount + 1);
|
|
1172
|
-
existing.push({ id: "", type: findingType, description, file_path: filePath, severity, category, status: "open" });
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
for (const [typeKey, items] of overflowBucket) {
|
|
1176
|
-
const [cat, type] = typeKey.split(":");
|
|
1177
|
-
const summary = `Gegroepeerd (${items.length} items):
|
|
1178
|
-
${items.map((i) => `\u2022 ${i}`).join("\n")}`;
|
|
1179
|
-
await supabase2.from("doc_suggestion").insert({
|
|
1180
|
-
repo_slug: repoSlug,
|
|
1181
|
-
refront_project_id: refrontProjectId,
|
|
1182
|
-
category: cat,
|
|
1183
|
-
type,
|
|
1184
|
-
severity: "info",
|
|
1185
|
-
description: summary.slice(0, 5e3),
|
|
1186
|
-
file_path: null,
|
|
1187
|
-
suggested_fix: null,
|
|
1188
|
-
status: "open"
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
const parts = [`${inserted} inserted`];
|
|
1192
|
-
if (deduplicated > 0) parts.push(`${deduplicated} deduplicated`);
|
|
1193
|
-
if (grouped > 0) parts.push(`${grouped} grouped into ${overflowBucket.size} summary row(s)`);
|
|
1194
|
-
if (errors > 0) parts.push(`${errors} errors`);
|
|
1195
|
-
return {
|
|
1196
|
-
content: [{
|
|
1197
|
-
type: "text",
|
|
1198
|
-
text: `Findings reported under ${category}: ${parts.join(", ")}`
|
|
1199
|
-
}]
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
// -----------------------------------------------------------------
|
|
1203
|
-
case "agent-save-documentation": {
|
|
1204
|
-
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1205
|
-
const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
|
|
1206
|
-
const scope = VALID_SCOPES.has(args2.scope) ? args2.scope : "module";
|
|
1207
|
-
const path = sanitizeString(args2.path, 500);
|
|
1208
|
-
const title = sanitizeString(args2.title, 500);
|
|
1209
|
-
const content = sanitizeString(args2.content, 1e5);
|
|
1210
|
-
const reviewStatus = VALID_REVIEW_STATUSES.has(args2.review_status) ? args2.review_status : "pending";
|
|
1211
|
-
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1212
|
-
if (!path) throw new Error("path is required");
|
|
1213
|
-
if (!title) throw new Error("title is required");
|
|
1214
|
-
if (!content) throw new Error("content is required");
|
|
1215
|
-
const { data: existing } = await supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug).eq("scope", scope).eq("path", path).maybeSingle();
|
|
1216
|
-
if (existing) {
|
|
1217
|
-
const { error: error2 } = await supabase2.from("project_documentation").update({
|
|
1218
|
-
title,
|
|
1219
|
-
content,
|
|
1220
|
-
review_status: reviewStatus,
|
|
1221
|
-
pdf_storage_path: null,
|
|
1222
|
-
pdf_compiled_at: null,
|
|
1223
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1224
|
-
}).eq("id", existing.id);
|
|
1225
|
-
if (error2) throw new Error(`Failed to update documentation: ${error2.message}`);
|
|
1226
|
-
return { content: [{ type: "text", text: `Documentation updated: ${title} (${scope}/${path}). PDF compilation pending.` }] };
|
|
1227
|
-
}
|
|
1228
|
-
const generatedBy = deps.workspaceId ? `agent-${deps.workspaceId.slice(0, 8)}` : "agent-mcp";
|
|
1229
|
-
const { error } = await supabase2.from("project_documentation").insert({
|
|
1230
|
-
refront_project_id: refrontProjectId,
|
|
1231
|
-
repo_slug: repoSlug,
|
|
1232
|
-
scope,
|
|
1233
|
-
path,
|
|
1234
|
-
title,
|
|
1235
|
-
content,
|
|
1236
|
-
generated_by: generatedBy,
|
|
1237
|
-
review_status: reviewStatus,
|
|
1238
|
-
pdf_storage_path: null,
|
|
1239
|
-
pdf_compiled_at: null
|
|
1240
|
-
});
|
|
1241
|
-
if (error) throw new Error(`Failed to save documentation: ${error.message}`);
|
|
1242
|
-
return { content: [{ type: "text", text: `Documentation saved: ${title} (${scope}/${path}). PDF compilation pending.` }] };
|
|
1243
|
-
}
|
|
1244
|
-
// -----------------------------------------------------------------
|
|
1245
|
-
case "agent-list-findings": {
|
|
1246
|
-
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1247
|
-
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1248
|
-
const limit = clamp(Number(args2.limit) || 50, 1, 200);
|
|
1249
|
-
let query = supabase2.from("doc_suggestion").select("id, type, severity, description, file_path, status, category, created_at").eq("repo_slug", repoSlug).order("created_at", { ascending: false }).limit(limit);
|
|
1250
|
-
if (args2.type && VALID_FINDING_TYPES.has(args2.type)) {
|
|
1251
|
-
query = query.eq("type", args2.type);
|
|
1252
|
-
}
|
|
1253
|
-
if (args2.severity && VALID_SEVERITIES.has(args2.severity)) {
|
|
1254
|
-
query = query.eq("severity", args2.severity);
|
|
1255
|
-
}
|
|
1256
|
-
if (args2.status) {
|
|
1257
|
-
query = query.eq("status", args2.status);
|
|
1258
|
-
}
|
|
1259
|
-
const { data: findings, error } = await query;
|
|
1260
|
-
if (error) throw new Error(`Failed to query findings: ${error.message}`);
|
|
1261
|
-
if (!findings || findings.length === 0) {
|
|
1262
|
-
return { content: [{ type: "text", text: `No findings found for repo "${repoSlug}"` }] };
|
|
1263
|
-
}
|
|
1264
|
-
const summary = findings.map(
|
|
1265
|
-
(f) => `[${f.severity}] ${f.type}: ${String(f.description).slice(0, 120)}${f.file_path ? ` (${f.file_path})` : ""} \u2014 ${f.status}`
|
|
1266
|
-
).join("\n");
|
|
1267
|
-
return {
|
|
1268
|
-
content: [{
|
|
1269
|
-
type: "text",
|
|
1270
|
-
text: `${findings.length} findings for "${repoSlug}":
|
|
1271
|
-
|
|
1272
|
-
${summary}`
|
|
1273
|
-
}]
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
// -----------------------------------------------------------------
|
|
1277
|
-
case "agent-get-documentation": {
|
|
1278
|
-
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1279
|
-
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1280
|
-
const limit = clamp(Number(args2.limit) || 20, 1, 100);
|
|
1281
|
-
let query = supabase2.from("project_documentation").select("id, repo_slug, scope, path, title, content, review_status, generated_by, created_at, updated_at").eq("repo_slug", repoSlug).order("updated_at", { ascending: false }).limit(limit);
|
|
1282
|
-
if (args2.scope && VALID_SCOPES.has(args2.scope)) {
|
|
1283
|
-
query = query.eq("scope", args2.scope);
|
|
1284
|
-
}
|
|
1285
|
-
if (args2.path) {
|
|
1286
|
-
query = query.eq("path", sanitizeString(args2.path, 500));
|
|
1287
|
-
}
|
|
1288
|
-
const { data: docs, error } = await query;
|
|
1289
|
-
if (error) throw new Error(`Failed to query documentation: ${error.message}`);
|
|
1290
|
-
if (!docs || docs.length === 0) {
|
|
1291
|
-
return { content: [{ type: "text", text: `No documentation found for repo "${repoSlug}"` }] };
|
|
1292
|
-
}
|
|
1293
|
-
const output = docs.map((d) => {
|
|
1294
|
-
const fullContent = String(d.content || "");
|
|
1295
|
-
const isChangelog = d.scope === "changelog";
|
|
1296
|
-
const maxPreview = isChangelog ? 5e4 : 500;
|
|
1297
|
-
const preview = fullContent.slice(0, maxPreview);
|
|
1298
|
-
return [
|
|
1299
|
-
`## ${d.title} (${d.scope}/${d.path})`,
|
|
1300
|
-
`Status: ${d.review_status} | By: ${d.generated_by || "unknown"}`,
|
|
1301
|
-
`Updated: ${d.updated_at || d.created_at}`,
|
|
1302
|
-
"",
|
|
1303
|
-
preview + (fullContent.length > maxPreview ? "\n...(truncated)" : ""),
|
|
1304
|
-
""
|
|
1305
|
-
].join("\n");
|
|
1306
|
-
}).join("\n---\n\n");
|
|
1307
|
-
return {
|
|
1308
|
-
content: [{ type: "text", text: `${docs.length} docs for "${repoSlug}":
|
|
1309
|
-
|
|
1310
|
-
${output}` }]
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
// -----------------------------------------------------------------
|
|
1314
|
-
case "agent-validate-suggestions": {
|
|
1315
|
-
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1316
|
-
const results = Array.isArray(args2.results) ? args2.results : [];
|
|
1317
|
-
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1318
|
-
if (results.length === 0) throw new Error("results array must not be empty");
|
|
1319
|
-
const validatedBy = deps.workspaceId ? `validator-${deps.workspaceId.slice(0, 8)}` : "validator-mcp";
|
|
1320
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1321
|
-
let dismissed = 0;
|
|
1322
|
-
let adjusted = 0;
|
|
1323
|
-
let validated = 0;
|
|
1324
|
-
let errors = 0;
|
|
1325
|
-
let resolved = 0;
|
|
1326
|
-
for (const r of results) {
|
|
1327
|
-
const id = sanitizeString(r.suggestion_id, 100);
|
|
1328
|
-
const verdict = r.verdict;
|
|
1329
|
-
const reason = sanitizeString(r.reason, 2e3);
|
|
1330
|
-
if (!id || !["valid", "invalid", "adjusted", "resolved"].includes(verdict)) {
|
|
1331
|
-
errors++;
|
|
1332
|
-
continue;
|
|
1333
|
-
}
|
|
1334
|
-
if (verdict === "resolved") {
|
|
1335
|
-
const { error } = await supabase2.from("doc_suggestion").update({
|
|
1336
|
-
status: "resolved",
|
|
1337
|
-
validated_at: now,
|
|
1338
|
-
validated_by: validatedBy
|
|
1339
|
-
}).eq("id", id).eq("repo_slug", repoSlug);
|
|
1340
|
-
if (error) errors++;
|
|
1341
|
-
else resolved++;
|
|
1342
|
-
} else if (verdict === "invalid") {
|
|
1343
|
-
const { error } = await supabase2.from("doc_suggestion").update({
|
|
1344
|
-
status: "dismissed",
|
|
1345
|
-
dismissed_reason: reason || "Dismissed by validation agent",
|
|
1346
|
-
validated_at: now,
|
|
1347
|
-
validated_by: validatedBy
|
|
1348
|
-
}).eq("id", id).eq("repo_slug", repoSlug);
|
|
1349
|
-
if (error) errors++;
|
|
1350
|
-
else dismissed++;
|
|
1351
|
-
} else if (verdict === "adjusted") {
|
|
1352
|
-
const updates = {
|
|
1353
|
-
validated_at: now,
|
|
1354
|
-
validated_by: validatedBy
|
|
1355
|
-
};
|
|
1356
|
-
if (r.adjusted_description) {
|
|
1357
|
-
updates.description = sanitizeString(r.adjusted_description, 2e3);
|
|
1358
|
-
}
|
|
1359
|
-
if (r.adjusted_severity && VALID_SEVERITIES.has(r.adjusted_severity)) {
|
|
1360
|
-
updates.severity = r.adjusted_severity;
|
|
1361
|
-
}
|
|
1362
|
-
if (r.adjusted_suggested_fix) {
|
|
1363
|
-
updates.suggested_fix = sanitizeString(r.adjusted_suggested_fix, 5e3);
|
|
1364
|
-
}
|
|
1365
|
-
const { error } = await supabase2.from("doc_suggestion").update(updates).eq("id", id).eq("repo_slug", repoSlug);
|
|
1366
|
-
if (error) errors++;
|
|
1367
|
-
else adjusted++;
|
|
1368
|
-
} else {
|
|
1369
|
-
const { error } = await supabase2.from("doc_suggestion").update({ validated_at: now, validated_by: validatedBy }).eq("id", id).eq("repo_slug", repoSlug);
|
|
1370
|
-
if (error) errors++;
|
|
1371
|
-
else validated++;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
const parts = [];
|
|
1375
|
-
if (resolved > 0) parts.push(`${resolved} resolved`);
|
|
1376
|
-
if (validated > 0) parts.push(`${validated} valid`);
|
|
1377
|
-
if (dismissed > 0) parts.push(`${dismissed} dismissed`);
|
|
1378
|
-
if (adjusted > 0) parts.push(`${adjusted} adjusted`);
|
|
1379
|
-
if (errors > 0) parts.push(`${errors} errors`);
|
|
1380
|
-
return {
|
|
1381
|
-
content: [{
|
|
1382
|
-
type: "text",
|
|
1383
|
-
text: `Validation complete for "${repoSlug}": ${parts.join(", ")}`
|
|
1384
|
-
}]
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
// -----------------------------------------------------------------
|
|
1388
|
-
// Lead Generation tools
|
|
1389
|
-
// -----------------------------------------------------------------
|
|
1390
|
-
case "agent-check-lead-exists": {
|
|
1391
|
-
const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
|
|
1392
|
-
const companyName = sanitizeString(args2.company_name, 500);
|
|
1393
|
-
if (!websiteUrl && !companyName) {
|
|
1394
|
-
throw new Error("At least one of website_url or company_name is required");
|
|
1395
|
-
}
|
|
1396
|
-
const checks = [];
|
|
1397
|
-
if (websiteUrl) {
|
|
1398
|
-
const { data: urlMatch } = await supabase2.from("lead").select("id, company_name, website_url").eq("website_url", websiteUrl).maybeSingle();
|
|
1399
|
-
if (urlMatch) {
|
|
1400
|
-
return {
|
|
1401
|
-
content: [{
|
|
1402
|
-
type: "text",
|
|
1403
|
-
text: `DUPLICATE: Lead already exists (URL match). ID: ${urlMatch.id}, Company: ${urlMatch.company_name}. SKIP this company.`
|
|
1404
|
-
}]
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
checks.push(`URL "${websiteUrl}" not found`);
|
|
1408
|
-
}
|
|
1409
|
-
if (companyName) {
|
|
1410
|
-
const normalized = normalizeCompanyName(companyName);
|
|
1411
|
-
if (normalized.length >= 3) {
|
|
1412
|
-
const { data: nameMatches } = await supabase2.from("lead").select("id, company_name, website_url").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
|
|
1413
|
-
if (nameMatches) {
|
|
1414
|
-
for (const match of nameMatches) {
|
|
1415
|
-
const matchNorm = normalizeCompanyName(match.company_name);
|
|
1416
|
-
if (textSimilarity(normalized, matchNorm) > 0.6) {
|
|
1417
|
-
return {
|
|
1418
|
-
content: [{
|
|
1419
|
-
type: "text",
|
|
1420
|
-
text: `DUPLICATE: Similar company found. ID: ${match.id}, Name: "${match.company_name}" (${match.website_url}). SKIP this company.`
|
|
1421
|
-
}]
|
|
1422
|
-
};
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
checks.push(`Name "${companyName}" has no similar matches`);
|
|
1428
|
-
}
|
|
1429
|
-
return {
|
|
1430
|
-
content: [{
|
|
1431
|
-
type: "text",
|
|
1432
|
-
text: `NOT FOUND: No duplicate detected. ${checks.join(". ")}. Safe to proceed.`
|
|
1433
|
-
}]
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
case "agent-save-lead": {
|
|
1437
|
-
const companyName = sanitizeString(args2.company_name, 500);
|
|
1438
|
-
const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
|
|
1439
|
-
if (!companyName) throw new Error("company_name is required");
|
|
1440
|
-
if (!websiteUrl) throw new Error("website_url is required");
|
|
1441
|
-
const { data: existing } = await supabase2.from("lead").select("id, company_name").eq("website_url", websiteUrl).maybeSingle();
|
|
1442
|
-
if (existing) {
|
|
1443
|
-
return {
|
|
1444
|
-
content: [{
|
|
1445
|
-
type: "text",
|
|
1446
|
-
text: `Lead already exists (URL match). ID: ${existing.id}, Company: "${existing.company_name}". Skipped.`
|
|
1447
|
-
}]
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
const normalized = normalizeCompanyName(companyName);
|
|
1451
|
-
if (normalized.length >= 3) {
|
|
1452
|
-
const { data: nameMatches } = await supabase2.from("lead").select("id, company_name").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
|
|
1453
|
-
if (nameMatches) {
|
|
1454
|
-
for (const match of nameMatches) {
|
|
1455
|
-
if (textSimilarity(normalized, normalizeCompanyName(match.company_name)) > 0.6) {
|
|
1456
|
-
return {
|
|
1457
|
-
content: [{
|
|
1458
|
-
type: "text",
|
|
1459
|
-
text: `Lead already exists (name match). ID: ${match.id}, Name: "${match.company_name}". Skipped.`
|
|
1460
|
-
}]
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
const fitScore = args2.fit_score ? clamp(Number(args2.fit_score), 1, 10) : null;
|
|
1467
|
-
const { data: inserted, error } = await supabase2.from("lead").insert({
|
|
1468
|
-
company_name: companyName,
|
|
1469
|
-
website_url: websiteUrl,
|
|
1470
|
-
industry: args2.industry ? sanitizeString(args2.industry, 200) : null,
|
|
1471
|
-
region: args2.region ? sanitizeString(args2.region, 200) : null,
|
|
1472
|
-
description: args2.description ? sanitizeString(args2.description, 2e3) : null,
|
|
1473
|
-
potential_fit: args2.potential_fit ? sanitizeString(args2.potential_fit, 2e3) : null,
|
|
1474
|
-
fit_score: fitScore,
|
|
1475
|
-
estimated_company_size: args2.estimated_company_size ? sanitizeString(args2.estimated_company_size, 50) : null,
|
|
1476
|
-
kvk_number: args2.kvk_number ? sanitizeString(args2.kvk_number, 20) : null,
|
|
1477
|
-
contact_name: args2.contact_name ? sanitizeString(args2.contact_name, 200) : null,
|
|
1478
|
-
contact_role: args2.contact_role ? sanitizeString(args2.contact_role, 200) : null,
|
|
1479
|
-
contact_email: args2.contact_email ? sanitizeString(args2.contact_email, 200) : null,
|
|
1480
|
-
contact_phone: args2.contact_phone ? sanitizeString(args2.contact_phone, 50) : null,
|
|
1481
|
-
contact_linkedin: args2.contact_linkedin ? sanitizeString(args2.contact_linkedin, 500) : null,
|
|
1482
|
-
general_email: args2.general_email ? sanitizeString(args2.general_email, 200) : null,
|
|
1483
|
-
general_phone: args2.general_phone ? sanitizeString(args2.general_phone, 50) : null,
|
|
1484
|
-
source_url: args2.source_url ? sanitizeString(args2.source_url, 500) : null,
|
|
1485
|
-
target_id: args2.target_id ? sanitizeString(args2.target_id, 100) : null,
|
|
1486
|
-
status: "new"
|
|
1487
|
-
}).select("id").single();
|
|
1488
|
-
if (error) throw new Error(`Failed to save lead: ${error.message}`);
|
|
1489
|
-
return {
|
|
1490
|
-
content: [{
|
|
1491
|
-
type: "text",
|
|
1492
|
-
text: `Lead saved: "${companyName}" (${websiteUrl}). ID: ${inserted.id}. Fit score: ${fitScore ?? "N/A"}.`
|
|
1493
|
-
}]
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
case "agent-save-email-draft": {
|
|
1497
|
-
const leadId = sanitizeString(args2.lead_id, 100);
|
|
1498
|
-
const subject = sanitizeString(args2.subject, 200);
|
|
1499
|
-
let body = sanitizeString(args2.body, 5e3);
|
|
1500
|
-
const tone = sanitizeString(args2.tone, 50) || "professional";
|
|
1501
|
-
if (!leadId) throw new Error("lead_id is required");
|
|
1502
|
-
if (!subject) throw new Error("subject is required");
|
|
1503
|
-
if (!body) throw new Error("body is required");
|
|
1504
|
-
body = body.replace(/[\u2013\u2014]/g, ",");
|
|
1505
|
-
const { data: inserted, error } = await supabase2.from("lead_email_draft").insert({ lead_id: leadId, subject, body, tone, status: "draft" }).select("id").single();
|
|
1506
|
-
if (error) throw new Error(`Failed to save email draft: ${error.message}`);
|
|
1507
|
-
await supabase2.from("lead").update({ status: "email_drafted", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", leadId).eq("status", "new");
|
|
1508
|
-
return {
|
|
1509
|
-
content: [{
|
|
1510
|
-
type: "text",
|
|
1511
|
-
text: `Email draft saved for lead ${leadId}. Draft ID: ${inserted.id}. Subject: "${subject}".`
|
|
1512
|
-
}]
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
|
-
case "agent-complete-target": {
|
|
1516
|
-
const targetId = sanitizeString(args2.target_id, 100);
|
|
1517
|
-
const resultsCount = clamp(Number(args2.results_count) || 0, 0, 9999);
|
|
1518
|
-
if (!targetId) throw new Error("target_id is required");
|
|
1519
|
-
const { error } = await supabase2.from("lead_generation_target").update({
|
|
1520
|
-
results_count: resultsCount,
|
|
1521
|
-
assigned_workspace_id: null
|
|
1522
|
-
}).eq("id", targetId);
|
|
1523
|
-
if (error) throw new Error(`Failed to update target: ${error.message}`);
|
|
1524
|
-
return {
|
|
1525
|
-
content: [{
|
|
1526
|
-
type: "text",
|
|
1527
|
-
text: `Target ${targetId} updated: ${resultsCount} leads found. Workspace released.`
|
|
1528
|
-
}]
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
// -----------------------------------------------------------------
|
|
1532
|
-
// Web Tools
|
|
1533
|
-
// -----------------------------------------------------------------
|
|
1534
751
|
case "web-search": {
|
|
1535
752
|
const query = sanitizeString(args2.query, 500);
|
|
1536
753
|
if (!query) throw new Error("query is required");
|
|
@@ -1585,9 +802,6 @@ LinkedIn: ${pageLinkedIn.join(", ")}`;
|
|
|
1585
802
|
}
|
|
1586
803
|
return { content: [{ type: "text", text }] };
|
|
1587
804
|
}
|
|
1588
|
-
// -----------------------------------------------------------------
|
|
1589
|
-
// Web Find Contacts — SOTA multi-page crawler + email extraction
|
|
1590
|
-
// -----------------------------------------------------------------
|
|
1591
805
|
case "web-find-contacts": {
|
|
1592
806
|
const inputUrl = sanitizeString(args2.url, 2e3);
|
|
1593
807
|
if (!inputUrl) throw new Error("url is required");
|
|
@@ -1608,18 +822,23 @@ LinkedIn: ${pageLinkedIn.join(", ")}`;
|
|
|
1608
822
|
if (!fullUrl.includes("www.")) urlsToTry.push(fullUrl.replace("https://", "http://www."));
|
|
1609
823
|
}
|
|
1610
824
|
let html = null;
|
|
825
|
+
let usedWayback = false;
|
|
1611
826
|
for (const tryUrl of urlsToTry) {
|
|
1612
827
|
html = await fetchRawHtml(tryUrl, 12e3);
|
|
1613
828
|
if (html) break;
|
|
1614
829
|
}
|
|
830
|
+
if (!html) {
|
|
831
|
+
html = await fetchWaybackHtml(`https://${domain}`, 15e3);
|
|
832
|
+
if (html) usedWayback = true;
|
|
833
|
+
}
|
|
1615
834
|
if (!html) throw new Error(`Could not fetch ${fullUrl} (site may be down or blocking)`);
|
|
1616
|
-
const contactPages = discoverContactPages(html, fullUrl);
|
|
835
|
+
const contactPages = usedWayback ? [] : discoverContactPages(html, fullUrl);
|
|
1617
836
|
const pagePromises = contactPages.map(async (pageUrl) => {
|
|
1618
837
|
const pageHtml = await fetchRawHtml(pageUrl, 8e3);
|
|
1619
838
|
return { url: pageUrl, html: pageHtml };
|
|
1620
839
|
});
|
|
1621
840
|
const pageResults = await Promise.allSettled(pagePromises);
|
|
1622
|
-
const successPages = [fullUrl];
|
|
841
|
+
const successPages = [usedWayback ? `(wayback) ${domain}` : fullUrl];
|
|
1623
842
|
const allHtmls = [html];
|
|
1624
843
|
for (const result of pageResults) {
|
|
1625
844
|
if (result.status === "fulfilled" && result.value.html) {
|
|
@@ -1627,6 +846,20 @@ LinkedIn: ${pageLinkedIn.join(", ")}`;
|
|
|
1627
846
|
successPages.push(result.value.url);
|
|
1628
847
|
}
|
|
1629
848
|
}
|
|
849
|
+
if (usedWayback) {
|
|
850
|
+
const waybackContactPaths = ["/contact", "/over-ons", "/about", "/team"];
|
|
851
|
+
const wbPromises = waybackContactPaths.map(async (path) => {
|
|
852
|
+
const wbHtml = await fetchWaybackHtml(`https://${domain}${path}`, 12e3);
|
|
853
|
+
return { path, html: wbHtml };
|
|
854
|
+
});
|
|
855
|
+
const wbResults = await Promise.allSettled(wbPromises);
|
|
856
|
+
for (const wr of wbResults) {
|
|
857
|
+
if (wr.status === "fulfilled" && wr.value.html) {
|
|
858
|
+
allHtmls.push(wr.value.html);
|
|
859
|
+
successPages.push(`(wayback) ${domain}${wr.value.path}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
1630
863
|
const allEmails = /* @__PURE__ */ new Set();
|
|
1631
864
|
const allPhones = /* @__PURE__ */ new Set();
|
|
1632
865
|
const allLinkedIn = /* @__PURE__ */ new Set();
|
|
@@ -1713,9 +946,8 @@ LinkedIn: ${pageLinkedIn.join(", ")}`;
|
|
|
1713
946
|
for (const p of failedPages) lines.push(` [--] ${p}`);
|
|
1714
947
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1715
948
|
}
|
|
1716
|
-
// -----------------------------------------------------------------
|
|
1717
949
|
default:
|
|
1718
|
-
return { content: [{ type: "text", text: `Unknown
|
|
950
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
1719
951
|
}
|
|
1720
952
|
}
|
|
1721
953
|
|
|
@@ -1729,7 +961,6 @@ var supabaseUrl = getArg("supabase-url") || process.env.SUPABASE_URL;
|
|
|
1729
961
|
var supabaseKey = getArg("supabase-key") || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
1730
962
|
var encryptionKey = getArg("encryption-key") || process.env.ENCRYPTION_KEY;
|
|
1731
963
|
var mijnhostApiKey = getArg("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
|
|
1732
|
-
var agentWorkspaceId = getArg("workspace-id") || process.env.AGENT_WORKSPACE_ID || null;
|
|
1733
964
|
var httpMode = args.includes("--http");
|
|
1734
965
|
var httpPort = Number(getArg("port")) || 3100;
|
|
1735
966
|
if (!apiKey) {
|
|
@@ -1792,7 +1023,6 @@ var MODULE_KEYS = [
|
|
|
1792
1023
|
"supabase",
|
|
1793
1024
|
"wiki",
|
|
1794
1025
|
"ci_cd",
|
|
1795
|
-
"source_control",
|
|
1796
1026
|
"domains",
|
|
1797
1027
|
"settings",
|
|
1798
1028
|
"agent_reporting"
|
|
@@ -2291,6 +1521,69 @@ function sshExecViaProxy(proxyOpts, targetOpts, command) {
|
|
|
2291
1521
|
});
|
|
2292
1522
|
});
|
|
2293
1523
|
}
|
|
1524
|
+
function connectSshClient(opts, proxy, readyTimeout = 6e4) {
|
|
1525
|
+
if (!proxy) {
|
|
1526
|
+
return new Promise((resolve, reject) => {
|
|
1527
|
+
const ssh = new Client();
|
|
1528
|
+
ssh.on("ready", () => resolve({ client: ssh, cleanup: () => ssh.end() }));
|
|
1529
|
+
ssh.on("error", (e) => reject(e));
|
|
1530
|
+
ssh.connect({
|
|
1531
|
+
host: opts.hostname,
|
|
1532
|
+
port: opts.port,
|
|
1533
|
+
username: opts.username,
|
|
1534
|
+
password: opts.password,
|
|
1535
|
+
privateKey: opts.privateKey,
|
|
1536
|
+
passphrase: opts.passphrase,
|
|
1537
|
+
readyTimeout
|
|
1538
|
+
});
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
return new Promise((resolve, reject) => {
|
|
1542
|
+
const proxyClient = new Client();
|
|
1543
|
+
proxyClient.on("ready", () => {
|
|
1544
|
+
proxyClient.forwardOut("127.0.0.1", 0, opts.hostname, opts.port, (err, tunnel) => {
|
|
1545
|
+
if (err) {
|
|
1546
|
+
proxyClient.end();
|
|
1547
|
+
reject(err);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const targetClient = new Client();
|
|
1551
|
+
targetClient.on(
|
|
1552
|
+
"ready",
|
|
1553
|
+
() => resolve({
|
|
1554
|
+
client: targetClient,
|
|
1555
|
+
cleanup: () => {
|
|
1556
|
+
targetClient.end();
|
|
1557
|
+
proxyClient.end();
|
|
1558
|
+
}
|
|
1559
|
+
})
|
|
1560
|
+
);
|
|
1561
|
+
targetClient.on("error", (e) => {
|
|
1562
|
+
proxyClient.end();
|
|
1563
|
+
reject(e);
|
|
1564
|
+
});
|
|
1565
|
+
targetClient.connect({
|
|
1566
|
+
sock: tunnel,
|
|
1567
|
+
username: opts.username,
|
|
1568
|
+
password: opts.password,
|
|
1569
|
+
privateKey: opts.privateKey,
|
|
1570
|
+
passphrase: opts.passphrase,
|
|
1571
|
+
readyTimeout
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
proxyClient.on("error", (e) => reject(e));
|
|
1576
|
+
proxyClient.connect({
|
|
1577
|
+
host: proxy.hostname,
|
|
1578
|
+
port: proxy.port,
|
|
1579
|
+
username: proxy.username,
|
|
1580
|
+
password: proxy.password,
|
|
1581
|
+
privateKey: proxy.privateKey,
|
|
1582
|
+
passphrase: proxy.passphrase,
|
|
1583
|
+
readyTimeout: proxy.timeout || 3e4
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
2294
1587
|
function sanitizePath(path) {
|
|
2295
1588
|
let normalized = path.replace(/\\/g, "/").replace(/\0/g, "");
|
|
2296
1589
|
const parts = normalized.split("/");
|
|
@@ -2311,34 +1604,31 @@ function assertWritablePath(path) {
|
|
|
2311
1604
|
}
|
|
2312
1605
|
}
|
|
2313
1606
|
}
|
|
2314
|
-
async function sftpReaddir(opts, dirPath) {
|
|
1607
|
+
async function sftpReaddir(opts, dirPath, proxy) {
|
|
2315
1608
|
const safe = sanitizePath(dirPath);
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
1609
|
+
let cleanup;
|
|
1610
|
+
try {
|
|
1611
|
+
const { client, cleanup: c } = await connectSshClient(opts, proxy, 3e4);
|
|
1612
|
+
cleanup = c;
|
|
1613
|
+
return await new Promise((resolve) => {
|
|
1614
|
+
const timer = setTimeout(() => {
|
|
1615
|
+
cleanup?.();
|
|
2323
1616
|
resolve("Error: timeout");
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
ssh.sftp((err, sftp) => {
|
|
1617
|
+
cleanup = void 0;
|
|
1618
|
+
}, 3e4);
|
|
1619
|
+
client.sftp((err, sftp) => {
|
|
2328
1620
|
if (err) {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
resolve(`Error: ${err.message}`);
|
|
2334
|
-
}
|
|
1621
|
+
clearTimeout(timer);
|
|
1622
|
+
cleanup?.();
|
|
1623
|
+
cleanup = void 0;
|
|
1624
|
+
resolve(`Error: ${err.message}`);
|
|
2335
1625
|
return;
|
|
2336
1626
|
}
|
|
2337
1627
|
sftp.readdir(safe, (err2, list) => {
|
|
2338
|
-
done = true;
|
|
2339
1628
|
clearTimeout(timer);
|
|
2340
1629
|
if (err2) {
|
|
2341
|
-
|
|
1630
|
+
cleanup?.();
|
|
1631
|
+
cleanup = void 0;
|
|
2342
1632
|
resolve(`Error: ${err2.message}`);
|
|
2343
1633
|
return;
|
|
2344
1634
|
}
|
|
@@ -2349,199 +1639,159 @@ async function sftpReaddir(opts, dirPath) {
|
|
|
2349
1639
|
const mtime = item.attrs.mtime ? new Date(item.attrs.mtime * 1e3).toISOString() : "";
|
|
2350
1640
|
return `${isDir ? "d" : "-"} ${String(size).padStart(10)} ${mtime} ${item.filename}`;
|
|
2351
1641
|
});
|
|
2352
|
-
|
|
1642
|
+
cleanup?.();
|
|
1643
|
+
cleanup = void 0;
|
|
2353
1644
|
resolve(entries.join("\n"));
|
|
2354
1645
|
});
|
|
2355
1646
|
});
|
|
2356
1647
|
});
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
resolve(`Error: ${e.message}`);
|
|
2362
|
-
}
|
|
2363
|
-
});
|
|
2364
|
-
ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 3e4 });
|
|
2365
|
-
});
|
|
1648
|
+
} catch (e) {
|
|
1649
|
+
cleanup?.();
|
|
1650
|
+
return `Error: ${e.message}`;
|
|
1651
|
+
}
|
|
2366
1652
|
}
|
|
2367
|
-
async function sftpRead(opts, filePath) {
|
|
1653
|
+
async function sftpRead(opts, filePath, proxy) {
|
|
2368
1654
|
const safe = sanitizePath(filePath);
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
1655
|
+
let cleanup;
|
|
1656
|
+
try {
|
|
1657
|
+
const { client, cleanup: c } = await connectSshClient(opts, proxy, 6e4);
|
|
1658
|
+
cleanup = c;
|
|
1659
|
+
return await new Promise((resolve) => {
|
|
1660
|
+
const timer = setTimeout(() => {
|
|
1661
|
+
cleanup?.();
|
|
2376
1662
|
resolve("Error: timeout");
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
ssh.sftp((err, sftp) => {
|
|
1663
|
+
cleanup = void 0;
|
|
1664
|
+
}, 6e4);
|
|
1665
|
+
client.sftp((err, sftp) => {
|
|
2381
1666
|
if (err) {
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
resolve(`Error: ${err.message}`);
|
|
2387
|
-
}
|
|
1667
|
+
clearTimeout(timer);
|
|
1668
|
+
cleanup?.();
|
|
1669
|
+
cleanup = void 0;
|
|
1670
|
+
resolve(`Error: ${err.message}`);
|
|
2388
1671
|
return;
|
|
2389
1672
|
}
|
|
2390
1673
|
sftp.stat(safe, (err2, stats) => {
|
|
2391
1674
|
if (err2) {
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
resolve(`Error: ${err2.message}`);
|
|
2397
|
-
}
|
|
1675
|
+
clearTimeout(timer);
|
|
1676
|
+
cleanup?.();
|
|
1677
|
+
cleanup = void 0;
|
|
1678
|
+
resolve(`Error: ${err2.message}`);
|
|
2398
1679
|
return;
|
|
2399
1680
|
}
|
|
2400
1681
|
if ((stats.size || 0) > 1048576) {
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
resolve(`Error: file too large (${stats.size} bytes, max 1MB)`);
|
|
2406
|
-
}
|
|
1682
|
+
clearTimeout(timer);
|
|
1683
|
+
cleanup?.();
|
|
1684
|
+
cleanup = void 0;
|
|
1685
|
+
resolve(`Error: file too large (${stats.size} bytes, max 1MB)`);
|
|
2407
1686
|
return;
|
|
2408
1687
|
}
|
|
2409
1688
|
const chunks = [];
|
|
2410
1689
|
const rs = sftp.createReadStream(safe);
|
|
2411
|
-
rs.on("data", (
|
|
1690
|
+
rs.on("data", (ch) => chunks.push(ch));
|
|
2412
1691
|
rs.on("end", () => {
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
resolve(Buffer.concat(chunks.map((c) => new Uint8Array(c))).toString("utf-8"));
|
|
2418
|
-
}
|
|
1692
|
+
clearTimeout(timer);
|
|
1693
|
+
cleanup?.();
|
|
1694
|
+
cleanup = void 0;
|
|
1695
|
+
resolve(Buffer.concat(chunks.map((ch) => new Uint8Array(ch))).toString("utf-8"));
|
|
2419
1696
|
});
|
|
2420
1697
|
rs.on("error", (e) => {
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
resolve(`Error: ${e.message}`);
|
|
2426
|
-
}
|
|
1698
|
+
clearTimeout(timer);
|
|
1699
|
+
cleanup?.();
|
|
1700
|
+
cleanup = void 0;
|
|
1701
|
+
resolve(`Error: ${e.message}`);
|
|
2427
1702
|
});
|
|
2428
1703
|
});
|
|
2429
1704
|
});
|
|
2430
1705
|
});
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
resolve(`Error: ${e.message}`);
|
|
2436
|
-
}
|
|
2437
|
-
});
|
|
2438
|
-
ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 6e4 });
|
|
2439
|
-
});
|
|
1706
|
+
} catch (e) {
|
|
1707
|
+
cleanup?.();
|
|
1708
|
+
return `Error: ${e.message}`;
|
|
1709
|
+
}
|
|
2440
1710
|
}
|
|
2441
|
-
async function sftpWrite(opts, filePath, content) {
|
|
1711
|
+
async function sftpWrite(opts, filePath, content, proxy) {
|
|
2442
1712
|
const safe = sanitizePath(filePath);
|
|
2443
1713
|
assertWritablePath(safe);
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
1714
|
+
let cleanup;
|
|
1715
|
+
try {
|
|
1716
|
+
const { client, cleanup: c } = await connectSshClient(opts, proxy, 6e4);
|
|
1717
|
+
cleanup = c;
|
|
1718
|
+
return await new Promise((resolve) => {
|
|
1719
|
+
const timer = setTimeout(() => {
|
|
1720
|
+
cleanup?.();
|
|
2451
1721
|
resolve("Error: timeout");
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
ssh.sftp((err, sftp) => {
|
|
1722
|
+
cleanup = void 0;
|
|
1723
|
+
}, 6e4);
|
|
1724
|
+
client.sftp((err, sftp) => {
|
|
2456
1725
|
if (err) {
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
resolve(`Error: ${err.message}`);
|
|
2462
|
-
}
|
|
1726
|
+
clearTimeout(timer);
|
|
1727
|
+
cleanup?.();
|
|
1728
|
+
cleanup = void 0;
|
|
1729
|
+
resolve(`Error: ${err.message}`);
|
|
2463
1730
|
return;
|
|
2464
1731
|
}
|
|
2465
1732
|
const ws = sftp.createWriteStream(safe, { mode: 420 });
|
|
2466
1733
|
ws.on("close", () => {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
resolve(`Written ${content.length} bytes to ${safe}`);
|
|
2472
|
-
}
|
|
1734
|
+
clearTimeout(timer);
|
|
1735
|
+
cleanup?.();
|
|
1736
|
+
cleanup = void 0;
|
|
1737
|
+
resolve(`Written ${content.length} bytes to ${safe}`);
|
|
2473
1738
|
});
|
|
2474
1739
|
ws.on("error", (e) => {
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
resolve(`Error: ${e.message}`);
|
|
2480
|
-
}
|
|
1740
|
+
clearTimeout(timer);
|
|
1741
|
+
cleanup?.();
|
|
1742
|
+
cleanup = void 0;
|
|
1743
|
+
resolve(`Error: ${e.message}`);
|
|
2481
1744
|
});
|
|
2482
1745
|
ws.end(Buffer.from(content, "utf-8"));
|
|
2483
1746
|
});
|
|
2484
1747
|
});
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
resolve(`Error: ${e.message}`);
|
|
2490
|
-
}
|
|
2491
|
-
});
|
|
2492
|
-
ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 6e4 });
|
|
2493
|
-
});
|
|
1748
|
+
} catch (e) {
|
|
1749
|
+
cleanup?.();
|
|
1750
|
+
return `Error: ${e.message}`;
|
|
1751
|
+
}
|
|
2494
1752
|
}
|
|
2495
|
-
async function sftpDelete(opts, filePath) {
|
|
1753
|
+
async function sftpDelete(opts, filePath, proxy) {
|
|
2496
1754
|
const safe = sanitizePath(filePath);
|
|
2497
1755
|
assertWritablePath(safe);
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
1756
|
+
let cleanup;
|
|
1757
|
+
try {
|
|
1758
|
+
const { client, cleanup: c } = await connectSshClient(opts, proxy, 3e4);
|
|
1759
|
+
cleanup = c;
|
|
1760
|
+
return await new Promise((resolve) => {
|
|
1761
|
+
const timer = setTimeout(() => {
|
|
1762
|
+
cleanup?.();
|
|
2505
1763
|
resolve("Error: timeout");
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
ssh.sftp((err, sftp) => {
|
|
1764
|
+
cleanup = void 0;
|
|
1765
|
+
}, 3e4);
|
|
1766
|
+
client.sftp((err, sftp) => {
|
|
2510
1767
|
if (err) {
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
resolve(`Error: ${err.message}`);
|
|
2516
|
-
}
|
|
1768
|
+
clearTimeout(timer);
|
|
1769
|
+
cleanup?.();
|
|
1770
|
+
cleanup = void 0;
|
|
1771
|
+
resolve(`Error: ${err.message}`);
|
|
2517
1772
|
return;
|
|
2518
1773
|
}
|
|
2519
|
-
sftp.unlink(safe, (
|
|
2520
|
-
if (
|
|
2521
|
-
sftp.rmdir(safe, (
|
|
2522
|
-
done = true;
|
|
1774
|
+
sftp.unlink(safe, (unlinkErr) => {
|
|
1775
|
+
if (unlinkErr) {
|
|
1776
|
+
sftp.rmdir(safe, (rmdirErr) => {
|
|
2523
1777
|
clearTimeout(timer);
|
|
2524
|
-
|
|
2525
|
-
|
|
1778
|
+
cleanup?.();
|
|
1779
|
+
cleanup = void 0;
|
|
1780
|
+
resolve(rmdirErr ? `Error: ${unlinkErr.message}` : `Deleted directory ${safe}`);
|
|
2526
1781
|
});
|
|
2527
1782
|
} else {
|
|
2528
|
-
done = true;
|
|
2529
1783
|
clearTimeout(timer);
|
|
2530
|
-
|
|
1784
|
+
cleanup?.();
|
|
1785
|
+
cleanup = void 0;
|
|
2531
1786
|
resolve(`Deleted file ${safe}`);
|
|
2532
1787
|
}
|
|
2533
1788
|
});
|
|
2534
1789
|
});
|
|
2535
1790
|
});
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
resolve(`Error: ${e.message}`);
|
|
2541
|
-
}
|
|
2542
|
-
});
|
|
2543
|
-
ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 3e4 });
|
|
2544
|
-
});
|
|
1791
|
+
} catch (e) {
|
|
1792
|
+
cleanup?.();
|
|
1793
|
+
return `Error: ${e.message}`;
|
|
1794
|
+
}
|
|
2545
1795
|
}
|
|
2546
1796
|
var BLOCKED_COMMANDS = [
|
|
2547
1797
|
"rm -rf /",
|
|
@@ -3059,25 +2309,25 @@ ${result.stdout}`);
|
|
|
3059
2309
|
${result.stderr}`);
|
|
3060
2310
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
3061
2311
|
}
|
|
3062
|
-
// ----- SFTP
|
|
2312
|
+
// ----- SFTP -----
|
|
3063
2313
|
case "sftp-list": {
|
|
3064
|
-
const { conn } = await getServerConnection(String(a.serverId));
|
|
3065
|
-
const listing = await sftpReaddir(conn, String(a.path || "/"));
|
|
2314
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2315
|
+
const listing = await sftpReaddir(conn, String(a.path || "/"), proxy);
|
|
3066
2316
|
return { content: [{ type: "text", text: listing }] };
|
|
3067
2317
|
}
|
|
3068
2318
|
case "sftp-read": {
|
|
3069
|
-
const { conn } = await getServerConnection(String(a.serverId));
|
|
3070
|
-
const content = await sftpRead(conn, String(a.path));
|
|
2319
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2320
|
+
const content = await sftpRead(conn, String(a.path), proxy);
|
|
3071
2321
|
return { content: [{ type: "text", text: content }] };
|
|
3072
2322
|
}
|
|
3073
2323
|
case "sftp-write": {
|
|
3074
|
-
const { conn } = await getServerConnection(String(a.serverId));
|
|
3075
|
-
const result = await sftpWrite(conn, String(a.path), String(a.content));
|
|
2324
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2325
|
+
const result = await sftpWrite(conn, String(a.path), String(a.content), proxy);
|
|
3076
2326
|
return { content: [{ type: "text", text: result }] };
|
|
3077
2327
|
}
|
|
3078
2328
|
case "sftp-delete": {
|
|
3079
|
-
const { conn } = await getServerConnection(String(a.serverId));
|
|
3080
|
-
const result = await sftpDelete(conn, String(a.path));
|
|
2329
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2330
|
+
const result = await sftpDelete(conn, String(a.path), proxy);
|
|
3081
2331
|
return { content: [{ type: "text", text: result }] };
|
|
3082
2332
|
}
|
|
3083
2333
|
// ----- Docker -----
|
|
@@ -3469,7 +2719,7 @@ ${lines.join("\n")}` }] };
|
|
|
3469
2719
|
return handleTriggerTool(name, a, { sshExec, getServerConnection });
|
|
3470
2720
|
}
|
|
3471
2721
|
if (AGENT_TOOL_NAMES.has(name)) {
|
|
3472
|
-
return handleAgentTool(name, a
|
|
2722
|
+
return handleAgentTool(name, a);
|
|
3473
2723
|
}
|
|
3474
2724
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
3475
2725
|
}
|
|
@@ -3500,16 +2750,6 @@ async function main() {
|
|
|
3500
2750
|
console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
|
|
3501
2751
|
const transports = /* @__PURE__ */ new Map();
|
|
3502
2752
|
const REST_TOOL_MAP = {
|
|
3503
|
-
"/api/report-coverage": "agent-report-coverage",
|
|
3504
|
-
"/api/report-finding": "agent-report-finding",
|
|
3505
|
-
"/api/save-documentation": "agent-save-documentation",
|
|
3506
|
-
"/api/list-findings": "agent-list-findings",
|
|
3507
|
-
"/api/get-documentation": "agent-get-documentation",
|
|
3508
|
-
"/api/validate-suggestions": "agent-validate-suggestions",
|
|
3509
|
-
"/api/check-lead-exists": "agent-check-lead-exists",
|
|
3510
|
-
"/api/save-lead": "agent-save-lead",
|
|
3511
|
-
"/api/save-email-draft": "agent-save-email-draft",
|
|
3512
|
-
"/api/complete-target": "agent-complete-target",
|
|
3513
2753
|
"/api/web-search": "web-search",
|
|
3514
2754
|
"/api/web-fetch": "web-fetch"
|
|
3515
2755
|
};
|
|
@@ -3528,7 +2768,7 @@ async function main() {
|
|
|
3528
2768
|
return;
|
|
3529
2769
|
}
|
|
3530
2770
|
try {
|
|
3531
|
-
const result = await handleAgentTool(restToolName, toolArgs
|
|
2771
|
+
const result = await handleAgentTool(restToolName, toolArgs);
|
|
3532
2772
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3533
2773
|
res.end(JSON.stringify({ ok: true, result }));
|
|
3534
2774
|
} catch (err) {
|