@mgsoftwarebv/mg-dashboard-mcp 2.6.3 → 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)}`;
@@ -1088,499 +746,8 @@ async function webFetch(url, extractLinks) {
1088
746
  clearTimeout(timeout);
1089
747
  }
1090
748
  }
1091
- function textSimilarity(a, b) {
1092
- if (a === b) return 1;
1093
- const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
1094
- const na = norm(a);
1095
- const nb = norm(b);
1096
- if (na === nb) return 1;
1097
- if (na.length < 3 || nb.length < 3) return na === nb ? 1 : 0;
1098
- const trigrams = (s) => {
1099
- const set = /* @__PURE__ */ new Set();
1100
- for (let i = 0; i <= s.length - 3; i++) set.add(s.slice(i, i + 3));
1101
- return set;
1102
- };
1103
- const setA = trigrams(na);
1104
- const setB = trigrams(nb);
1105
- let intersection = 0;
1106
- for (const t of setA) if (setB.has(t)) intersection++;
1107
- const union = setA.size + setB.size - intersection;
1108
- return union === 0 ? 0 : intersection / union;
1109
- }
1110
- async function handleAgentTool(name, args2, deps) {
1111
- const { supabase: supabase2 } = deps;
749
+ async function handleAgentTool(name, args2) {
1112
750
  switch (name) {
1113
- // -----------------------------------------------------------------
1114
- case "agent-report-coverage": {
1115
- const repoSlug = sanitizeString(args2.repo_slug, 200);
1116
- const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
1117
- const entries = Array.isArray(args2.entries) ? args2.entries : [];
1118
- if (!repoSlug) throw new Error("repo_slug is required");
1119
- if (entries.length === 0) throw new Error("entries array must not be empty");
1120
- const wsId = deps.workspaceId;
1121
- const scanCommit = wsId ? `agent-scan-${wsId.slice(0, 8)}` : `agent-scan-${Date.now().toString(36)}`;
1122
- let upserted = 0;
1123
- let errors = 0;
1124
- for (const entry of entries) {
1125
- const { error } = await supabase2.from("doc_coverage").upsert(
1126
- {
1127
- refront_project_id: refrontProjectId,
1128
- repo_slug: repoSlug,
1129
- path: sanitizeString(entry.path, 500),
1130
- total_functions: clamp(Number(entry.total_functions) || 0, 0, 99999),
1131
- documented_functions: clamp(Number(entry.documented_functions) || 0, 0, 99999),
1132
- total_types: clamp(Number(entry.total_types) || 0, 0, 99999),
1133
- documented_types: clamp(Number(entry.documented_types) || 0, 0, 99999),
1134
- total_endpoints: clamp(Number(entry.total_endpoints) || 0, 0, 99999),
1135
- documented_endpoints: clamp(Number(entry.documented_endpoints) || 0, 0, 99999),
1136
- scan_commit: scanCommit,
1137
- scanned_at: (/* @__PURE__ */ new Date()).toISOString()
1138
- },
1139
- { onConflict: "repo_slug,path" }
1140
- );
1141
- if (error) errors++;
1142
- else upserted++;
1143
- }
1144
- return {
1145
- content: [{
1146
- type: "text",
1147
- text: `Coverage reported: ${upserted} entries upserted${errors > 0 ? `, ${errors} errors` : ""}`
1148
- }]
1149
- };
1150
- }
1151
- // -----------------------------------------------------------------
1152
- case "agent-report-finding": {
1153
- const repoSlug = sanitizeString(args2.repo_slug, 200);
1154
- const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
1155
- const category = args2.category;
1156
- const findings = Array.isArray(args2.findings) ? args2.findings : [];
1157
- if (!repoSlug) throw new Error("repo_slug is required");
1158
- if (!category || !["scan_findings", "perf_audit"].includes(category)) {
1159
- throw new Error('category must be "scan_findings" or "perf_audit"');
1160
- }
1161
- if (findings.length === 0) throw new Error("findings array must not be empty");
1162
- const SIMILARITY_THRESHOLD = 0.55;
1163
- const MAX_OPEN_PER_TYPE = 30;
1164
- 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);
1165
- const existing = existingFindings ?? [];
1166
- const typeCountMap = /* @__PURE__ */ new Map();
1167
- for (const e of existing) {
1168
- if (e.status !== "open") continue;
1169
- const key = `${e.category}:${e.type}`;
1170
- typeCountMap.set(key, (typeCountMap.get(key) ?? 0) + 1);
1171
- }
1172
- let inserted = 0;
1173
- let deduplicated = 0;
1174
- let grouped = 0;
1175
- let errors = 0;
1176
- const overflowBucket = /* @__PURE__ */ new Map();
1177
- for (const f of findings) {
1178
- const findingType = VALID_FINDING_TYPES.has(f.type) ? f.type : "improvement";
1179
- const severity = VALID_SEVERITIES.has(f.severity) ? f.severity : "info";
1180
- const description = sanitizeString(f.description, 2e3);
1181
- const filePath = f.file_path ? sanitizeString(f.file_path, 500) : null;
1182
- if (!description) continue;
1183
- const isDuplicate = existing.some((e) => {
1184
- if (e.type !== findingType) return false;
1185
- if (filePath && e.file_path === filePath && ["dismissed", "resolved"].includes(e.status)) {
1186
- return true;
1187
- }
1188
- if (filePath && e.file_path === filePath) {
1189
- return textSimilarity(e.description ?? "", description) > 0.4;
1190
- }
1191
- return textSimilarity(e.description ?? "", description) > SIMILARITY_THRESHOLD;
1192
- });
1193
- if (isDuplicate) {
1194
- deduplicated++;
1195
- continue;
1196
- }
1197
- const typeKey = `${category}:${findingType}`;
1198
- const currentCount = typeCountMap.get(typeKey) ?? 0;
1199
- if (currentCount >= MAX_OPEN_PER_TYPE) {
1200
- const bucket = overflowBucket.get(typeKey) ?? [];
1201
- bucket.push(`${filePath ? `${filePath}: ` : ""}${description.slice(0, 120)}`);
1202
- overflowBucket.set(typeKey, bucket);
1203
- grouped++;
1204
- continue;
1205
- }
1206
- const { error } = await supabase2.from("doc_suggestion").insert({
1207
- repo_slug: repoSlug,
1208
- refront_project_id: refrontProjectId,
1209
- category,
1210
- type: findingType,
1211
- severity,
1212
- description,
1213
- file_path: filePath,
1214
- suggested_fix: f.suggested_fix ? sanitizeString(f.suggested_fix, 5e3) : null,
1215
- status: "open"
1216
- });
1217
- if (error) {
1218
- errors++;
1219
- } else {
1220
- inserted++;
1221
- typeCountMap.set(typeKey, currentCount + 1);
1222
- existing.push({ id: "", type: findingType, description, file_path: filePath, severity, category, status: "open" });
1223
- }
1224
- }
1225
- for (const [typeKey, items] of overflowBucket) {
1226
- const [cat, type] = typeKey.split(":");
1227
- const summary = `Gegroepeerd (${items.length} items):
1228
- ${items.map((i) => `\u2022 ${i}`).join("\n")}`;
1229
- await supabase2.from("doc_suggestion").insert({
1230
- repo_slug: repoSlug,
1231
- refront_project_id: refrontProjectId,
1232
- category: cat,
1233
- type,
1234
- severity: "info",
1235
- description: summary.slice(0, 5e3),
1236
- file_path: null,
1237
- suggested_fix: null,
1238
- status: "open"
1239
- });
1240
- }
1241
- const parts = [`${inserted} inserted`];
1242
- if (deduplicated > 0) parts.push(`${deduplicated} deduplicated`);
1243
- if (grouped > 0) parts.push(`${grouped} grouped into ${overflowBucket.size} summary row(s)`);
1244
- if (errors > 0) parts.push(`${errors} errors`);
1245
- return {
1246
- content: [{
1247
- type: "text",
1248
- text: `Findings reported under ${category}: ${parts.join(", ")}`
1249
- }]
1250
- };
1251
- }
1252
- // -----------------------------------------------------------------
1253
- case "agent-save-documentation": {
1254
- const repoSlug = sanitizeString(args2.repo_slug, 200);
1255
- const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
1256
- const scope = VALID_SCOPES.has(args2.scope) ? args2.scope : "module";
1257
- const path = sanitizeString(args2.path, 500);
1258
- const title = sanitizeString(args2.title, 500);
1259
- const content = sanitizeString(args2.content, 1e5);
1260
- const reviewStatus = VALID_REVIEW_STATUSES.has(args2.review_status) ? args2.review_status : "pending";
1261
- if (!repoSlug) throw new Error("repo_slug is required");
1262
- if (!path) throw new Error("path is required");
1263
- if (!title) throw new Error("title is required");
1264
- if (!content) throw new Error("content is required");
1265
- const { data: existing } = await supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug).eq("scope", scope).eq("path", path).maybeSingle();
1266
- if (existing) {
1267
- const { error: error2 } = await supabase2.from("project_documentation").update({
1268
- title,
1269
- content,
1270
- review_status: reviewStatus,
1271
- pdf_storage_path: null,
1272
- pdf_compiled_at: null,
1273
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
1274
- }).eq("id", existing.id);
1275
- if (error2) throw new Error(`Failed to update documentation: ${error2.message}`);
1276
- return { content: [{ type: "text", text: `Documentation updated: ${title} (${scope}/${path}). PDF compilation pending.` }] };
1277
- }
1278
- const generatedBy = deps.workspaceId ? `agent-${deps.workspaceId.slice(0, 8)}` : "agent-mcp";
1279
- const { error } = await supabase2.from("project_documentation").insert({
1280
- refront_project_id: refrontProjectId,
1281
- repo_slug: repoSlug,
1282
- scope,
1283
- path,
1284
- title,
1285
- content,
1286
- generated_by: generatedBy,
1287
- review_status: reviewStatus,
1288
- pdf_storage_path: null,
1289
- pdf_compiled_at: null
1290
- });
1291
- if (error) throw new Error(`Failed to save documentation: ${error.message}`);
1292
- return { content: [{ type: "text", text: `Documentation saved: ${title} (${scope}/${path}). PDF compilation pending.` }] };
1293
- }
1294
- // -----------------------------------------------------------------
1295
- case "agent-list-findings": {
1296
- const repoSlug = sanitizeString(args2.repo_slug, 200);
1297
- if (!repoSlug) throw new Error("repo_slug is required");
1298
- const limit = clamp(Number(args2.limit) || 50, 1, 200);
1299
- 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);
1300
- if (args2.type && VALID_FINDING_TYPES.has(args2.type)) {
1301
- query = query.eq("type", args2.type);
1302
- }
1303
- if (args2.severity && VALID_SEVERITIES.has(args2.severity)) {
1304
- query = query.eq("severity", args2.severity);
1305
- }
1306
- if (args2.status) {
1307
- query = query.eq("status", args2.status);
1308
- }
1309
- const { data: findings, error } = await query;
1310
- if (error) throw new Error(`Failed to query findings: ${error.message}`);
1311
- if (!findings || findings.length === 0) {
1312
- return { content: [{ type: "text", text: `No findings found for repo "${repoSlug}"` }] };
1313
- }
1314
- const summary = findings.map(
1315
- (f) => `[${f.severity}] ${f.type}: ${String(f.description).slice(0, 120)}${f.file_path ? ` (${f.file_path})` : ""} \u2014 ${f.status}`
1316
- ).join("\n");
1317
- return {
1318
- content: [{
1319
- type: "text",
1320
- text: `${findings.length} findings for "${repoSlug}":
1321
-
1322
- ${summary}`
1323
- }]
1324
- };
1325
- }
1326
- // -----------------------------------------------------------------
1327
- case "agent-get-documentation": {
1328
- const repoSlug = sanitizeString(args2.repo_slug, 200);
1329
- if (!repoSlug) throw new Error("repo_slug is required");
1330
- const limit = clamp(Number(args2.limit) || 20, 1, 100);
1331
- 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);
1332
- if (args2.scope && VALID_SCOPES.has(args2.scope)) {
1333
- query = query.eq("scope", args2.scope);
1334
- }
1335
- if (args2.path) {
1336
- query = query.eq("path", sanitizeString(args2.path, 500));
1337
- }
1338
- const { data: docs, error } = await query;
1339
- if (error) throw new Error(`Failed to query documentation: ${error.message}`);
1340
- if (!docs || docs.length === 0) {
1341
- return { content: [{ type: "text", text: `No documentation found for repo "${repoSlug}"` }] };
1342
- }
1343
- const output = docs.map((d) => {
1344
- const fullContent = String(d.content || "");
1345
- const isChangelog = d.scope === "changelog";
1346
- const maxPreview = isChangelog ? 5e4 : 500;
1347
- const preview = fullContent.slice(0, maxPreview);
1348
- return [
1349
- `## ${d.title} (${d.scope}/${d.path})`,
1350
- `Status: ${d.review_status} | By: ${d.generated_by || "unknown"}`,
1351
- `Updated: ${d.updated_at || d.created_at}`,
1352
- "",
1353
- preview + (fullContent.length > maxPreview ? "\n...(truncated)" : ""),
1354
- ""
1355
- ].join("\n");
1356
- }).join("\n---\n\n");
1357
- return {
1358
- content: [{ type: "text", text: `${docs.length} docs for "${repoSlug}":
1359
-
1360
- ${output}` }]
1361
- };
1362
- }
1363
- // -----------------------------------------------------------------
1364
- case "agent-validate-suggestions": {
1365
- const repoSlug = sanitizeString(args2.repo_slug, 200);
1366
- const results = Array.isArray(args2.results) ? args2.results : [];
1367
- if (!repoSlug) throw new Error("repo_slug is required");
1368
- if (results.length === 0) throw new Error("results array must not be empty");
1369
- const validatedBy = deps.workspaceId ? `validator-${deps.workspaceId.slice(0, 8)}` : "validator-mcp";
1370
- const now = (/* @__PURE__ */ new Date()).toISOString();
1371
- let dismissed = 0;
1372
- let adjusted = 0;
1373
- let validated = 0;
1374
- let errors = 0;
1375
- let resolved = 0;
1376
- for (const r of results) {
1377
- const id = sanitizeString(r.suggestion_id, 100);
1378
- const verdict = r.verdict;
1379
- const reason = sanitizeString(r.reason, 2e3);
1380
- if (!id || !["valid", "invalid", "adjusted", "resolved"].includes(verdict)) {
1381
- errors++;
1382
- continue;
1383
- }
1384
- if (verdict === "resolved") {
1385
- const { error } = await supabase2.from("doc_suggestion").update({
1386
- status: "resolved",
1387
- validated_at: now,
1388
- validated_by: validatedBy
1389
- }).eq("id", id).eq("repo_slug", repoSlug);
1390
- if (error) errors++;
1391
- else resolved++;
1392
- } else if (verdict === "invalid") {
1393
- const { error } = await supabase2.from("doc_suggestion").update({
1394
- status: "dismissed",
1395
- dismissed_reason: reason || "Dismissed by validation agent",
1396
- validated_at: now,
1397
- validated_by: validatedBy
1398
- }).eq("id", id).eq("repo_slug", repoSlug);
1399
- if (error) errors++;
1400
- else dismissed++;
1401
- } else if (verdict === "adjusted") {
1402
- const updates = {
1403
- validated_at: now,
1404
- validated_by: validatedBy
1405
- };
1406
- if (r.adjusted_description) {
1407
- updates.description = sanitizeString(r.adjusted_description, 2e3);
1408
- }
1409
- if (r.adjusted_severity && VALID_SEVERITIES.has(r.adjusted_severity)) {
1410
- updates.severity = r.adjusted_severity;
1411
- }
1412
- if (r.adjusted_suggested_fix) {
1413
- updates.suggested_fix = sanitizeString(r.adjusted_suggested_fix, 5e3);
1414
- }
1415
- const { error } = await supabase2.from("doc_suggestion").update(updates).eq("id", id).eq("repo_slug", repoSlug);
1416
- if (error) errors++;
1417
- else adjusted++;
1418
- } else {
1419
- const { error } = await supabase2.from("doc_suggestion").update({ validated_at: now, validated_by: validatedBy }).eq("id", id).eq("repo_slug", repoSlug);
1420
- if (error) errors++;
1421
- else validated++;
1422
- }
1423
- }
1424
- const parts = [];
1425
- if (resolved > 0) parts.push(`${resolved} resolved`);
1426
- if (validated > 0) parts.push(`${validated} valid`);
1427
- if (dismissed > 0) parts.push(`${dismissed} dismissed`);
1428
- if (adjusted > 0) parts.push(`${adjusted} adjusted`);
1429
- if (errors > 0) parts.push(`${errors} errors`);
1430
- return {
1431
- content: [{
1432
- type: "text",
1433
- text: `Validation complete for "${repoSlug}": ${parts.join(", ")}`
1434
- }]
1435
- };
1436
- }
1437
- // -----------------------------------------------------------------
1438
- // Lead Generation tools
1439
- // -----------------------------------------------------------------
1440
- case "agent-check-lead-exists": {
1441
- const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
1442
- const companyName = sanitizeString(args2.company_name, 500);
1443
- if (!websiteUrl && !companyName) {
1444
- throw new Error("At least one of website_url or company_name is required");
1445
- }
1446
- const checks = [];
1447
- if (websiteUrl) {
1448
- const { data: urlMatch } = await supabase2.from("lead").select("id, company_name, website_url").eq("website_url", websiteUrl).maybeSingle();
1449
- if (urlMatch) {
1450
- return {
1451
- content: [{
1452
- type: "text",
1453
- text: `DUPLICATE: Lead already exists (URL match). ID: ${urlMatch.id}, Company: ${urlMatch.company_name}. SKIP this company.`
1454
- }]
1455
- };
1456
- }
1457
- checks.push(`URL "${websiteUrl}" not found`);
1458
- }
1459
- if (companyName) {
1460
- const normalized = normalizeCompanyName(companyName);
1461
- if (normalized.length >= 3) {
1462
- const { data: nameMatches } = await supabase2.from("lead").select("id, company_name, website_url").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
1463
- if (nameMatches) {
1464
- for (const match of nameMatches) {
1465
- const matchNorm = normalizeCompanyName(match.company_name);
1466
- if (textSimilarity(normalized, matchNorm) > 0.6) {
1467
- return {
1468
- content: [{
1469
- type: "text",
1470
- text: `DUPLICATE: Similar company found. ID: ${match.id}, Name: "${match.company_name}" (${match.website_url}). SKIP this company.`
1471
- }]
1472
- };
1473
- }
1474
- }
1475
- }
1476
- }
1477
- checks.push(`Name "${companyName}" has no similar matches`);
1478
- }
1479
- return {
1480
- content: [{
1481
- type: "text",
1482
- text: `NOT FOUND: No duplicate detected. ${checks.join(". ")}. Safe to proceed.`
1483
- }]
1484
- };
1485
- }
1486
- case "agent-save-lead": {
1487
- const companyName = sanitizeString(args2.company_name, 500);
1488
- const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
1489
- if (!companyName) throw new Error("company_name is required");
1490
- if (!websiteUrl) throw new Error("website_url is required");
1491
- const { data: existing } = await supabase2.from("lead").select("id, company_name").eq("website_url", websiteUrl).maybeSingle();
1492
- if (existing) {
1493
- return {
1494
- content: [{
1495
- type: "text",
1496
- text: `Lead already exists (URL match). ID: ${existing.id}, Company: "${existing.company_name}". Skipped.`
1497
- }]
1498
- };
1499
- }
1500
- const normalized = normalizeCompanyName(companyName);
1501
- if (normalized.length >= 3) {
1502
- const { data: nameMatches } = await supabase2.from("lead").select("id, company_name").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
1503
- if (nameMatches) {
1504
- for (const match of nameMatches) {
1505
- if (textSimilarity(normalized, normalizeCompanyName(match.company_name)) > 0.6) {
1506
- return {
1507
- content: [{
1508
- type: "text",
1509
- text: `Lead already exists (name match). ID: ${match.id}, Name: "${match.company_name}". Skipped.`
1510
- }]
1511
- };
1512
- }
1513
- }
1514
- }
1515
- }
1516
- const fitScore = args2.fit_score ? clamp(Number(args2.fit_score), 1, 10) : null;
1517
- const { data: inserted, error } = await supabase2.from("lead").insert({
1518
- company_name: companyName,
1519
- website_url: websiteUrl,
1520
- industry: args2.industry ? sanitizeString(args2.industry, 200) : null,
1521
- region: args2.region ? sanitizeString(args2.region, 200) : null,
1522
- description: args2.description ? sanitizeString(args2.description, 2e3) : null,
1523
- potential_fit: args2.potential_fit ? sanitizeString(args2.potential_fit, 2e3) : null,
1524
- fit_score: fitScore,
1525
- estimated_company_size: args2.estimated_company_size ? sanitizeString(args2.estimated_company_size, 50) : null,
1526
- kvk_number: args2.kvk_number ? sanitizeString(args2.kvk_number, 20) : null,
1527
- contact_name: args2.contact_name ? sanitizeString(args2.contact_name, 200) : null,
1528
- contact_role: args2.contact_role ? sanitizeString(args2.contact_role, 200) : null,
1529
- contact_email: args2.contact_email ? sanitizeString(args2.contact_email, 200) : null,
1530
- contact_phone: args2.contact_phone ? sanitizeString(args2.contact_phone, 50) : null,
1531
- contact_linkedin: args2.contact_linkedin ? sanitizeString(args2.contact_linkedin, 500) : null,
1532
- general_email: args2.general_email ? sanitizeString(args2.general_email, 200) : null,
1533
- general_phone: args2.general_phone ? sanitizeString(args2.general_phone, 50) : null,
1534
- source_url: args2.source_url ? sanitizeString(args2.source_url, 500) : null,
1535
- target_id: args2.target_id ? sanitizeString(args2.target_id, 100) : null,
1536
- status: "new"
1537
- }).select("id").single();
1538
- if (error) throw new Error(`Failed to save lead: ${error.message}`);
1539
- return {
1540
- content: [{
1541
- type: "text",
1542
- text: `Lead saved: "${companyName}" (${websiteUrl}). ID: ${inserted.id}. Fit score: ${fitScore ?? "N/A"}.`
1543
- }]
1544
- };
1545
- }
1546
- case "agent-save-email-draft": {
1547
- const leadId = sanitizeString(args2.lead_id, 100);
1548
- const subject = sanitizeString(args2.subject, 200);
1549
- let body = sanitizeString(args2.body, 5e3);
1550
- const tone = sanitizeString(args2.tone, 50) || "professional";
1551
- if (!leadId) throw new Error("lead_id is required");
1552
- if (!subject) throw new Error("subject is required");
1553
- if (!body) throw new Error("body is required");
1554
- body = body.replace(/[\u2013\u2014]/g, ",");
1555
- const { data: inserted, error } = await supabase2.from("lead_email_draft").insert({ lead_id: leadId, subject, body, tone, status: "draft" }).select("id").single();
1556
- if (error) throw new Error(`Failed to save email draft: ${error.message}`);
1557
- await supabase2.from("lead").update({ status: "email_drafted", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", leadId).eq("status", "new");
1558
- return {
1559
- content: [{
1560
- type: "text",
1561
- text: `Email draft saved for lead ${leadId}. Draft ID: ${inserted.id}. Subject: "${subject}".`
1562
- }]
1563
- };
1564
- }
1565
- case "agent-complete-target": {
1566
- const targetId = sanitizeString(args2.target_id, 100);
1567
- const resultsCount = clamp(Number(args2.results_count) || 0, 0, 9999);
1568
- if (!targetId) throw new Error("target_id is required");
1569
- const { error } = await supabase2.from("lead_generation_target").update({
1570
- results_count: resultsCount,
1571
- assigned_workspace_id: null
1572
- }).eq("id", targetId);
1573
- if (error) throw new Error(`Failed to update target: ${error.message}`);
1574
- return {
1575
- content: [{
1576
- type: "text",
1577
- text: `Target ${targetId} updated: ${resultsCount} leads found. Workspace released.`
1578
- }]
1579
- };
1580
- }
1581
- // -----------------------------------------------------------------
1582
- // Web Tools
1583
- // -----------------------------------------------------------------
1584
751
  case "web-search": {
1585
752
  const query = sanitizeString(args2.query, 500);
1586
753
  if (!query) throw new Error("query is required");
@@ -1635,9 +802,6 @@ LinkedIn: ${pageLinkedIn.join(", ")}`;
1635
802
  }
1636
803
  return { content: [{ type: "text", text }] };
1637
804
  }
1638
- // -----------------------------------------------------------------
1639
- // Web Find Contacts — SOTA multi-page crawler + email extraction
1640
- // -----------------------------------------------------------------
1641
805
  case "web-find-contacts": {
1642
806
  const inputUrl = sanitizeString(args2.url, 2e3);
1643
807
  if (!inputUrl) throw new Error("url is required");
@@ -1782,9 +946,8 @@ LinkedIn: ${pageLinkedIn.join(", ")}`;
1782
946
  for (const p of failedPages) lines.push(` [--] ${p}`);
1783
947
  return { content: [{ type: "text", text: lines.join("\n") }] };
1784
948
  }
1785
- // -----------------------------------------------------------------
1786
949
  default:
1787
- return { content: [{ type: "text", text: `Unknown agent tool: ${name}` }] };
950
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
1788
951
  }
1789
952
  }
1790
953
 
@@ -1798,7 +961,6 @@ var supabaseUrl = getArg("supabase-url") || process.env.SUPABASE_URL;
1798
961
  var supabaseKey = getArg("supabase-key") || process.env.SUPABASE_SERVICE_ROLE_KEY;
1799
962
  var encryptionKey = getArg("encryption-key") || process.env.ENCRYPTION_KEY;
1800
963
  var mijnhostApiKey = getArg("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
1801
- var agentWorkspaceId = getArg("workspace-id") || process.env.AGENT_WORKSPACE_ID || null;
1802
964
  var httpMode = args.includes("--http");
1803
965
  var httpPort = Number(getArg("port")) || 3100;
1804
966
  if (!apiKey) {
@@ -1861,7 +1023,6 @@ var MODULE_KEYS = [
1861
1023
  "supabase",
1862
1024
  "wiki",
1863
1025
  "ci_cd",
1864
- "source_control",
1865
1026
  "domains",
1866
1027
  "settings",
1867
1028
  "agent_reporting"
@@ -2360,6 +1521,69 @@ function sshExecViaProxy(proxyOpts, targetOpts, command) {
2360
1521
  });
2361
1522
  });
2362
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
+ }
2363
1587
  function sanitizePath(path) {
2364
1588
  let normalized = path.replace(/\\/g, "/").replace(/\0/g, "");
2365
1589
  const parts = normalized.split("/");
@@ -2380,34 +1604,31 @@ function assertWritablePath(path) {
2380
1604
  }
2381
1605
  }
2382
1606
  }
2383
- async function sftpReaddir(opts, dirPath) {
1607
+ async function sftpReaddir(opts, dirPath, proxy) {
2384
1608
  const safe = sanitizePath(dirPath);
2385
- return new Promise((resolve) => {
2386
- const ssh = new Client();
2387
- let done = false;
2388
- const timer = setTimeout(() => {
2389
- if (!done) {
2390
- done = true;
2391
- 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?.();
2392
1616
  resolve("Error: timeout");
2393
- }
2394
- }, 3e4);
2395
- ssh.on("ready", () => {
2396
- ssh.sftp((err, sftp) => {
1617
+ cleanup = void 0;
1618
+ }, 3e4);
1619
+ client.sftp((err, sftp) => {
2397
1620
  if (err) {
2398
- if (!done) {
2399
- done = true;
2400
- clearTimeout(timer);
2401
- ssh.end();
2402
- resolve(`Error: ${err.message}`);
2403
- }
1621
+ clearTimeout(timer);
1622
+ cleanup?.();
1623
+ cleanup = void 0;
1624
+ resolve(`Error: ${err.message}`);
2404
1625
  return;
2405
1626
  }
2406
1627
  sftp.readdir(safe, (err2, list) => {
2407
- done = true;
2408
1628
  clearTimeout(timer);
2409
1629
  if (err2) {
2410
- ssh.end();
1630
+ cleanup?.();
1631
+ cleanup = void 0;
2411
1632
  resolve(`Error: ${err2.message}`);
2412
1633
  return;
2413
1634
  }
@@ -2418,199 +1639,159 @@ async function sftpReaddir(opts, dirPath) {
2418
1639
  const mtime = item.attrs.mtime ? new Date(item.attrs.mtime * 1e3).toISOString() : "";
2419
1640
  return `${isDir ? "d" : "-"} ${String(size).padStart(10)} ${mtime} ${item.filename}`;
2420
1641
  });
2421
- ssh.end();
1642
+ cleanup?.();
1643
+ cleanup = void 0;
2422
1644
  resolve(entries.join("\n"));
2423
1645
  });
2424
1646
  });
2425
1647
  });
2426
- ssh.on("error", (e) => {
2427
- if (!done) {
2428
- done = true;
2429
- clearTimeout(timer);
2430
- resolve(`Error: ${e.message}`);
2431
- }
2432
- });
2433
- ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 3e4 });
2434
- });
1648
+ } catch (e) {
1649
+ cleanup?.();
1650
+ return `Error: ${e.message}`;
1651
+ }
2435
1652
  }
2436
- async function sftpRead(opts, filePath) {
1653
+ async function sftpRead(opts, filePath, proxy) {
2437
1654
  const safe = sanitizePath(filePath);
2438
- return new Promise((resolve) => {
2439
- const ssh = new Client();
2440
- let done = false;
2441
- const timer = setTimeout(() => {
2442
- if (!done) {
2443
- done = true;
2444
- 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?.();
2445
1662
  resolve("Error: timeout");
2446
- }
2447
- }, 6e4);
2448
- ssh.on("ready", () => {
2449
- ssh.sftp((err, sftp) => {
1663
+ cleanup = void 0;
1664
+ }, 6e4);
1665
+ client.sftp((err, sftp) => {
2450
1666
  if (err) {
2451
- if (!done) {
2452
- done = true;
2453
- clearTimeout(timer);
2454
- ssh.end();
2455
- resolve(`Error: ${err.message}`);
2456
- }
1667
+ clearTimeout(timer);
1668
+ cleanup?.();
1669
+ cleanup = void 0;
1670
+ resolve(`Error: ${err.message}`);
2457
1671
  return;
2458
1672
  }
2459
1673
  sftp.stat(safe, (err2, stats) => {
2460
1674
  if (err2) {
2461
- if (!done) {
2462
- done = true;
2463
- clearTimeout(timer);
2464
- ssh.end();
2465
- resolve(`Error: ${err2.message}`);
2466
- }
1675
+ clearTimeout(timer);
1676
+ cleanup?.();
1677
+ cleanup = void 0;
1678
+ resolve(`Error: ${err2.message}`);
2467
1679
  return;
2468
1680
  }
2469
1681
  if ((stats.size || 0) > 1048576) {
2470
- if (!done) {
2471
- done = true;
2472
- clearTimeout(timer);
2473
- ssh.end();
2474
- resolve(`Error: file too large (${stats.size} bytes, max 1MB)`);
2475
- }
1682
+ clearTimeout(timer);
1683
+ cleanup?.();
1684
+ cleanup = void 0;
1685
+ resolve(`Error: file too large (${stats.size} bytes, max 1MB)`);
2476
1686
  return;
2477
1687
  }
2478
1688
  const chunks = [];
2479
1689
  const rs = sftp.createReadStream(safe);
2480
- rs.on("data", (c) => chunks.push(c));
1690
+ rs.on("data", (ch) => chunks.push(ch));
2481
1691
  rs.on("end", () => {
2482
- if (!done) {
2483
- done = true;
2484
- clearTimeout(timer);
2485
- ssh.end();
2486
- resolve(Buffer.concat(chunks.map((c) => new Uint8Array(c))).toString("utf-8"));
2487
- }
1692
+ clearTimeout(timer);
1693
+ cleanup?.();
1694
+ cleanup = void 0;
1695
+ resolve(Buffer.concat(chunks.map((ch) => new Uint8Array(ch))).toString("utf-8"));
2488
1696
  });
2489
1697
  rs.on("error", (e) => {
2490
- if (!done) {
2491
- done = true;
2492
- clearTimeout(timer);
2493
- ssh.end();
2494
- resolve(`Error: ${e.message}`);
2495
- }
1698
+ clearTimeout(timer);
1699
+ cleanup?.();
1700
+ cleanup = void 0;
1701
+ resolve(`Error: ${e.message}`);
2496
1702
  });
2497
1703
  });
2498
1704
  });
2499
1705
  });
2500
- ssh.on("error", (e) => {
2501
- if (!done) {
2502
- done = true;
2503
- clearTimeout(timer);
2504
- resolve(`Error: ${e.message}`);
2505
- }
2506
- });
2507
- ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 6e4 });
2508
- });
1706
+ } catch (e) {
1707
+ cleanup?.();
1708
+ return `Error: ${e.message}`;
1709
+ }
2509
1710
  }
2510
- async function sftpWrite(opts, filePath, content) {
1711
+ async function sftpWrite(opts, filePath, content, proxy) {
2511
1712
  const safe = sanitizePath(filePath);
2512
1713
  assertWritablePath(safe);
2513
- return new Promise((resolve) => {
2514
- const ssh = new Client();
2515
- let done = false;
2516
- const timer = setTimeout(() => {
2517
- if (!done) {
2518
- done = true;
2519
- 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?.();
2520
1721
  resolve("Error: timeout");
2521
- }
2522
- }, 6e4);
2523
- ssh.on("ready", () => {
2524
- ssh.sftp((err, sftp) => {
1722
+ cleanup = void 0;
1723
+ }, 6e4);
1724
+ client.sftp((err, sftp) => {
2525
1725
  if (err) {
2526
- if (!done) {
2527
- done = true;
2528
- clearTimeout(timer);
2529
- ssh.end();
2530
- resolve(`Error: ${err.message}`);
2531
- }
1726
+ clearTimeout(timer);
1727
+ cleanup?.();
1728
+ cleanup = void 0;
1729
+ resolve(`Error: ${err.message}`);
2532
1730
  return;
2533
1731
  }
2534
1732
  const ws = sftp.createWriteStream(safe, { mode: 420 });
2535
1733
  ws.on("close", () => {
2536
- if (!done) {
2537
- done = true;
2538
- clearTimeout(timer);
2539
- ssh.end();
2540
- resolve(`Written ${content.length} bytes to ${safe}`);
2541
- }
1734
+ clearTimeout(timer);
1735
+ cleanup?.();
1736
+ cleanup = void 0;
1737
+ resolve(`Written ${content.length} bytes to ${safe}`);
2542
1738
  });
2543
1739
  ws.on("error", (e) => {
2544
- if (!done) {
2545
- done = true;
2546
- clearTimeout(timer);
2547
- ssh.end();
2548
- resolve(`Error: ${e.message}`);
2549
- }
1740
+ clearTimeout(timer);
1741
+ cleanup?.();
1742
+ cleanup = void 0;
1743
+ resolve(`Error: ${e.message}`);
2550
1744
  });
2551
1745
  ws.end(Buffer.from(content, "utf-8"));
2552
1746
  });
2553
1747
  });
2554
- ssh.on("error", (e) => {
2555
- if (!done) {
2556
- done = true;
2557
- clearTimeout(timer);
2558
- resolve(`Error: ${e.message}`);
2559
- }
2560
- });
2561
- ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 6e4 });
2562
- });
1748
+ } catch (e) {
1749
+ cleanup?.();
1750
+ return `Error: ${e.message}`;
1751
+ }
2563
1752
  }
2564
- async function sftpDelete(opts, filePath) {
1753
+ async function sftpDelete(opts, filePath, proxy) {
2565
1754
  const safe = sanitizePath(filePath);
2566
1755
  assertWritablePath(safe);
2567
- return new Promise((resolve) => {
2568
- const ssh = new Client();
2569
- let done = false;
2570
- const timer = setTimeout(() => {
2571
- if (!done) {
2572
- done = true;
2573
- 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?.();
2574
1763
  resolve("Error: timeout");
2575
- }
2576
- }, 3e4);
2577
- ssh.on("ready", () => {
2578
- ssh.sftp((err, sftp) => {
1764
+ cleanup = void 0;
1765
+ }, 3e4);
1766
+ client.sftp((err, sftp) => {
2579
1767
  if (err) {
2580
- if (!done) {
2581
- done = true;
2582
- clearTimeout(timer);
2583
- ssh.end();
2584
- resolve(`Error: ${err.message}`);
2585
- }
1768
+ clearTimeout(timer);
1769
+ cleanup?.();
1770
+ cleanup = void 0;
1771
+ resolve(`Error: ${err.message}`);
2586
1772
  return;
2587
1773
  }
2588
- sftp.unlink(safe, (err2) => {
2589
- if (err2) {
2590
- sftp.rmdir(safe, (err22) => {
2591
- done = true;
1774
+ sftp.unlink(safe, (unlinkErr) => {
1775
+ if (unlinkErr) {
1776
+ sftp.rmdir(safe, (rmdirErr) => {
2592
1777
  clearTimeout(timer);
2593
- ssh.end();
2594
- resolve(err22 ? `Error: ${err2.message}` : `Deleted directory ${safe}`);
1778
+ cleanup?.();
1779
+ cleanup = void 0;
1780
+ resolve(rmdirErr ? `Error: ${unlinkErr.message}` : `Deleted directory ${safe}`);
2595
1781
  });
2596
1782
  } else {
2597
- done = true;
2598
1783
  clearTimeout(timer);
2599
- ssh.end();
1784
+ cleanup?.();
1785
+ cleanup = void 0;
2600
1786
  resolve(`Deleted file ${safe}`);
2601
1787
  }
2602
1788
  });
2603
1789
  });
2604
1790
  });
2605
- ssh.on("error", (e) => {
2606
- if (!done) {
2607
- done = true;
2608
- clearTimeout(timer);
2609
- resolve(`Error: ${e.message}`);
2610
- }
2611
- });
2612
- ssh.connect({ host: opts.hostname, port: opts.port, username: opts.username, password: opts.password, privateKey: opts.privateKey, passphrase: opts.passphrase, readyTimeout: 3e4 });
2613
- });
1791
+ } catch (e) {
1792
+ cleanup?.();
1793
+ return `Error: ${e.message}`;
1794
+ }
2614
1795
  }
2615
1796
  var BLOCKED_COMMANDS = [
2616
1797
  "rm -rf /",
@@ -3128,25 +2309,25 @@ ${result.stdout}`);
3128
2309
  ${result.stderr}`);
3129
2310
  return { content: [{ type: "text", text: output.join("\n") }] };
3130
2311
  }
3131
- // ----- SFTP (no proxy support yet — direct connection only) -----
2312
+ // ----- SFTP -----
3132
2313
  case "sftp-list": {
3133
- const { conn } = await getServerConnection(String(a.serverId));
3134
- 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);
3135
2316
  return { content: [{ type: "text", text: listing }] };
3136
2317
  }
3137
2318
  case "sftp-read": {
3138
- const { conn } = await getServerConnection(String(a.serverId));
3139
- 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);
3140
2321
  return { content: [{ type: "text", text: content }] };
3141
2322
  }
3142
2323
  case "sftp-write": {
3143
- const { conn } = await getServerConnection(String(a.serverId));
3144
- 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);
3145
2326
  return { content: [{ type: "text", text: result }] };
3146
2327
  }
3147
2328
  case "sftp-delete": {
3148
- const { conn } = await getServerConnection(String(a.serverId));
3149
- 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);
3150
2331
  return { content: [{ type: "text", text: result }] };
3151
2332
  }
3152
2333
  // ----- Docker -----
@@ -3538,7 +2719,7 @@ ${lines.join("\n")}` }] };
3538
2719
  return handleTriggerTool(name, a, { sshExec, getServerConnection });
3539
2720
  }
3540
2721
  if (AGENT_TOOL_NAMES.has(name)) {
3541
- return handleAgentTool(name, a, { supabase, workspaceId: agentWorkspaceId });
2722
+ return handleAgentTool(name, a);
3542
2723
  }
3543
2724
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
3544
2725
  }
@@ -3569,16 +2750,6 @@ async function main() {
3569
2750
  console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
3570
2751
  const transports = /* @__PURE__ */ new Map();
3571
2752
  const REST_TOOL_MAP = {
3572
- "/api/report-coverage": "agent-report-coverage",
3573
- "/api/report-finding": "agent-report-finding",
3574
- "/api/save-documentation": "agent-save-documentation",
3575
- "/api/list-findings": "agent-list-findings",
3576
- "/api/get-documentation": "agent-get-documentation",
3577
- "/api/validate-suggestions": "agent-validate-suggestions",
3578
- "/api/check-lead-exists": "agent-check-lead-exists",
3579
- "/api/save-lead": "agent-save-lead",
3580
- "/api/save-email-draft": "agent-save-email-draft",
3581
- "/api/complete-target": "agent-complete-target",
3582
2753
  "/api/web-search": "web-search",
3583
2754
  "/api/web-fetch": "web-fetch"
3584
2755
  };
@@ -3597,7 +2768,7 @@ async function main() {
3597
2768
  return;
3598
2769
  }
3599
2770
  try {
3600
- const result = await handleAgentTool(restToolName, toolArgs, { supabase, workspaceId: agentWorkspaceId });
2771
+ const result = await handleAgentTool(restToolName, toolArgs);
3601
2772
  res.writeHead(200, { "Content-Type": "application/json" });
3602
2773
  res.end(JSON.stringify({ ok: true, result }));
3603
2774
  } catch (err) {