@mgsoftwarebv/mg-dashboard-mcp 2.3.2 → 2.3.4

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
@@ -375,404 +375,6 @@ ${rawJson.substring(0, 500)}`
375
375
  }
376
376
  }
377
377
 
378
- // src/script-tools.ts
379
- var SCRIPTS = [
380
- {
381
- name: "commit-feedback",
382
- filename: "commit-feedback.ps1",
383
- description: "PowerShell script that provides colored terminal feedback for the Ctrl+Shift+K commit & push keybinding. Polls for a new commit and shows branch, message, and file stats when done.",
384
- content: `param()
385
-
386
- $initial = git log --oneline -1 2>$null
387
-
388
- Write-Host "\`n==========================================" -ForegroundColor Cyan
389
- Write-Host " Starting Commit & Push..." -ForegroundColor Cyan
390
- Write-Host "==========================================" -ForegroundColor Cyan
391
- Write-Host " [1/5] Pulling latest..." -ForegroundColor DarkGray
392
- Write-Host " [2/5] Staging changes..." -ForegroundColor DarkGray
393
- Write-Host " [3/5] Generating AI commit message..." -ForegroundColor DarkGray
394
- Write-Host " [4/5] Committing..." -ForegroundColor DarkGray
395
- Write-Host " [5/5] Pushing to remote..." -ForegroundColor DarkGray
396
- Write-Host ""
397
- Write-Host " Waiting for commit" -ForegroundColor DarkGray -NoNewline
398
-
399
- $timeout = 60
400
- $elapsed = 0
401
- while ($elapsed -lt $timeout) {
402
- $current = git log --oneline -1 2>$null
403
- if ($current -ne $initial) { break }
404
- Start-Sleep -Seconds 1
405
- $elapsed++
406
- Write-Host "." -ForegroundColor DarkGray -NoNewline
407
- }
408
- Write-Host ""
409
-
410
- if ($current -eq $initial) {
411
- Write-Host ""
412
- Write-Host " [!] No new commit detected (timed out after \${timeout}s)" -ForegroundColor Yellow
413
- Write-Host ""
414
- exit
415
- }
416
-
417
- Start-Sleep -Seconds 2
418
-
419
- $branch = git rev-parse --abbrev-ref HEAD 2>$null
420
- $commit = git log --format='%h %s' -1 2>$null
421
-
422
- Write-Host ""
423
- Write-Host "==========================================" -ForegroundColor Cyan
424
- Write-Host " [OK] Commit & Push Complete" -ForegroundColor Green
425
- Write-Host "==========================================" -ForegroundColor Cyan
426
- Write-Host " Branch: $branch" -ForegroundColor Yellow
427
- Write-Host " Commit: $commit" -ForegroundColor White
428
- Write-Host ""
429
- git --no-pager diff HEAD~1 --stat --color=always 2>$null
430
- Write-Host ""
431
- Write-Host "==========================================" -ForegroundColor Cyan
432
- Write-Host ""
433
- `
434
- },
435
- {
436
- name: "create-pr",
437
- filename: "create-pr.cmd",
438
- description: "Windows batch script that auto-detects the target branch based on your release pipeline (v*-changes branches), merges the target into your branch, pushes, and creates a PR via gh CLI. Bound to Ctrl+Shift+J.",
439
- content: `@echo off
440
- setlocal enabledelayedexpansion
441
- title MG Dashboard - Create PR
442
-
443
- REM --- ANSI color setup ---
444
- for /F %%a in ('echo prompt $E ^| cmd') do set "ESC=%%a"
445
- set "GREEN=%ESC%[32m"
446
- set "RED=%ESC%[31m"
447
- set "YELLOW=%ESC%[33m"
448
- set "CYAN=%ESC%[36m"
449
- set "DIM=%ESC%[90m"
450
- set "BOLD=%ESC%[1m"
451
- set "RESET=%ESC%[0m"
452
-
453
- echo.
454
- echo %CYAN%%BOLD%==========================================%RESET%
455
- echo %CYAN%%BOLD% MG Dashboard - Create Pull Request%RESET%
456
- echo %CYAN%%BOLD%==========================================%RESET%
457
- echo.
458
-
459
- REM --- Merge origin/main first ---
460
- echo %CYAN%[0/4]%RESET% Merging origin/main into current branch...
461
- git fetch --prune >nul 2>&1
462
- git merge origin/main --no-edit >nul 2>&1
463
- if errorlevel 1 (
464
- git merge --abort >nul 2>&1
465
- echo %RED%X Merge conflict with origin/main. Resolve manually.%RESET%
466
- echo.
467
- exit /b 1
468
- )
469
- echo %GREEN% OK%RESET%
470
- echo.
471
-
472
- REM --- Prerequisite: gh CLI ---
473
- where gh >nul 2>&1
474
- if errorlevel 1 (
475
- echo %RED%X GitHub CLI ^(gh^) is not installed.%RESET%
476
- echo %DIM% Install it from: https://cli.github.com/%RESET%
477
- echo.
478
- exit /b 1
479
- )
480
-
481
- REM --- Prerequisite: git repo ---
482
- git rev-parse --is-inside-work-tree >nul 2>&1
483
- if errorlevel 1 (
484
- echo %RED%X Not inside a git repository.%RESET%
485
- echo.
486
- exit /b 1
487
- )
488
-
489
- REM --- Detect repo from git remote ---
490
- set "REPO="
491
- for /f "tokens=*" %%u in ('git remote get-url origin 2^>nul') do set "REMOTE_URL=%%u"
492
- if defined REMOTE_URL (
493
- echo !REMOTE_URL! | findstr /r "git@github.com:" >nul 2>&1
494
- if not errorlevel 1 (
495
- set "REPO=!REMOTE_URL:git@github.com:=!"
496
- set "REPO=!REPO:.git=!"
497
- )
498
- echo !REMOTE_URL! | findstr /r "https://github.com/" >nul 2>&1
499
- if not errorlevel 1 (
500
- set "REPO=!REMOTE_URL:https://github.com/=!"
501
- set "REPO=!REPO:.git=!"
502
- )
503
- )
504
- if not defined REPO (
505
- echo %RED%X Could not detect GitHub repo from git remote.%RESET%
506
- echo %DIM% Make sure origin points to a GitHub repository.%RESET%
507
- echo.
508
- exit /b 1
509
- )
510
-
511
- REM --- Get current branch ---
512
- for /f "tokens=*" %%b in ('git rev-parse --abbrev-ref HEAD') do set "CURRENT_BRANCH=%%b"
513
-
514
- echo %DIM% Repository: !REPO!%RESET%
515
- echo %DIM% Branch: %CURRENT_BRANCH%%RESET%
516
- echo.
517
-
518
- REM --- Guard: don't PR from main or a bare version branch ---
519
- if "%CURRENT_BRANCH%"=="main" (
520
- echo %RED%X You are on main. Switch to a feature or changes branch first.%RESET%
521
- echo.
522
- exit /b 1
523
- )
524
- echo %CURRENT_BRANCH% | findstr /r "^v[0-9]*\\.[0-9]*\\.[0-9]*$" >nul 2>&1
525
- if not errorlevel 1 (
526
- echo %RED%X You are on a version branch ^(%CURRENT_BRANCH%^). Switch to a feature or changes branch first.%RESET%
527
- echo.
528
- exit /b 1
529
- )
530
-
531
- REM --- Fetch latest remote refs ---
532
- echo %CYAN%[1/4]%RESET% Fetching remote branches...
533
- git fetch --prune >nul 2>&1
534
-
535
- REM --- Determine target branch ---
536
- set "TARGET="
537
-
538
- REM Check if current branch is a -changes branch (last 8 chars)
539
- if "!CURRENT_BRANCH:~-8!"=="-changes" (
540
- set "TARGET=!CURRENT_BRANCH:~0,-8!"
541
- echo %GREEN% Detected -changes branch. Target: %BOLD%!TARGET!%RESET%
542
- goto :do_merge
543
- )
544
-
545
- REM Look for v*-changes branches on remote (highest version first)
546
- set "TARGET="
547
- for /f "tokens=*" %%r in ('git branch -r --sort=-version:refname --list "origin/v*-changes" 2^>nul') do (
548
- if not defined TARGET (
549
- set "RAW=%%r"
550
- set "BRANCH=!RAW: origin/=!"
551
- set "BRANCH=!BRANCH:origin/=!"
552
- set "TARGET=!BRANCH!"
553
- )
554
- )
555
-
556
- if defined TARGET (
557
- echo %GREEN% Release pipeline detected. Target: %BOLD%!TARGET!%RESET%
558
- ) else (
559
- REM No -changes branch; try highest version branch
560
- set "TARGET="
561
- for /f "tokens=*" %%r in ('git branch -r --sort=-version:refname --list "origin/v*" 2^>nul') do (
562
- if not defined TARGET (
563
- set "RAW=%%r"
564
- set "BRANCH=!RAW: origin/=!"
565
- set "BRANCH=!BRANCH:origin/=!"
566
- set "TAIL=!BRANCH:~-8!"
567
- if not "!TAIL!"=="-changes" set "TARGET=!BRANCH!"
568
- )
569
- )
570
- if defined TARGET (
571
- echo %GREEN% Version branch detected. Target: %BOLD%!TARGET!%RESET%
572
- ) else (
573
- set "TARGET=main"
574
- echo %YELLOW% No release pipeline or version branch found. Target: %BOLD%main%RESET%
575
- )
576
- )
577
-
578
- :do_merge
579
- echo.
580
-
581
- REM --- Merge target into current branch ---
582
- echo %CYAN%[2/4]%RESET% Merging %BOLD%!TARGET!%RESET% into %BOLD%%CURRENT_BRANCH%%RESET%...
583
- git merge origin/!TARGET! --no-edit
584
- if errorlevel 1 (
585
- echo.
586
- echo %RED%X Merge conflict detected.%RESET%
587
- git merge --abort >nul 2>&1
588
- echo %DIM% Resolve conflicts manually, then retry.%RESET%
589
- echo.
590
- exit /b 1
591
- )
592
- echo %GREEN% OK%RESET%
593
- echo.
594
-
595
- REM --- Push current branch to origin ---
596
- echo %CYAN%[3/4]%RESET% Pushing %CURRENT_BRANCH% to origin...
597
- git push -u origin HEAD
598
- if errorlevel 1 (
599
- echo.
600
- echo %RED%X Failed to push branch to origin.%RESET%
601
- echo.
602
- exit /b 1
603
- )
604
- echo %GREEN% OK%RESET%
605
- echo.
606
-
607
- REM --- Guard: don't PR into the same branch ---
608
- if "!TARGET!"=="!CURRENT_BRANCH!" (
609
- echo %RED%X Target branch is the same as current branch ^(!TARGET!^). Cannot create PR.%RESET%
610
- echo.
611
- exit /b 1
612
- )
613
-
614
- echo %CYAN%[4/4]%RESET% Creating PR: %BOLD%%CURRENT_BRANCH%%RESET% %DIM%->%RESET% %BOLD%%TARGET%%RESET%
615
- echo.
616
-
617
- REM --- Create the PR ---
618
- gh pr create --repo "!REPO!" --base "%TARGET%" --fill
619
- if errorlevel 1 (
620
- echo.
621
- echo %RED%X Failed to create pull request.%RESET%
622
- echo %DIM% Check the error above. A PR may already exist for this branch.%RESET%
623
- echo.
624
- exit /b 1
625
- )
626
-
627
- echo.
628
- echo %GREEN%%BOLD% PR created successfully!%RESET%
629
- echo.
630
- `
631
- },
632
- {
633
- name: "commit-and-pr",
634
- filename: "commit-and-pr.ps1",
635
- description: "PowerShell script that combines commit & push with PR creation. Checks for pending changes first: if changes exist, waits for the commit (triggered by Cursor keybinding), shows a summary, then runs create-pr.cmd. If no changes, skips straight to PR. Requires create-pr.cmd in the same directory. Bound to Ctrl+Shift+M.",
636
- content: `param()
637
-
638
- $hasChanges = (git status --porcelain 2>$null)
639
-
640
- if ($hasChanges) {
641
- $initial = git log --oneline -1 2>$null
642
-
643
- Write-Host "\`n==========================================" -ForegroundColor Cyan
644
- Write-Host " Starting Commit, Push & PR..." -ForegroundColor Cyan
645
- Write-Host "==========================================" -ForegroundColor Cyan
646
- Write-Host " [1/5] Pulling latest..." -ForegroundColor DarkGray
647
- Write-Host " [2/5] Staging changes..." -ForegroundColor DarkGray
648
- Write-Host " [3/5] Generating AI commit message..." -ForegroundColor DarkGray
649
- Write-Host " [4/5] Committing..." -ForegroundColor DarkGray
650
- Write-Host " [5/5] Pushing to remote..." -ForegroundColor DarkGray
651
- Write-Host ""
652
- Write-Host " Waiting for commit" -ForegroundColor DarkGray -NoNewline
653
-
654
- $timeout = 60
655
- $elapsed = 0
656
- while ($elapsed -lt $timeout) {
657
- $current = git log --oneline -1 2>$null
658
- if ($current -ne $initial) { break }
659
- Start-Sleep -Seconds 1
660
- $elapsed++
661
- Write-Host "." -ForegroundColor DarkGray -NoNewline
662
- }
663
- Write-Host ""
664
-
665
- if ($current -eq $initial) {
666
- Write-Host ""
667
- Write-Host " [!] No new commit detected (timed out after \${timeout}s)" -ForegroundColor Yellow
668
- Write-Host " Skipping PR creation." -ForegroundColor Yellow
669
- Write-Host ""
670
- exit
671
- }
672
-
673
- Start-Sleep -Seconds 2
674
-
675
- $branch = git rev-parse --abbrev-ref HEAD 2>$null
676
- $commit = git log --format='%h %s' -1 2>$null
677
-
678
- Write-Host ""
679
- Write-Host "==========================================" -ForegroundColor Cyan
680
- Write-Host " [OK] Commit & Push Complete" -ForegroundColor Green
681
- Write-Host "==========================================" -ForegroundColor Cyan
682
- Write-Host " Branch: $branch" -ForegroundColor Yellow
683
- Write-Host " Commit: $commit" -ForegroundColor White
684
- Write-Host ""
685
- git --no-pager diff HEAD~1 --stat --color=always 2>$null
686
- Write-Host ""
687
- Write-Host "==========================================" -ForegroundColor Cyan
688
- Write-Host ""
689
- } else {
690
- Write-Host "\`n==========================================" -ForegroundColor Cyan
691
- Write-Host " No changes to commit" -ForegroundColor DarkGray
692
- Write-Host " Proceeding to PR creation..." -ForegroundColor Cyan
693
- Write-Host "==========================================" -ForegroundColor Cyan
694
- Write-Host ""
695
- }
696
-
697
- & .\\create-pr.cmd
698
- `
699
- }
700
- ];
701
- var SCRIPT_TOOLS = [
702
- {
703
- name: "scripts-list",
704
- description: "List available MG Dashboard workflow scripts that can be downloaded into your project. Returns script names, filenames, and descriptions.",
705
- inputSchema: { type: "object", properties: {}, required: [] }
706
- },
707
- {
708
- name: "script-get",
709
- description: "Get the content of an MG Dashboard workflow script by name. Save the returned content to the filename indicated in the response. Place scripts in the repository root.",
710
- inputSchema: {
711
- type: "object",
712
- properties: {
713
- name: {
714
- type: "string",
715
- description: `Script name. Available: ${SCRIPTS.map((s) => s.name).join(", ")}`
716
- }
717
- },
718
- required: ["name"]
719
- }
720
- }
721
- ];
722
- var SCRIPT_TOOL_NAMES = new Set(SCRIPT_TOOLS.map((t) => t.name));
723
- function handleScriptTool(toolName, args2) {
724
- switch (toolName) {
725
- case "scripts-list": {
726
- const list = SCRIPTS.map((s) => ({
727
- name: s.name,
728
- filename: s.filename,
729
- description: s.description
730
- }));
731
- return {
732
- content: [
733
- {
734
- type: "text",
735
- text: JSON.stringify(list, null, 2)
736
- }
737
- ]
738
- };
739
- }
740
- case "script-get": {
741
- const scriptName = String(args2.name);
742
- const script = SCRIPTS.find((s) => s.name === scriptName);
743
- if (!script) {
744
- const available = SCRIPTS.map((s) => s.name).join(", ");
745
- return {
746
- content: [
747
- {
748
- type: "text",
749
- text: `Unknown script: "${scriptName}". Available scripts: ${available}`
750
- }
751
- ]
752
- };
753
- }
754
- return {
755
- content: [
756
- {
757
- type: "text",
758
- text: [
759
- `Filename: ${script.filename}`,
760
- `Description: ${script.description}`,
761
- `Place this file in the repository root.`,
762
- "",
763
- "--- CONTENT START ---",
764
- script.content,
765
- "--- CONTENT END ---"
766
- ].join("\n")
767
- }
768
- ]
769
- };
770
- }
771
- default:
772
- return { content: [{ type: "text", text: `Unknown script tool: ${toolName}` }] };
773
- }
774
- }
775
-
776
378
  // src/agent-tools.ts
777
379
  var VALID_FINDING_TYPES = /* @__PURE__ */ new Set([
778
380
  "missing_docs",
@@ -794,13 +396,15 @@ var VALID_SCOPES = /* @__PURE__ */ new Set([
794
396
  "module",
795
397
  "component",
796
398
  "api",
797
- "package"
399
+ "package",
400
+ "security",
401
+ "server_audit"
798
402
  ]);
799
403
  var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
800
404
  "pending",
801
- "approved",
802
- "rejected",
803
- "outdated"
405
+ "agent_approved",
406
+ "agent_flagged",
407
+ "human_approved"
804
408
  ]);
805
409
  var AGENT_TOOLS = [
806
410
  {
@@ -908,10 +512,10 @@ var AGENT_TOOLS = [
908
512
  },
909
513
  path: {
910
514
  type: "string",
911
- description: 'Path within the repo (e.g. "packages/ui", "__perf_audit__")'
515
+ description: 'Path within the repo (e.g. "packages/ui")'
912
516
  },
913
517
  title: { type: "string", description: "Document title" },
914
- content: { type: "string", description: "Documentation content (markdown)" },
518
+ content: { type: "string", description: "Documentation content (Typst markup)" },
915
519
  review_status: {
916
520
  type: "string",
917
521
  enum: [...VALID_REVIEW_STATUSES],
@@ -940,7 +544,7 @@ var AGENT_TOOLS = [
940
544
  },
941
545
  status: {
942
546
  type: "string",
943
- enum: ["open", "accepted", "rejected", "fixed"],
547
+ enum: ["open", "ticket_created", "resolved", "dismissed"],
944
548
  description: "Filter by status (default: all)"
945
549
  },
946
550
  limit: {
@@ -971,6 +575,56 @@ var AGENT_TOOLS = [
971
575
  },
972
576
  required: ["repo_slug"]
973
577
  }
578
+ },
579
+ {
580
+ name: "agent-validate-suggestions",
581
+ description: "Validate existing open suggestions against the actual codebase. For each suggestion, report whether it is valid, invalid (should be dismissed), or needs adjustment. Invalid suggestions are auto-dismissed with a reason.",
582
+ inputSchema: {
583
+ type: "object",
584
+ properties: {
585
+ repo_slug: {
586
+ type: "string",
587
+ description: "Repository slug being validated"
588
+ },
589
+ results: {
590
+ type: "array",
591
+ description: "Validation results per suggestion",
592
+ items: {
593
+ type: "object",
594
+ properties: {
595
+ suggestion_id: {
596
+ type: "string",
597
+ description: "UUID of the doc_suggestion being validated"
598
+ },
599
+ verdict: {
600
+ type: "string",
601
+ enum: ["valid", "invalid", "adjusted"],
602
+ description: "Validation verdict: valid (keep as-is), invalid (dismiss), adjusted (update fields)"
603
+ },
604
+ reason: {
605
+ type: "string",
606
+ description: "Explanation of why this suggestion is valid/invalid/adjusted (max 2000 chars)"
607
+ },
608
+ adjusted_description: {
609
+ type: "string",
610
+ description: "Updated description (only for verdict=adjusted, max 2000 chars)"
611
+ },
612
+ adjusted_severity: {
613
+ type: "string",
614
+ enum: ["info", "warning", "critical"],
615
+ description: "Updated severity (only for verdict=adjusted)"
616
+ },
617
+ adjusted_suggested_fix: {
618
+ type: "string",
619
+ description: "Updated suggested fix (only for verdict=adjusted, max 5000 chars)"
620
+ }
621
+ },
622
+ required: ["suggestion_id", "verdict", "reason"]
623
+ }
624
+ }
625
+ },
626
+ required: ["repo_slug", "results"]
627
+ }
974
628
  }
975
629
  ];
976
630
  var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
@@ -979,7 +633,8 @@ var AGENT_TOOL_MODULE_MAP = {
979
633
  "agent-report-finding": "agent_reporting",
980
634
  "agent-save-documentation": "agent_reporting",
981
635
  "agent-list-findings": "agent_reporting",
982
- "agent-get-documentation": "agent_reporting"
636
+ "agent-get-documentation": "agent_reporting",
637
+ "agent-validate-suggestions": "agent_reporting"
983
638
  };
984
639
  function clamp(val, min, max) {
985
640
  return Math.max(min, Math.min(max, val));
@@ -987,25 +642,24 @@ function clamp(val, min, max) {
987
642
  function sanitizeString(val, maxLen) {
988
643
  return String(val ?? "").slice(0, maxLen);
989
644
  }
990
- async function getOrCreateParentDoc(supabase2, repoSlug, refrontProjectId, category) {
991
- const path = category === "perf_audit" ? "__perf_audit__" : "__scan_findings__";
992
- const title = category === "perf_audit" ? `Performance Audit: ${repoSlug}` : `Scan Findings: ${repoSlug}`;
993
- const { data: existing } = await supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug).eq("path", path).maybeSingle();
994
- if (existing) return existing.id;
995
- const { data: inserted, error } = await supabase2.from("project_documentation").insert({
996
- refront_project_id: refrontProjectId,
997
- repo_slug: repoSlug,
998
- scope: "architecture",
999
- path,
1000
- title,
1001
- content: `Auto-generated parent record for ${category.replace("_", " ")} suggestions.`,
1002
- generated_by: `${category}-agent-v1`,
1003
- review_status: "pending"
1004
- }).select("id").single();
1005
- if (error || !inserted) {
1006
- throw new Error(`Failed to create parent doc record: ${error?.message}`);
1007
- }
1008
- return inserted.id;
645
+ function textSimilarity(a, b) {
646
+ if (a === b) return 1;
647
+ const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
648
+ const na = norm(a);
649
+ const nb = norm(b);
650
+ if (na === nb) return 1;
651
+ if (na.length < 3 || nb.length < 3) return na === nb ? 1 : 0;
652
+ const trigrams = (s) => {
653
+ const set = /* @__PURE__ */ new Set();
654
+ for (let i = 0; i <= s.length - 3; i++) set.add(s.slice(i, i + 3));
655
+ return set;
656
+ };
657
+ const setA = trigrams(na);
658
+ const setB = trigrams(nb);
659
+ let intersection = 0;
660
+ for (const t of setA) if (setB.has(t)) intersection++;
661
+ const union = setA.size + setB.size - intersection;
662
+ return union === 0 ? 0 : intersection / union;
1009
663
  }
1010
664
  async function handleAgentTool(name, args2, deps) {
1011
665
  const { supabase: supabase2 } = deps;
@@ -1061,30 +715,90 @@ async function handleAgentTool(name, args2, deps) {
1061
715
  throw new Error('category must be "scan_findings" or "perf_audit"');
1062
716
  }
1063
717
  if (findings.length === 0) throw new Error("findings array must not be empty");
1064
- const parentDocId = await getOrCreateParentDoc(supabase2, repoSlug, refrontProjectId, category);
718
+ const SIMILARITY_THRESHOLD = 0.55;
719
+ const MAX_OPEN_PER_TYPE = 30;
720
+ 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"]).limit(500);
721
+ const existing = existingFindings ?? [];
722
+ const typeCountMap = /* @__PURE__ */ new Map();
723
+ for (const e of existing) {
724
+ if (e.status !== "open") continue;
725
+ const key = `${e.category}:${e.type}`;
726
+ typeCountMap.set(key, (typeCountMap.get(key) ?? 0) + 1);
727
+ }
1065
728
  let inserted = 0;
729
+ let deduplicated = 0;
730
+ let grouped = 0;
1066
731
  let errors = 0;
732
+ const overflowBucket = /* @__PURE__ */ new Map();
1067
733
  for (const f of findings) {
1068
734
  const findingType = VALID_FINDING_TYPES.has(f.type) ? f.type : "improvement";
1069
735
  const severity = VALID_SEVERITIES.has(f.severity) ? f.severity : "info";
1070
736
  const description = sanitizeString(f.description, 2e3);
737
+ const filePath = f.file_path ? sanitizeString(f.file_path, 500) : null;
1071
738
  if (!description) continue;
739
+ const isDuplicate = existing.some((e) => {
740
+ if (e.type !== findingType) return false;
741
+ if (filePath && e.file_path === filePath) {
742
+ return textSimilarity(e.description ?? "", description) > 0.4;
743
+ }
744
+ return textSimilarity(e.description ?? "", description) > SIMILARITY_THRESHOLD;
745
+ });
746
+ if (isDuplicate) {
747
+ deduplicated++;
748
+ continue;
749
+ }
750
+ const typeKey = `${category}:${findingType}`;
751
+ const currentCount = typeCountMap.get(typeKey) ?? 0;
752
+ if (currentCount >= MAX_OPEN_PER_TYPE) {
753
+ const bucket = overflowBucket.get(typeKey) ?? [];
754
+ bucket.push(`${filePath ? `${filePath}: ` : ""}${description.slice(0, 120)}`);
755
+ overflowBucket.set(typeKey, bucket);
756
+ grouped++;
757
+ continue;
758
+ }
1072
759
  const { error } = await supabase2.from("doc_suggestion").insert({
1073
- documentation_id: parentDocId,
760
+ repo_slug: repoSlug,
761
+ refront_project_id: refrontProjectId,
762
+ category,
1074
763
  type: findingType,
1075
764
  severity,
1076
765
  description,
1077
- file_path: f.file_path ? sanitizeString(f.file_path, 500) : null,
766
+ file_path: filePath,
1078
767
  suggested_fix: f.suggested_fix ? sanitizeString(f.suggested_fix, 5e3) : null,
1079
768
  status: "open"
1080
769
  });
1081
- if (error) errors++;
1082
- else inserted++;
770
+ if (error) {
771
+ errors++;
772
+ } else {
773
+ inserted++;
774
+ typeCountMap.set(typeKey, currentCount + 1);
775
+ existing.push({ id: "", type: findingType, description, file_path: filePath, severity, category, status: "open" });
776
+ }
777
+ }
778
+ for (const [typeKey, items] of overflowBucket) {
779
+ const [cat, type] = typeKey.split(":");
780
+ const summary = `Gegroepeerd (${items.length} items):
781
+ ${items.map((i) => `\u2022 ${i}`).join("\n")}`;
782
+ await supabase2.from("doc_suggestion").insert({
783
+ repo_slug: repoSlug,
784
+ refront_project_id: refrontProjectId,
785
+ category: cat,
786
+ type,
787
+ severity: "info",
788
+ description: summary.slice(0, 5e3),
789
+ file_path: null,
790
+ suggested_fix: null,
791
+ status: "open"
792
+ });
1083
793
  }
794
+ const parts = [`${inserted} inserted`];
795
+ if (deduplicated > 0) parts.push(`${deduplicated} deduplicated`);
796
+ if (grouped > 0) parts.push(`${grouped} grouped into ${overflowBucket.size} summary row(s)`);
797
+ if (errors > 0) parts.push(`${errors} errors`);
1084
798
  return {
1085
799
  content: [{
1086
800
  type: "text",
1087
- text: `Findings reported: ${inserted} inserted under ${category}${errors > 0 ? `, ${errors} errors` : ""}`
801
+ text: `Findings reported under ${category}: ${parts.join(", ")}`
1088
802
  }]
1089
803
  };
1090
804
  }
@@ -1108,10 +822,12 @@ async function handleAgentTool(name, args2, deps) {
1108
822
  title,
1109
823
  content,
1110
824
  review_status: reviewStatus,
825
+ pdf_storage_path: null,
826
+ pdf_compiled_at: null,
1111
827
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1112
828
  }).eq("id", existing.id);
1113
829
  if (error2) throw new Error(`Failed to update documentation: ${error2.message}`);
1114
- return { content: [{ type: "text", text: `Documentation updated: ${title} (${scope}/${path})` }] };
830
+ return { content: [{ type: "text", text: `Documentation updated: ${title} (${scope}/${path}). PDF compilation pending.` }] };
1115
831
  }
1116
832
  const generatedBy = deps.workspaceId ? `agent-${deps.workspaceId.slice(0, 8)}` : "agent-mcp";
1117
833
  const { error } = await supabase2.from("project_documentation").insert({
@@ -1122,22 +838,19 @@ async function handleAgentTool(name, args2, deps) {
1122
838
  title,
1123
839
  content,
1124
840
  generated_by: generatedBy,
1125
- review_status: reviewStatus
841
+ review_status: reviewStatus,
842
+ pdf_storage_path: null,
843
+ pdf_compiled_at: null
1126
844
  });
1127
845
  if (error) throw new Error(`Failed to save documentation: ${error.message}`);
1128
- return { content: [{ type: "text", text: `Documentation saved: ${title} (${scope}/${path})` }] };
846
+ return { content: [{ type: "text", text: `Documentation saved: ${title} (${scope}/${path}). PDF compilation pending.` }] };
1129
847
  }
1130
848
  // -----------------------------------------------------------------
1131
849
  case "agent-list-findings": {
1132
850
  const repoSlug = sanitizeString(args2.repo_slug, 200);
1133
851
  if (!repoSlug) throw new Error("repo_slug is required");
1134
852
  const limit = clamp(Number(args2.limit) || 50, 1, 200);
1135
- let docQuery = supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug);
1136
- const docIds = await docQuery;
1137
- if (!docIds.data || docIds.data.length === 0) {
1138
- return { content: [{ type: "text", text: `No documentation records found for repo "${repoSlug}"` }] };
1139
- }
1140
- let query = supabase2.from("doc_suggestion").select("id, type, severity, description, file_path, status, created_at").in("documentation_id", docIds.data.map((d) => d.id)).order("created_at", { ascending: false }).limit(limit);
853
+ 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);
1141
854
  if (args2.type && VALID_FINDING_TYPES.has(args2.type)) {
1142
855
  query = query.eq("type", args2.type);
1143
856
  }
@@ -1199,6 +912,70 @@ ${output}` }]
1199
912
  };
1200
913
  }
1201
914
  // -----------------------------------------------------------------
915
+ case "agent-validate-suggestions": {
916
+ const repoSlug = sanitizeString(args2.repo_slug, 200);
917
+ const results = Array.isArray(args2.results) ? args2.results : [];
918
+ if (!repoSlug) throw new Error("repo_slug is required");
919
+ if (results.length === 0) throw new Error("results array must not be empty");
920
+ const validatedBy = deps.workspaceId ? `validator-${deps.workspaceId.slice(0, 8)}` : "validator-mcp";
921
+ const now = (/* @__PURE__ */ new Date()).toISOString();
922
+ let dismissed = 0;
923
+ let adjusted = 0;
924
+ let validated = 0;
925
+ let errors = 0;
926
+ for (const r of results) {
927
+ const id = sanitizeString(r.suggestion_id, 100);
928
+ const verdict = r.verdict;
929
+ const reason = sanitizeString(r.reason, 2e3);
930
+ if (!id || !["valid", "invalid", "adjusted"].includes(verdict)) {
931
+ errors++;
932
+ continue;
933
+ }
934
+ if (verdict === "invalid") {
935
+ const { error } = await supabase2.from("doc_suggestion").update({
936
+ status: "dismissed",
937
+ dismissed_reason: reason || "Dismissed by validation agent",
938
+ validated_at: now,
939
+ validated_by: validatedBy
940
+ }).eq("id", id).eq("repo_slug", repoSlug);
941
+ if (error) errors++;
942
+ else dismissed++;
943
+ } else if (verdict === "adjusted") {
944
+ const updates = {
945
+ validated_at: now,
946
+ validated_by: validatedBy
947
+ };
948
+ if (r.adjusted_description) {
949
+ updates.description = sanitizeString(r.adjusted_description, 2e3);
950
+ }
951
+ if (r.adjusted_severity && VALID_SEVERITIES.has(r.adjusted_severity)) {
952
+ updates.severity = r.adjusted_severity;
953
+ }
954
+ if (r.adjusted_suggested_fix) {
955
+ updates.suggested_fix = sanitizeString(r.adjusted_suggested_fix, 5e3);
956
+ }
957
+ const { error } = await supabase2.from("doc_suggestion").update(updates).eq("id", id).eq("repo_slug", repoSlug);
958
+ if (error) errors++;
959
+ else adjusted++;
960
+ } else {
961
+ const { error } = await supabase2.from("doc_suggestion").update({ validated_at: now, validated_by: validatedBy }).eq("id", id).eq("repo_slug", repoSlug);
962
+ if (error) errors++;
963
+ else validated++;
964
+ }
965
+ }
966
+ const parts = [];
967
+ if (validated > 0) parts.push(`${validated} valid`);
968
+ if (dismissed > 0) parts.push(`${dismissed} dismissed`);
969
+ if (adjusted > 0) parts.push(`${adjusted} adjusted`);
970
+ if (errors > 0) parts.push(`${errors} errors`);
971
+ return {
972
+ content: [{
973
+ type: "text",
974
+ text: `Validation complete for "${repoSlug}": ${parts.join(", ")}`
975
+ }]
976
+ };
977
+ }
978
+ // -----------------------------------------------------------------
1202
979
  default:
1203
980
  return { content: [{ type: "text", text: `Unknown agent tool: ${name}` }] };
1204
981
  }
@@ -1336,6 +1113,7 @@ var TOOL_MODULE_MAP = {
1336
1113
  "env-store": "ci_cd",
1337
1114
  "domain-list": "domains",
1338
1115
  "domain-get": "domains",
1116
+ "domain-update-ns": "domains",
1339
1117
  "dns-list": "domains",
1340
1118
  "dns-create": "domains",
1341
1119
  "dns-update": "domains",
@@ -1570,7 +1348,7 @@ async function attemptVercelSync(appName, environment, knownStageId) {
1570
1348
  const envVars = keys.map((key) => ({
1571
1349
  key,
1572
1350
  value: pairs[key],
1573
- type: "plain",
1351
+ type: "encrypted",
1574
1352
  ...targeting
1575
1353
  }));
1576
1354
  const { created, error } = await syncEnvVarsToVercel(token, app.vercelProjectId, envVars);
@@ -2366,7 +2144,7 @@ var TOOLS = [
2366
2144
  type: "object",
2367
2145
  properties: {
2368
2146
  appName: { type: "string", description: "Application name (e.g. backoffice, api, web)" },
2369
- environment: { type: "string", description: "Environment name (e.g. production, staging, development, local)" },
2147
+ environment: { type: "string", enum: ["production", "staging", "development", "local"], description: "Environment name" },
2370
2148
  releaseProfile: { type: "string", description: "Release profile name (usually matches the project folder name or git repo name, e.g. prefabaanbouw). Required when multiple profiles exist. Use env-list to discover available profiles." }
2371
2149
  },
2372
2150
  required: ["appName", "environment"]
@@ -2379,7 +2157,7 @@ var TOOLS = [
2379
2157
  type: "object",
2380
2158
  properties: {
2381
2159
  appName: { type: "string", description: "Application name (e.g. backoffice, api, web)" },
2382
- environment: { type: "string", description: "Environment name (e.g. production, staging, development, local)" },
2160
+ environment: { type: "string", enum: ["production", "staging", "development", "local"], description: "Environment name" },
2383
2161
  content: { type: "string", description: "The .env file content to store" },
2384
2162
  description: { type: "string", description: "Optional description" },
2385
2163
  releaseProfile: { type: "string", description: "Release profile name (usually matches the project folder name or git repo name, e.g. prefabaanbouw). Required when multiple profiles exist. Use env-list to discover available profiles." }
@@ -2415,6 +2193,18 @@ var TOOLS = [
2415
2193
  required: ["domain"]
2416
2194
  }
2417
2195
  },
2196
+ {
2197
+ name: "domain-update-ns",
2198
+ description: 'Update nameservers for a domain. Use alias "default-mijnhost" to reset to mijn.host defaults. Pushes the change to the domain registry.',
2199
+ inputSchema: {
2200
+ type: "object",
2201
+ properties: {
2202
+ domain: { type: "string", description: "Domain name (e.g. example.com)" },
2203
+ nameserver: { type: "string", description: 'Nameserver profile alias (e.g. "default-mijnhost")' }
2204
+ },
2205
+ required: ["domain", "nameserver"]
2206
+ }
2207
+ },
2418
2208
  {
2419
2209
  name: "dns-list",
2420
2210
  description: "List all DNS records for a domain. Returns type (A, AAAA, CNAME, MX, TXT, etc.), name, value, and TTL for each record.",
@@ -2473,8 +2263,6 @@ var TOOLS = [
2473
2263
  },
2474
2264
  // ----- Trigger.dev -----
2475
2265
  ...TRIGGER_TOOLS,
2476
- // ----- Scripts -----
2477
- ...SCRIPT_TOOLS,
2478
2266
  // ----- Agent Reporting -----
2479
2267
  ...AGENT_TOOLS
2480
2268
  ];
@@ -2823,6 +2611,24 @@ ${lines.join("\n")}` }] };
2823
2611
  }
2824
2612
  return { content: [{ type: "text", text: sections.join("\n") }] };
2825
2613
  }
2614
+ case "domain-update-ns": {
2615
+ const domain = String(a.domain);
2616
+ const nameserver = String(a.nameserver);
2617
+ if (!domain || !nameserver) throw new Error("domain and nameserver are required");
2618
+ await mijnhostFetch(`/domains/${encodeURIComponent(domain)}`, {
2619
+ method: "PUT",
2620
+ body: JSON.stringify({ nameserver })
2621
+ });
2622
+ const verify = await mijnhostFetch(`/domains/${encodeURIComponent(domain)}`);
2623
+ const ns = verify.data.nameservers;
2624
+ return {
2625
+ content: [{
2626
+ type: "text",
2627
+ text: `Nameservers for ${domain} updated to profile "${nameserver}".
2628
+ Current nameservers: ${ns.join(", ")}`
2629
+ }]
2630
+ };
2631
+ }
2826
2632
  case "dns-list": {
2827
2633
  const domain = String(a.domain);
2828
2634
  if (!domain) throw new Error("domain is required");
@@ -2924,9 +2730,6 @@ ${lines.join("\n")}` }] };
2924
2730
  if (TRIGGER_TOOL_NAMES.has(name)) {
2925
2731
  return handleTriggerTool(name, a, { sshExec, getServerConnection });
2926
2732
  }
2927
- if (SCRIPT_TOOL_NAMES.has(name)) {
2928
- return handleScriptTool(name, a);
2929
- }
2930
2733
  if (AGENT_TOOL_NAMES.has(name)) {
2931
2734
  return handleAgentTool(name, a, { supabase, workspaceId: agentWorkspaceId });
2932
2735
  }
@@ -2963,7 +2766,8 @@ async function main() {
2963
2766
  "/api/report-finding": "agent-report-finding",
2964
2767
  "/api/save-documentation": "agent-save-documentation",
2965
2768
  "/api/list-findings": "agent-list-findings",
2966
- "/api/get-documentation": "agent-get-documentation"
2769
+ "/api/get-documentation": "agent-get-documentation",
2770
+ "/api/validate-suggestions": "agent-validate-suggestions"
2967
2771
  };
2968
2772
  const httpServer = createServer(async (req, res) => {
2969
2773
  const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);