@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 +194 -1023
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -376,339 +376,13 @@ ${rawJson.substring(0, 500)}`
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
// src/agent-tools.ts
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
"
|
|
384
|
-
"improvement",
|
|
385
|
-
"n_plus_1_query",
|
|
386
|
-
"bundle_size",
|
|
387
|
-
"unnecessary_rerender",
|
|
388
|
-
"slow_endpoint",
|
|
389
|
-
"memory_leak",
|
|
390
|
-
"missing_memoization",
|
|
391
|
-
"large_dependency",
|
|
392
|
-
"dead_code",
|
|
393
|
-
"unused_dependency",
|
|
394
|
-
"env_leak",
|
|
395
|
-
"i18n_missing_key",
|
|
396
|
-
"i18n_unused_key"
|
|
397
|
-
]);
|
|
398
|
-
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["info", "warning", "critical"]);
|
|
399
|
-
var VALID_SCOPES = /* @__PURE__ */ new Set([
|
|
400
|
-
"architecture",
|
|
401
|
-
"module",
|
|
402
|
-
"component",
|
|
403
|
-
"api",
|
|
404
|
-
"package",
|
|
405
|
-
"security",
|
|
406
|
-
"server_audit",
|
|
407
|
-
"changelog",
|
|
408
|
-
"api-reference"
|
|
409
|
-
]);
|
|
410
|
-
var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
|
|
411
|
-
"pending",
|
|
412
|
-
"agent_approved",
|
|
413
|
-
"agent_flagged",
|
|
414
|
-
"human_approved"
|
|
415
|
-
]);
|
|
416
|
-
function normalizeCompanyName(name) {
|
|
417
|
-
return name.toLowerCase().replace(/\b(b\.?v\.?|n\.?v\.?|v\.?o\.?f\.?|c\.?v\.?|holding|groep|group|nederland|netherlands)\b/gi, "").replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
|
|
379
|
+
function clamp(val, min, max) {
|
|
380
|
+
return Math.max(min, Math.min(max, val));
|
|
381
|
+
}
|
|
382
|
+
function sanitizeString(val, maxLen) {
|
|
383
|
+
return String(val ?? "").slice(0, maxLen);
|
|
418
384
|
}
|
|
419
385
|
var AGENT_TOOLS = [
|
|
420
|
-
{
|
|
421
|
-
name: "agent-report-coverage",
|
|
422
|
-
description: "Report documentation coverage data for a repository. Upserts records into doc_coverage. Call once per logical unit (package, module, theme, plugin directory). Provide all entries in a single call for efficiency.",
|
|
423
|
-
inputSchema: {
|
|
424
|
-
type: "object",
|
|
425
|
-
properties: {
|
|
426
|
-
repo_slug: {
|
|
427
|
-
type: "string",
|
|
428
|
-
description: 'Repository slug (e.g. "mg-dashboard", "bna-wordpress")'
|
|
429
|
-
},
|
|
430
|
-
refront_project_id: {
|
|
431
|
-
type: "string",
|
|
432
|
-
description: "Refront project UUID linked to this repository"
|
|
433
|
-
},
|
|
434
|
-
entries: {
|
|
435
|
-
type: "array",
|
|
436
|
-
description: "Array of coverage entries, one per logical unit",
|
|
437
|
-
items: {
|
|
438
|
-
type: "object",
|
|
439
|
-
properties: {
|
|
440
|
-
path: { type: "string", description: 'Logical unit path (e.g. "packages/ui", "wp-content/themes/bna")' },
|
|
441
|
-
total_functions: { type: "number", description: "Total public functions/methods" },
|
|
442
|
-
documented_functions: { type: "number", description: "Functions with doc comments" },
|
|
443
|
-
total_types: { type: "number", description: "Total classes/interfaces/types" },
|
|
444
|
-
documented_types: { type: "number", description: "Types with doc comments" },
|
|
445
|
-
total_endpoints: { type: "number", description: "Total API/REST/hook endpoints" },
|
|
446
|
-
documented_endpoints: { type: "number", description: "Endpoints with documentation" }
|
|
447
|
-
},
|
|
448
|
-
required: ["path", "total_functions", "documented_functions"]
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
},
|
|
452
|
-
required: ["repo_slug", "entries"]
|
|
453
|
-
}
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
name: "agent-report-finding",
|
|
457
|
-
description: "Report documentation or performance findings for a repository. Inserts records into doc_suggestion. Auto-creates a parent documentation record if one does not exist yet. Batch multiple findings in one call.",
|
|
458
|
-
inputSchema: {
|
|
459
|
-
type: "object",
|
|
460
|
-
properties: {
|
|
461
|
-
repo_slug: {
|
|
462
|
-
type: "string",
|
|
463
|
-
description: "Repository slug"
|
|
464
|
-
},
|
|
465
|
-
refront_project_id: {
|
|
466
|
-
type: "string",
|
|
467
|
-
description: "Refront project UUID linked to this repository"
|
|
468
|
-
},
|
|
469
|
-
category: {
|
|
470
|
-
type: "string",
|
|
471
|
-
enum: ["scan_findings", "perf_audit"],
|
|
472
|
-
description: 'Finding category: "scan_findings" for doc issues, "perf_audit" for performance issues'
|
|
473
|
-
},
|
|
474
|
-
findings: {
|
|
475
|
-
type: "array",
|
|
476
|
-
description: "Array of findings to report",
|
|
477
|
-
items: {
|
|
478
|
-
type: "object",
|
|
479
|
-
properties: {
|
|
480
|
-
type: {
|
|
481
|
-
type: "string",
|
|
482
|
-
enum: [...VALID_FINDING_TYPES],
|
|
483
|
-
description: "Finding type"
|
|
484
|
-
},
|
|
485
|
-
severity: {
|
|
486
|
-
type: "string",
|
|
487
|
-
enum: ["info", "warning", "critical"],
|
|
488
|
-
description: "Severity level"
|
|
489
|
-
},
|
|
490
|
-
description: {
|
|
491
|
-
type: "string",
|
|
492
|
-
description: "Clear description of the issue (max 2000 chars)"
|
|
493
|
-
},
|
|
494
|
-
file_path: {
|
|
495
|
-
type: "string",
|
|
496
|
-
description: "Relative file path where the issue was found"
|
|
497
|
-
},
|
|
498
|
-
suggested_fix: {
|
|
499
|
-
type: "string",
|
|
500
|
-
description: "Suggested fix with code example (max 5000 chars)"
|
|
501
|
-
}
|
|
502
|
-
},
|
|
503
|
-
required: ["type", "severity", "description"]
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
},
|
|
507
|
-
required: ["repo_slug", "category", "findings"]
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
name: "agent-save-documentation",
|
|
512
|
-
description: "Save or update a documentation record for a repository. Upserts by repo_slug + scope + path combination.",
|
|
513
|
-
inputSchema: {
|
|
514
|
-
type: "object",
|
|
515
|
-
properties: {
|
|
516
|
-
repo_slug: { type: "string", description: "Repository slug" },
|
|
517
|
-
refront_project_id: { type: "string", description: "Refront project UUID (optional for standalone repos)" },
|
|
518
|
-
scope: {
|
|
519
|
-
type: "string",
|
|
520
|
-
enum: [...VALID_SCOPES],
|
|
521
|
-
description: "Documentation scope"
|
|
522
|
-
},
|
|
523
|
-
path: {
|
|
524
|
-
type: "string",
|
|
525
|
-
description: 'Path within the repo (e.g. "packages/ui")'
|
|
526
|
-
},
|
|
527
|
-
title: { type: "string", description: "Document title" },
|
|
528
|
-
content: { type: "string", description: "Documentation content (Typst markup)" },
|
|
529
|
-
review_status: {
|
|
530
|
-
type: "string",
|
|
531
|
-
enum: [...VALID_REVIEW_STATUSES],
|
|
532
|
-
description: 'Review status (default: "pending")'
|
|
533
|
-
}
|
|
534
|
-
},
|
|
535
|
-
required: ["repo_slug", "scope", "path", "title", "content"]
|
|
536
|
-
}
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
name: "agent-list-findings",
|
|
540
|
-
description: "List existing findings (doc_suggestion records) for a repository. Use this to check what has already been reported before submitting new findings.",
|
|
541
|
-
inputSchema: {
|
|
542
|
-
type: "object",
|
|
543
|
-
properties: {
|
|
544
|
-
repo_slug: { type: "string", description: "Repository slug" },
|
|
545
|
-
type: {
|
|
546
|
-
type: "string",
|
|
547
|
-
enum: [...VALID_FINDING_TYPES],
|
|
548
|
-
description: "Filter by finding type"
|
|
549
|
-
},
|
|
550
|
-
severity: {
|
|
551
|
-
type: "string",
|
|
552
|
-
enum: ["info", "warning", "critical"],
|
|
553
|
-
description: "Filter by severity"
|
|
554
|
-
},
|
|
555
|
-
status: {
|
|
556
|
-
type: "string",
|
|
557
|
-
enum: ["open", "ticket_created", "resolved", "dismissed"],
|
|
558
|
-
description: "Filter by status (default: all)"
|
|
559
|
-
},
|
|
560
|
-
limit: {
|
|
561
|
-
type: "number",
|
|
562
|
-
description: "Max results to return (default: 50, max: 200)"
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
|
-
required: ["repo_slug"]
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
name: "agent-get-documentation",
|
|
570
|
-
description: "Retrieve existing documentation records for a repository. Use this to read current docs before generating or reviewing.",
|
|
571
|
-
inputSchema: {
|
|
572
|
-
type: "object",
|
|
573
|
-
properties: {
|
|
574
|
-
repo_slug: { type: "string", description: "Repository slug" },
|
|
575
|
-
scope: {
|
|
576
|
-
type: "string",
|
|
577
|
-
enum: [...VALID_SCOPES],
|
|
578
|
-
description: "Filter by scope"
|
|
579
|
-
},
|
|
580
|
-
path: { type: "string", description: "Filter by exact path" },
|
|
581
|
-
limit: {
|
|
582
|
-
type: "number",
|
|
583
|
-
description: "Max results to return (default: 20, max: 100)"
|
|
584
|
-
}
|
|
585
|
-
},
|
|
586
|
-
required: ["repo_slug"]
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
name: "agent-validate-suggestions",
|
|
591
|
-
description: "Validate existing open suggestions against the actual codebase. For each suggestion, report whether it is resolved (fixed), valid (still open), invalid (should be dismissed), or needs adjustment.",
|
|
592
|
-
inputSchema: {
|
|
593
|
-
type: "object",
|
|
594
|
-
properties: {
|
|
595
|
-
repo_slug: {
|
|
596
|
-
type: "string",
|
|
597
|
-
description: "Repository slug being validated"
|
|
598
|
-
},
|
|
599
|
-
results: {
|
|
600
|
-
type: "array",
|
|
601
|
-
description: "Validation results per suggestion",
|
|
602
|
-
items: {
|
|
603
|
-
type: "object",
|
|
604
|
-
properties: {
|
|
605
|
-
suggestion_id: {
|
|
606
|
-
type: "string",
|
|
607
|
-
description: "UUID of the doc_suggestion being validated"
|
|
608
|
-
},
|
|
609
|
-
verdict: {
|
|
610
|
-
type: "string",
|
|
611
|
-
enum: ["valid", "invalid", "adjusted", "resolved"],
|
|
612
|
-
description: "resolved = fix applied (sets status resolved), valid = still relevant (keep open), invalid = dismiss, adjusted = update fields"
|
|
613
|
-
},
|
|
614
|
-
reason: {
|
|
615
|
-
type: "string",
|
|
616
|
-
description: "Explanation of why this suggestion is valid/invalid/adjusted (max 2000 chars)"
|
|
617
|
-
},
|
|
618
|
-
adjusted_description: {
|
|
619
|
-
type: "string",
|
|
620
|
-
description: "Updated description (only for verdict=adjusted, max 2000 chars)"
|
|
621
|
-
},
|
|
622
|
-
adjusted_severity: {
|
|
623
|
-
type: "string",
|
|
624
|
-
enum: ["info", "warning", "critical"],
|
|
625
|
-
description: "Updated severity (only for verdict=adjusted)"
|
|
626
|
-
},
|
|
627
|
-
adjusted_suggested_fix: {
|
|
628
|
-
type: "string",
|
|
629
|
-
description: "Updated suggested fix (only for verdict=adjusted, max 5000 chars)"
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
required: ["suggestion_id", "verdict", "reason"]
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
},
|
|
636
|
-
required: ["repo_slug", "results"]
|
|
637
|
-
}
|
|
638
|
-
},
|
|
639
|
-
// -- Lead Generation tools --------------------------------------------------
|
|
640
|
-
{
|
|
641
|
-
name: "agent-check-lead-exists",
|
|
642
|
-
description: "Check if a lead already exists by website URL or company name. Call this BEFORE visiting a website to avoid wasting time on duplicates.",
|
|
643
|
-
inputSchema: {
|
|
644
|
-
type: "object",
|
|
645
|
-
properties: {
|
|
646
|
-
website_url: {
|
|
647
|
-
type: "string",
|
|
648
|
-
description: 'Website URL to check (e.g. "https://example.nl")'
|
|
649
|
-
},
|
|
650
|
-
company_name: {
|
|
651
|
-
type: "string",
|
|
652
|
-
description: "Company name to fuzzy-match against existing leads"
|
|
653
|
-
}
|
|
654
|
-
},
|
|
655
|
-
required: []
|
|
656
|
-
}
|
|
657
|
-
},
|
|
658
|
-
{
|
|
659
|
-
name: "agent-save-lead",
|
|
660
|
-
description: "Save a discovered lead (company) to the database. Handles dedup on website_url + fuzzy match on company name. Returns the lead ID and whether it already existed.",
|
|
661
|
-
inputSchema: {
|
|
662
|
-
type: "object",
|
|
663
|
-
properties: {
|
|
664
|
-
company_name: { type: "string", description: "Company name" },
|
|
665
|
-
website_url: { type: "string", description: "Company website URL" },
|
|
666
|
-
industry: { type: "string", description: "Industry sector" },
|
|
667
|
-
region: { type: "string", description: 'Geographic region (e.g. "amsterdam", "rotterdam")' },
|
|
668
|
-
description: { type: "string", description: "AI-generated summary of what the company does (max 2000 chars)" },
|
|
669
|
-
potential_fit: { type: "string", description: "Why MG Software could help this company (max 2000 chars)" },
|
|
670
|
-
fit_score: { type: "number", description: "Fit score 1-10 for MG Software partnership" },
|
|
671
|
-
estimated_company_size: { type: "string", description: 'Estimated employee count range (e.g. "1-10", "10-50", "50-200")' },
|
|
672
|
-
kvk_number: { type: "string", description: "KvK (Chamber of Commerce) number if found" },
|
|
673
|
-
contact_name: { type: "string", description: "Primary contact person name" },
|
|
674
|
-
contact_role: { type: "string", description: "Contact person role/title" },
|
|
675
|
-
contact_email: { type: "string", description: "Contact person email" },
|
|
676
|
-
contact_phone: { type: "string", description: "Contact person phone" },
|
|
677
|
-
contact_linkedin: { type: "string", description: "Contact person LinkedIn URL" },
|
|
678
|
-
general_email: { type: "string", description: "General company email (info@...)" },
|
|
679
|
-
general_phone: { type: "string", description: "General company phone number" },
|
|
680
|
-
source_url: { type: "string", description: "URL where this lead was found (Google result, directory, etc.)" },
|
|
681
|
-
target_id: { type: "string", description: "UUID of the lead_generation_target this lead was found for" }
|
|
682
|
-
},
|
|
683
|
-
required: ["company_name", "website_url"]
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
name: "agent-save-email-draft",
|
|
688
|
-
description: "Save a cold email draft for a lead. The email will be shown in the backoffice for manual review and copy. Do NOT use em-dashes or en-dashes in the email.",
|
|
689
|
-
inputSchema: {
|
|
690
|
-
type: "object",
|
|
691
|
-
properties: {
|
|
692
|
-
lead_id: { type: "string", description: "UUID of the lead this email is for" },
|
|
693
|
-
subject: { type: "string", description: "Email subject line (Dutch, max 200 chars)" },
|
|
694
|
-
body: { type: "string", description: "Email body text (Dutch, max 5000 chars, NO em-dashes or en-dashes)" },
|
|
695
|
-
tone: { type: "string", description: "Tone of the email (default: professional)" }
|
|
696
|
-
},
|
|
697
|
-
required: ["lead_id", "subject", "body"]
|
|
698
|
-
}
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
name: "agent-complete-target",
|
|
702
|
-
description: "Mark a lead_generation_target as completed with the number of leads found.",
|
|
703
|
-
inputSchema: {
|
|
704
|
-
type: "object",
|
|
705
|
-
properties: {
|
|
706
|
-
target_id: { type: "string", description: "UUID of the lead_generation_target" },
|
|
707
|
-
results_count: { type: "number", description: "Number of new leads saved for this target" }
|
|
708
|
-
},
|
|
709
|
-
required: ["target_id", "results_count"]
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
386
|
{
|
|
713
387
|
name: "web-search",
|
|
714
388
|
description: "Search the web using DuckDuckGo. Returns a list of results with title, URL, and snippet. Use this to find companies, websites, directories, etc.",
|
|
@@ -758,26 +432,10 @@ var AGENT_TOOLS = [
|
|
|
758
432
|
];
|
|
759
433
|
var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
|
|
760
434
|
var AGENT_TOOL_MODULE_MAP = {
|
|
761
|
-
"agent-report-coverage": "agent_reporting",
|
|
762
|
-
"agent-report-finding": "agent_reporting",
|
|
763
|
-
"agent-save-documentation": "agent_reporting",
|
|
764
|
-
"agent-list-findings": "agent_reporting",
|
|
765
|
-
"agent-get-documentation": "agent_reporting",
|
|
766
|
-
"agent-validate-suggestions": "agent_reporting",
|
|
767
|
-
"agent-check-lead-exists": "agent_reporting",
|
|
768
|
-
"agent-save-lead": "agent_reporting",
|
|
769
|
-
"agent-save-email-draft": "agent_reporting",
|
|
770
|
-
"agent-complete-target": "agent_reporting",
|
|
771
435
|
"web-search": "agent_reporting",
|
|
772
436
|
"web-fetch": "agent_reporting",
|
|
773
437
|
"web-find-contacts": "agent_reporting"
|
|
774
438
|
};
|
|
775
|
-
function clamp(val, min, max) {
|
|
776
|
-
return Math.max(min, Math.min(max, val));
|
|
777
|
-
}
|
|
778
|
-
function sanitizeString(val, maxLen) {
|
|
779
|
-
return String(val ?? "").slice(0, maxLen);
|
|
780
|
-
}
|
|
781
439
|
var WEB_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
782
440
|
async function webSearch(query, maxResults) {
|
|
783
441
|
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
@@ -1088,499 +746,8 @@ async function webFetch(url, extractLinks) {
|
|
|
1088
746
|
clearTimeout(timeout);
|
|
1089
747
|
}
|
|
1090
748
|
}
|
|
1091
|
-
function
|
|
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
|
|
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
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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
|
-
|
|
2395
|
-
|
|
2396
|
-
ssh.sftp((err, sftp) => {
|
|
1617
|
+
cleanup = void 0;
|
|
1618
|
+
}, 3e4);
|
|
1619
|
+
client.sftp((err, sftp) => {
|
|
2397
1620
|
if (err) {
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1642
|
+
cleanup?.();
|
|
1643
|
+
cleanup = void 0;
|
|
2422
1644
|
resolve(entries.join("\n"));
|
|
2423
1645
|
});
|
|
2424
1646
|
});
|
|
2425
1647
|
});
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
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
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
-
|
|
2448
|
-
|
|
2449
|
-
ssh.sftp((err, sftp) => {
|
|
1663
|
+
cleanup = void 0;
|
|
1664
|
+
}, 6e4);
|
|
1665
|
+
client.sftp((err, sftp) => {
|
|
2450
1666
|
if (err) {
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
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
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
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", (
|
|
1690
|
+
rs.on("data", (ch) => chunks.push(ch));
|
|
2481
1691
|
rs.on("end", () => {
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
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
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
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
|
-
|
|
2523
|
-
|
|
2524
|
-
ssh.sftp((err, sftp) => {
|
|
1722
|
+
cleanup = void 0;
|
|
1723
|
+
}, 6e4);
|
|
1724
|
+
client.sftp((err, sftp) => {
|
|
2525
1725
|
if (err) {
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
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
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
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
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
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
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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
|
-
|
|
2577
|
-
|
|
2578
|
-
ssh.sftp((err, sftp) => {
|
|
1764
|
+
cleanup = void 0;
|
|
1765
|
+
}, 3e4);
|
|
1766
|
+
client.sftp((err, sftp) => {
|
|
2579
1767
|
if (err) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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, (
|
|
2589
|
-
if (
|
|
2590
|
-
sftp.rmdir(safe, (
|
|
2591
|
-
done = true;
|
|
1774
|
+
sftp.unlink(safe, (unlinkErr) => {
|
|
1775
|
+
if (unlinkErr) {
|
|
1776
|
+
sftp.rmdir(safe, (rmdirErr) => {
|
|
2592
1777
|
clearTimeout(timer);
|
|
2593
|
-
|
|
2594
|
-
|
|
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
|
-
|
|
1784
|
+
cleanup?.();
|
|
1785
|
+
cleanup = void 0;
|
|
2600
1786
|
resolve(`Deleted file ${safe}`);
|
|
2601
1787
|
}
|
|
2602
1788
|
});
|
|
2603
1789
|
});
|
|
2604
1790
|
});
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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
|
|
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
|
|
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
|
|
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) {
|