@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 CHANGED
@@ -376,339 +376,13 @@ ${rawJson.substring(0, 500)}`
376
376
  }
377
377
 
378
378
  // src/agent-tools.ts
379
- var VALID_FINDING_TYPES = /* @__PURE__ */ new Set([
380
- "missing_docs",
381
- "inaccurate",
382
- "incomplete",
383
- "outdated",
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
- return await res.text();
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 textSimilarity(a, b) {
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 agent tool: ${name}` }] };
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
- return new Promise((resolve) => {
2317
- const ssh = new Client();
2318
- let done = false;
2319
- const timer = setTimeout(() => {
2320
- if (!done) {
2321
- done = true;
2322
- ssh.end();
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
- }, 3e4);
2326
- ssh.on("ready", () => {
2327
- ssh.sftp((err, sftp) => {
1617
+ cleanup = void 0;
1618
+ }, 3e4);
1619
+ client.sftp((err, sftp) => {
2328
1620
  if (err) {
2329
- if (!done) {
2330
- done = true;
2331
- clearTimeout(timer);
2332
- ssh.end();
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
- ssh.end();
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
- ssh.end();
1642
+ cleanup?.();
1643
+ cleanup = void 0;
2353
1644
  resolve(entries.join("\n"));
2354
1645
  });
2355
1646
  });
2356
1647
  });
2357
- ssh.on("error", (e) => {
2358
- if (!done) {
2359
- done = true;
2360
- clearTimeout(timer);
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
- return new Promise((resolve) => {
2370
- const ssh = new Client();
2371
- let done = false;
2372
- const timer = setTimeout(() => {
2373
- if (!done) {
2374
- done = true;
2375
- ssh.end();
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
- }, 6e4);
2379
- ssh.on("ready", () => {
2380
- ssh.sftp((err, sftp) => {
1663
+ cleanup = void 0;
1664
+ }, 6e4);
1665
+ client.sftp((err, sftp) => {
2381
1666
  if (err) {
2382
- if (!done) {
2383
- done = true;
2384
- clearTimeout(timer);
2385
- ssh.end();
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
- if (!done) {
2393
- done = true;
2394
- clearTimeout(timer);
2395
- ssh.end();
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
- if (!done) {
2402
- done = true;
2403
- clearTimeout(timer);
2404
- ssh.end();
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", (c) => chunks.push(c));
1690
+ rs.on("data", (ch) => chunks.push(ch));
2412
1691
  rs.on("end", () => {
2413
- if (!done) {
2414
- done = true;
2415
- clearTimeout(timer);
2416
- ssh.end();
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
- if (!done) {
2422
- done = true;
2423
- clearTimeout(timer);
2424
- ssh.end();
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
- ssh.on("error", (e) => {
2432
- if (!done) {
2433
- done = true;
2434
- clearTimeout(timer);
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
- return new Promise((resolve) => {
2445
- const ssh = new Client();
2446
- let done = false;
2447
- const timer = setTimeout(() => {
2448
- if (!done) {
2449
- done = true;
2450
- ssh.end();
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
- }, 6e4);
2454
- ssh.on("ready", () => {
2455
- ssh.sftp((err, sftp) => {
1722
+ cleanup = void 0;
1723
+ }, 6e4);
1724
+ client.sftp((err, sftp) => {
2456
1725
  if (err) {
2457
- if (!done) {
2458
- done = true;
2459
- clearTimeout(timer);
2460
- ssh.end();
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
- if (!done) {
2468
- done = true;
2469
- clearTimeout(timer);
2470
- ssh.end();
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
- if (!done) {
2476
- done = true;
2477
- clearTimeout(timer);
2478
- ssh.end();
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
- ssh.on("error", (e) => {
2486
- if (!done) {
2487
- done = true;
2488
- clearTimeout(timer);
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
- return new Promise((resolve) => {
2499
- const ssh = new Client();
2500
- let done = false;
2501
- const timer = setTimeout(() => {
2502
- if (!done) {
2503
- done = true;
2504
- ssh.end();
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
- }, 3e4);
2508
- ssh.on("ready", () => {
2509
- ssh.sftp((err, sftp) => {
1764
+ cleanup = void 0;
1765
+ }, 3e4);
1766
+ client.sftp((err, sftp) => {
2510
1767
  if (err) {
2511
- if (!done) {
2512
- done = true;
2513
- clearTimeout(timer);
2514
- ssh.end();
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, (err2) => {
2520
- if (err2) {
2521
- sftp.rmdir(safe, (err22) => {
2522
- done = true;
1774
+ sftp.unlink(safe, (unlinkErr) => {
1775
+ if (unlinkErr) {
1776
+ sftp.rmdir(safe, (rmdirErr) => {
2523
1777
  clearTimeout(timer);
2524
- ssh.end();
2525
- resolve(err22 ? `Error: ${err2.message}` : `Deleted directory ${safe}`);
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
- ssh.end();
1784
+ cleanup?.();
1785
+ cleanup = void 0;
2531
1786
  resolve(`Deleted file ${safe}`);
2532
1787
  }
2533
1788
  });
2534
1789
  });
2535
1790
  });
2536
- ssh.on("error", (e) => {
2537
- if (!done) {
2538
- done = true;
2539
- clearTimeout(timer);
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 (no proxy support yet — direct connection only) -----
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, { supabase, workspaceId: agentWorkspaceId });
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, { supabase, workspaceId: agentWorkspaceId });
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) {