@mgsoftwarebv/mg-dashboard-mcp 2.2.3 → 2.3.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 +869 -397
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -72,18 +72,6 @@ var TRIGGER_TOOLS = [
|
|
|
72
72
|
required: ["project", "taskId"]
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
|
-
{
|
|
76
|
-
name: "trigger-cancel-run",
|
|
77
|
-
description: "Cancel a running or queued Trigger.dev task run.",
|
|
78
|
-
inputSchema: {
|
|
79
|
-
type: "object",
|
|
80
|
-
properties: {
|
|
81
|
-
project: { type: "string", description: "Project slug from trigger-list" },
|
|
82
|
-
runId: { type: "string", description: "Run ID to cancel (e.g. run_xxxxx)" }
|
|
83
|
-
},
|
|
84
|
-
required: ["project", "runId"]
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
75
|
{
|
|
88
76
|
name: "trigger-replay-run",
|
|
89
77
|
description: "Replay a previously failed or completed run with the same payload. Returns the new run ID.",
|
|
@@ -103,7 +91,6 @@ var TRIGGER_TOOL_MODULE_MAP = {
|
|
|
103
91
|
"trigger-runs": "ci_cd",
|
|
104
92
|
"trigger-run-detail": "ci_cd",
|
|
105
93
|
"trigger-test-task": "ci_cd",
|
|
106
|
-
"trigger-cancel-run": "ci_cd",
|
|
107
94
|
"trigger-replay-run": "ci_cd"
|
|
108
95
|
};
|
|
109
96
|
async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
|
|
@@ -262,14 +249,10 @@ ${rawJson.substring(0, 500)}` }] };
|
|
|
262
249
|
const project = String(args2.project);
|
|
263
250
|
const runId = String(args2.runId);
|
|
264
251
|
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
265
|
-
const rawJson = await
|
|
266
|
-
conn,
|
|
267
|
-
proxy,
|
|
268
|
-
|
|
269
|
-
instance,
|
|
270
|
-
"GET",
|
|
271
|
-
`/api/v3/runs/${encodeURIComponent(runId)}`
|
|
272
|
-
);
|
|
252
|
+
const [rawJson, logs] = await Promise.all([
|
|
253
|
+
triggerApi(conn, proxy, sshExec2, instance, "GET", `/api/v3/runs/${encodeURIComponent(runId)}`),
|
|
254
|
+
fetchRunLogs(runId, conn, proxy, sshExec2)
|
|
255
|
+
]);
|
|
273
256
|
let run;
|
|
274
257
|
try {
|
|
275
258
|
run = JSON.parse(rawJson);
|
|
@@ -277,7 +260,6 @@ ${rawJson.substring(0, 500)}` }] };
|
|
|
277
260
|
return { content: [{ type: "text", text: `Invalid API response:
|
|
278
261
|
${rawJson.substring(0, 500)}` }] };
|
|
279
262
|
}
|
|
280
|
-
const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
|
|
281
263
|
let text = formatRunDetail(run);
|
|
282
264
|
if (logs) {
|
|
283
265
|
text += "\n\n--- Logs ---\n" + logs;
|
|
@@ -358,7 +340,7 @@ ${triggerJson.substring(0, 500)}` }] };
|
|
|
358
340
|
};
|
|
359
341
|
}
|
|
360
342
|
// -----------------------------------------------------------------
|
|
361
|
-
case "trigger-
|
|
343
|
+
case "trigger-replay-run": {
|
|
362
344
|
const project = String(args2.project);
|
|
363
345
|
const runId = String(args2.runId);
|
|
364
346
|
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
@@ -368,53 +350,855 @@ ${triggerJson.substring(0, 500)}` }] };
|
|
|
368
350
|
sshExec2,
|
|
369
351
|
instance,
|
|
370
352
|
"POST",
|
|
371
|
-
`/api/
|
|
353
|
+
`/api/v1/runs/${encodeURIComponent(runId)}/replay`
|
|
372
354
|
);
|
|
373
355
|
let result;
|
|
374
356
|
try {
|
|
375
357
|
result = JSON.parse(rawJson);
|
|
376
358
|
} catch {
|
|
377
|
-
return { content: [{ type: "text", text: `
|
|
359
|
+
return { content: [{ type: "text", text: `Replay response:
|
|
378
360
|
${rawJson.substring(0, 500)}` }] };
|
|
379
361
|
}
|
|
380
362
|
return {
|
|
381
363
|
content: [{
|
|
382
364
|
type: "text",
|
|
383
|
-
text: `Run
|
|
365
|
+
text: result.id ? `Run replayed. New run ID: ${result.id}` : `Replay response:
|
|
366
|
+
${rawJson.substring(0, 500)}`
|
|
384
367
|
}]
|
|
385
368
|
};
|
|
386
369
|
}
|
|
387
370
|
// -----------------------------------------------------------------
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
371
|
+
default:
|
|
372
|
+
return { content: [{ type: "text", text: `Unknown trigger tool: ${name}` }] };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/script-tools.ts
|
|
377
|
+
var SCRIPTS = [
|
|
378
|
+
{
|
|
379
|
+
name: "commit-feedback",
|
|
380
|
+
filename: "commit-feedback.ps1",
|
|
381
|
+
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.",
|
|
382
|
+
content: `param()
|
|
383
|
+
|
|
384
|
+
$initial = git log --oneline -1 2>$null
|
|
385
|
+
|
|
386
|
+
Write-Host "\`n==========================================" -ForegroundColor Cyan
|
|
387
|
+
Write-Host " Starting Commit & Push..." -ForegroundColor Cyan
|
|
388
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
389
|
+
Write-Host " [1/5] Pulling latest..." -ForegroundColor DarkGray
|
|
390
|
+
Write-Host " [2/5] Staging changes..." -ForegroundColor DarkGray
|
|
391
|
+
Write-Host " [3/5] Generating AI commit message..." -ForegroundColor DarkGray
|
|
392
|
+
Write-Host " [4/5] Committing..." -ForegroundColor DarkGray
|
|
393
|
+
Write-Host " [5/5] Pushing to remote..." -ForegroundColor DarkGray
|
|
394
|
+
Write-Host ""
|
|
395
|
+
Write-Host " Waiting for commit" -ForegroundColor DarkGray -NoNewline
|
|
396
|
+
|
|
397
|
+
$timeout = 60
|
|
398
|
+
$elapsed = 0
|
|
399
|
+
while ($elapsed -lt $timeout) {
|
|
400
|
+
$current = git log --oneline -1 2>$null
|
|
401
|
+
if ($current -ne $initial) { break }
|
|
402
|
+
Start-Sleep -Seconds 1
|
|
403
|
+
$elapsed++
|
|
404
|
+
Write-Host "." -ForegroundColor DarkGray -NoNewline
|
|
405
|
+
}
|
|
406
|
+
Write-Host ""
|
|
407
|
+
|
|
408
|
+
if ($current -eq $initial) {
|
|
409
|
+
Write-Host ""
|
|
410
|
+
Write-Host " [!] No new commit detected (timed out after \${timeout}s)" -ForegroundColor Yellow
|
|
411
|
+
Write-Host ""
|
|
412
|
+
exit
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
Start-Sleep -Seconds 2
|
|
416
|
+
|
|
417
|
+
$branch = git rev-parse --abbrev-ref HEAD 2>$null
|
|
418
|
+
$commit = git log --format='%h %s' -1 2>$null
|
|
419
|
+
|
|
420
|
+
Write-Host ""
|
|
421
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
422
|
+
Write-Host " [OK] Commit & Push Complete" -ForegroundColor Green
|
|
423
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
424
|
+
Write-Host " Branch: $branch" -ForegroundColor Yellow
|
|
425
|
+
Write-Host " Commit: $commit" -ForegroundColor White
|
|
426
|
+
Write-Host ""
|
|
427
|
+
git --no-pager diff HEAD~1 --stat --color=always 2>$null
|
|
428
|
+
Write-Host ""
|
|
429
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
430
|
+
Write-Host ""
|
|
431
|
+
`
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "create-pr",
|
|
435
|
+
filename: "create-pr.cmd",
|
|
436
|
+
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.",
|
|
437
|
+
content: `@echo off
|
|
438
|
+
setlocal enabledelayedexpansion
|
|
439
|
+
title MG Dashboard - Create PR
|
|
440
|
+
|
|
441
|
+
REM --- ANSI color setup ---
|
|
442
|
+
for /F %%a in ('echo prompt $E ^| cmd') do set "ESC=%%a"
|
|
443
|
+
set "GREEN=%ESC%[32m"
|
|
444
|
+
set "RED=%ESC%[31m"
|
|
445
|
+
set "YELLOW=%ESC%[33m"
|
|
446
|
+
set "CYAN=%ESC%[36m"
|
|
447
|
+
set "DIM=%ESC%[90m"
|
|
448
|
+
set "BOLD=%ESC%[1m"
|
|
449
|
+
set "RESET=%ESC%[0m"
|
|
450
|
+
|
|
451
|
+
echo.
|
|
452
|
+
echo %CYAN%%BOLD%==========================================%RESET%
|
|
453
|
+
echo %CYAN%%BOLD% MG Dashboard - Create Pull Request%RESET%
|
|
454
|
+
echo %CYAN%%BOLD%==========================================%RESET%
|
|
455
|
+
echo.
|
|
456
|
+
|
|
457
|
+
REM --- Merge origin/main first ---
|
|
458
|
+
echo %CYAN%[0/4]%RESET% Merging origin/main into current branch...
|
|
459
|
+
git fetch --prune >nul 2>&1
|
|
460
|
+
git merge origin/main --no-edit >nul 2>&1
|
|
461
|
+
if errorlevel 1 (
|
|
462
|
+
git merge --abort >nul 2>&1
|
|
463
|
+
echo %RED%X Merge conflict with origin/main. Resolve manually.%RESET%
|
|
464
|
+
echo.
|
|
465
|
+
exit /b 1
|
|
466
|
+
)
|
|
467
|
+
echo %GREEN% OK%RESET%
|
|
468
|
+
echo.
|
|
469
|
+
|
|
470
|
+
REM --- Prerequisite: gh CLI ---
|
|
471
|
+
where gh >nul 2>&1
|
|
472
|
+
if errorlevel 1 (
|
|
473
|
+
echo %RED%X GitHub CLI ^(gh^) is not installed.%RESET%
|
|
474
|
+
echo %DIM% Install it from: https://cli.github.com/%RESET%
|
|
475
|
+
echo.
|
|
476
|
+
exit /b 1
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
REM --- Prerequisite: git repo ---
|
|
480
|
+
git rev-parse --is-inside-work-tree >nul 2>&1
|
|
481
|
+
if errorlevel 1 (
|
|
482
|
+
echo %RED%X Not inside a git repository.%RESET%
|
|
483
|
+
echo.
|
|
484
|
+
exit /b 1
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
REM --- Detect repo from git remote ---
|
|
488
|
+
set "REPO="
|
|
489
|
+
for /f "tokens=*" %%u in ('git remote get-url origin 2^>nul') do set "REMOTE_URL=%%u"
|
|
490
|
+
if defined REMOTE_URL (
|
|
491
|
+
echo !REMOTE_URL! | findstr /r "git@github.com:" >nul 2>&1
|
|
492
|
+
if not errorlevel 1 (
|
|
493
|
+
set "REPO=!REMOTE_URL:git@github.com:=!"
|
|
494
|
+
set "REPO=!REPO:.git=!"
|
|
495
|
+
)
|
|
496
|
+
echo !REMOTE_URL! | findstr /r "https://github.com/" >nul 2>&1
|
|
497
|
+
if not errorlevel 1 (
|
|
498
|
+
set "REPO=!REMOTE_URL:https://github.com/=!"
|
|
499
|
+
set "REPO=!REPO:.git=!"
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
if not defined REPO (
|
|
503
|
+
echo %RED%X Could not detect GitHub repo from git remote.%RESET%
|
|
504
|
+
echo %DIM% Make sure origin points to a GitHub repository.%RESET%
|
|
505
|
+
echo.
|
|
506
|
+
exit /b 1
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
REM --- Get current branch ---
|
|
510
|
+
for /f "tokens=*" %%b in ('git rev-parse --abbrev-ref HEAD') do set "CURRENT_BRANCH=%%b"
|
|
511
|
+
|
|
512
|
+
echo %DIM% Repository: !REPO!%RESET%
|
|
513
|
+
echo %DIM% Branch: %CURRENT_BRANCH%%RESET%
|
|
514
|
+
echo.
|
|
515
|
+
|
|
516
|
+
REM --- Guard: don't PR from main or a bare version branch ---
|
|
517
|
+
if "%CURRENT_BRANCH%"=="main" (
|
|
518
|
+
echo %RED%X You are on main. Switch to a feature or changes branch first.%RESET%
|
|
519
|
+
echo.
|
|
520
|
+
exit /b 1
|
|
521
|
+
)
|
|
522
|
+
echo %CURRENT_BRANCH% | findstr /r "^v[0-9]*\\.[0-9]*\\.[0-9]*$" >nul 2>&1
|
|
523
|
+
if not errorlevel 1 (
|
|
524
|
+
echo %RED%X You are on a version branch ^(%CURRENT_BRANCH%^). Switch to a feature or changes branch first.%RESET%
|
|
525
|
+
echo.
|
|
526
|
+
exit /b 1
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
REM --- Fetch latest remote refs ---
|
|
530
|
+
echo %CYAN%[1/4]%RESET% Fetching remote branches...
|
|
531
|
+
git fetch --prune >nul 2>&1
|
|
532
|
+
|
|
533
|
+
REM --- Determine target branch ---
|
|
534
|
+
set "TARGET="
|
|
535
|
+
|
|
536
|
+
REM Check if current branch is a -changes branch (last 8 chars)
|
|
537
|
+
if "!CURRENT_BRANCH:~-8!"=="-changes" (
|
|
538
|
+
set "TARGET=!CURRENT_BRANCH:~0,-8!"
|
|
539
|
+
echo %GREEN% Detected -changes branch. Target: %BOLD%!TARGET!%RESET%
|
|
540
|
+
goto :do_merge
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
REM Look for v*-changes branches on remote (highest version first)
|
|
544
|
+
set "TARGET="
|
|
545
|
+
for /f "tokens=*" %%r in ('git branch -r --sort=-version:refname --list "origin/v*-changes" 2^>nul') do (
|
|
546
|
+
if not defined TARGET (
|
|
547
|
+
set "RAW=%%r"
|
|
548
|
+
set "BRANCH=!RAW: origin/=!"
|
|
549
|
+
set "BRANCH=!BRANCH:origin/=!"
|
|
550
|
+
set "TARGET=!BRANCH!"
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if defined TARGET (
|
|
555
|
+
echo %GREEN% Release pipeline detected. Target: %BOLD%!TARGET!%RESET%
|
|
556
|
+
) else (
|
|
557
|
+
REM No -changes branch; try highest version branch
|
|
558
|
+
set "TARGET="
|
|
559
|
+
for /f "tokens=*" %%r in ('git branch -r --sort=-version:refname --list "origin/v*" 2^>nul') do (
|
|
560
|
+
if not defined TARGET (
|
|
561
|
+
set "RAW=%%r"
|
|
562
|
+
set "BRANCH=!RAW: origin/=!"
|
|
563
|
+
set "BRANCH=!BRANCH:origin/=!"
|
|
564
|
+
set "TAIL=!BRANCH:~-8!"
|
|
565
|
+
if not "!TAIL!"=="-changes" set "TARGET=!BRANCH!"
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
if defined TARGET (
|
|
569
|
+
echo %GREEN% Version branch detected. Target: %BOLD%!TARGET!%RESET%
|
|
570
|
+
) else (
|
|
571
|
+
set "TARGET=main"
|
|
572
|
+
echo %YELLOW% No release pipeline or version branch found. Target: %BOLD%main%RESET%
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
:do_merge
|
|
577
|
+
echo.
|
|
578
|
+
|
|
579
|
+
REM --- Merge target into current branch ---
|
|
580
|
+
echo %CYAN%[2/4]%RESET% Merging %BOLD%!TARGET!%RESET% into %BOLD%%CURRENT_BRANCH%%RESET%...
|
|
581
|
+
git merge origin/!TARGET! --no-edit
|
|
582
|
+
if errorlevel 1 (
|
|
583
|
+
echo.
|
|
584
|
+
echo %RED%X Merge conflict detected.%RESET%
|
|
585
|
+
git merge --abort >nul 2>&1
|
|
586
|
+
echo %DIM% Resolve conflicts manually, then retry.%RESET%
|
|
587
|
+
echo.
|
|
588
|
+
exit /b 1
|
|
589
|
+
)
|
|
590
|
+
echo %GREEN% OK%RESET%
|
|
591
|
+
echo.
|
|
592
|
+
|
|
593
|
+
REM --- Push current branch to origin ---
|
|
594
|
+
echo %CYAN%[3/4]%RESET% Pushing %CURRENT_BRANCH% to origin...
|
|
595
|
+
git push -u origin HEAD
|
|
596
|
+
if errorlevel 1 (
|
|
597
|
+
echo.
|
|
598
|
+
echo %RED%X Failed to push branch to origin.%RESET%
|
|
599
|
+
echo.
|
|
600
|
+
exit /b 1
|
|
601
|
+
)
|
|
602
|
+
echo %GREEN% OK%RESET%
|
|
603
|
+
echo.
|
|
604
|
+
|
|
605
|
+
REM --- Guard: don't PR into the same branch ---
|
|
606
|
+
if "!TARGET!"=="!CURRENT_BRANCH!" (
|
|
607
|
+
echo %RED%X Target branch is the same as current branch ^(!TARGET!^). Cannot create PR.%RESET%
|
|
608
|
+
echo.
|
|
609
|
+
exit /b 1
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
echo %CYAN%[4/4]%RESET% Creating PR: %BOLD%%CURRENT_BRANCH%%RESET% %DIM%->%RESET% %BOLD%%TARGET%%RESET%
|
|
613
|
+
echo.
|
|
614
|
+
|
|
615
|
+
REM --- Create the PR ---
|
|
616
|
+
gh pr create --repo "!REPO!" --base "%TARGET%" --fill
|
|
617
|
+
if errorlevel 1 (
|
|
618
|
+
echo.
|
|
619
|
+
echo %RED%X Failed to create pull request.%RESET%
|
|
620
|
+
echo %DIM% Check the error above. A PR may already exist for this branch.%RESET%
|
|
621
|
+
echo.
|
|
622
|
+
exit /b 1
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
echo.
|
|
626
|
+
echo %GREEN%%BOLD% PR created successfully!%RESET%
|
|
627
|
+
echo.
|
|
628
|
+
`
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "commit-and-pr",
|
|
632
|
+
filename: "commit-and-pr.ps1",
|
|
633
|
+
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.",
|
|
634
|
+
content: `param()
|
|
635
|
+
|
|
636
|
+
$hasChanges = (git status --porcelain 2>$null)
|
|
637
|
+
|
|
638
|
+
if ($hasChanges) {
|
|
639
|
+
$initial = git log --oneline -1 2>$null
|
|
640
|
+
|
|
641
|
+
Write-Host "\`n==========================================" -ForegroundColor Cyan
|
|
642
|
+
Write-Host " Starting Commit, Push & PR..." -ForegroundColor Cyan
|
|
643
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
644
|
+
Write-Host " [1/5] Pulling latest..." -ForegroundColor DarkGray
|
|
645
|
+
Write-Host " [2/5] Staging changes..." -ForegroundColor DarkGray
|
|
646
|
+
Write-Host " [3/5] Generating AI commit message..." -ForegroundColor DarkGray
|
|
647
|
+
Write-Host " [4/5] Committing..." -ForegroundColor DarkGray
|
|
648
|
+
Write-Host " [5/5] Pushing to remote..." -ForegroundColor DarkGray
|
|
649
|
+
Write-Host ""
|
|
650
|
+
Write-Host " Waiting for commit" -ForegroundColor DarkGray -NoNewline
|
|
651
|
+
|
|
652
|
+
$timeout = 60
|
|
653
|
+
$elapsed = 0
|
|
654
|
+
while ($elapsed -lt $timeout) {
|
|
655
|
+
$current = git log --oneline -1 2>$null
|
|
656
|
+
if ($current -ne $initial) { break }
|
|
657
|
+
Start-Sleep -Seconds 1
|
|
658
|
+
$elapsed++
|
|
659
|
+
Write-Host "." -ForegroundColor DarkGray -NoNewline
|
|
660
|
+
}
|
|
661
|
+
Write-Host ""
|
|
662
|
+
|
|
663
|
+
if ($current -eq $initial) {
|
|
664
|
+
Write-Host ""
|
|
665
|
+
Write-Host " [!] No new commit detected (timed out after \${timeout}s)" -ForegroundColor Yellow
|
|
666
|
+
Write-Host " Skipping PR creation." -ForegroundColor Yellow
|
|
667
|
+
Write-Host ""
|
|
668
|
+
exit
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
Start-Sleep -Seconds 2
|
|
672
|
+
|
|
673
|
+
$branch = git rev-parse --abbrev-ref HEAD 2>$null
|
|
674
|
+
$commit = git log --format='%h %s' -1 2>$null
|
|
675
|
+
|
|
676
|
+
Write-Host ""
|
|
677
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
678
|
+
Write-Host " [OK] Commit & Push Complete" -ForegroundColor Green
|
|
679
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
680
|
+
Write-Host " Branch: $branch" -ForegroundColor Yellow
|
|
681
|
+
Write-Host " Commit: $commit" -ForegroundColor White
|
|
682
|
+
Write-Host ""
|
|
683
|
+
git --no-pager diff HEAD~1 --stat --color=always 2>$null
|
|
684
|
+
Write-Host ""
|
|
685
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
686
|
+
Write-Host ""
|
|
687
|
+
} else {
|
|
688
|
+
Write-Host "\`n==========================================" -ForegroundColor Cyan
|
|
689
|
+
Write-Host " No changes to commit" -ForegroundColor DarkGray
|
|
690
|
+
Write-Host " Proceeding to PR creation..." -ForegroundColor Cyan
|
|
691
|
+
Write-Host "==========================================" -ForegroundColor Cyan
|
|
692
|
+
Write-Host ""
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
& .\\create-pr.cmd
|
|
696
|
+
`
|
|
697
|
+
}
|
|
698
|
+
];
|
|
699
|
+
var SCRIPT_TOOLS = [
|
|
700
|
+
{
|
|
701
|
+
name: "scripts-list",
|
|
702
|
+
description: "List available MG Dashboard workflow scripts that can be downloaded into your project. Returns script names, filenames, and descriptions.",
|
|
703
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "script-get",
|
|
707
|
+
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.",
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: "object",
|
|
710
|
+
properties: {
|
|
711
|
+
name: {
|
|
712
|
+
type: "string",
|
|
713
|
+
description: `Script name. Available: ${SCRIPTS.map((s) => s.name).join(", ")}`
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
required: ["name"]
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
];
|
|
720
|
+
var SCRIPT_TOOL_NAMES = new Set(SCRIPT_TOOLS.map((t) => t.name));
|
|
721
|
+
function handleScriptTool(toolName, args2) {
|
|
722
|
+
switch (toolName) {
|
|
723
|
+
case "scripts-list": {
|
|
724
|
+
const list = SCRIPTS.map((s) => ({
|
|
725
|
+
name: s.name,
|
|
726
|
+
filename: s.filename,
|
|
727
|
+
description: s.description
|
|
728
|
+
}));
|
|
729
|
+
return {
|
|
730
|
+
content: [
|
|
731
|
+
{
|
|
732
|
+
type: "text",
|
|
733
|
+
text: JSON.stringify(list, null, 2)
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
case "script-get": {
|
|
739
|
+
const scriptName = String(args2.name);
|
|
740
|
+
const script = SCRIPTS.find((s) => s.name === scriptName);
|
|
741
|
+
if (!script) {
|
|
742
|
+
const available = SCRIPTS.map((s) => s.name).join(", ");
|
|
743
|
+
return {
|
|
744
|
+
content: [
|
|
745
|
+
{
|
|
746
|
+
type: "text",
|
|
747
|
+
text: `Unknown script: "${scriptName}". Available scripts: ${available}`
|
|
748
|
+
}
|
|
749
|
+
]
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
content: [
|
|
754
|
+
{
|
|
755
|
+
type: "text",
|
|
756
|
+
text: [
|
|
757
|
+
`Filename: ${script.filename}`,
|
|
758
|
+
`Description: ${script.description}`,
|
|
759
|
+
`Place this file in the repository root.`,
|
|
760
|
+
"",
|
|
761
|
+
"--- CONTENT START ---",
|
|
762
|
+
script.content,
|
|
763
|
+
"--- CONTENT END ---"
|
|
764
|
+
].join("\n")
|
|
765
|
+
}
|
|
766
|
+
]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
default:
|
|
770
|
+
return { content: [{ type: "text", text: `Unknown script tool: ${toolName}` }] };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/agent-tools.ts
|
|
775
|
+
var VALID_FINDING_TYPES = /* @__PURE__ */ new Set([
|
|
776
|
+
"missing_docs",
|
|
777
|
+
"inaccurate",
|
|
778
|
+
"incomplete",
|
|
779
|
+
"outdated",
|
|
780
|
+
"improvement",
|
|
781
|
+
"n_plus_1_query",
|
|
782
|
+
"bundle_size",
|
|
783
|
+
"unnecessary_rerender",
|
|
784
|
+
"slow_endpoint",
|
|
785
|
+
"memory_leak",
|
|
786
|
+
"missing_memoization",
|
|
787
|
+
"large_dependency"
|
|
788
|
+
]);
|
|
789
|
+
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["info", "warning", "critical"]);
|
|
790
|
+
var VALID_SCOPES = /* @__PURE__ */ new Set([
|
|
791
|
+
"architecture",
|
|
792
|
+
"module",
|
|
793
|
+
"component",
|
|
794
|
+
"api",
|
|
795
|
+
"package"
|
|
796
|
+
]);
|
|
797
|
+
var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
|
|
798
|
+
"pending",
|
|
799
|
+
"approved",
|
|
800
|
+
"rejected",
|
|
801
|
+
"outdated"
|
|
802
|
+
]);
|
|
803
|
+
var AGENT_TOOLS = [
|
|
804
|
+
{
|
|
805
|
+
name: "agent-report-coverage",
|
|
806
|
+
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.",
|
|
807
|
+
inputSchema: {
|
|
808
|
+
type: "object",
|
|
809
|
+
properties: {
|
|
810
|
+
repo_slug: {
|
|
811
|
+
type: "string",
|
|
812
|
+
description: 'Repository slug (e.g. "mg-dashboard", "bna-wordpress")'
|
|
813
|
+
},
|
|
814
|
+
refront_project_id: {
|
|
815
|
+
type: "string",
|
|
816
|
+
description: "Refront project UUID linked to this repository"
|
|
817
|
+
},
|
|
818
|
+
entries: {
|
|
819
|
+
type: "array",
|
|
820
|
+
description: "Array of coverage entries, one per logical unit",
|
|
821
|
+
items: {
|
|
822
|
+
type: "object",
|
|
823
|
+
properties: {
|
|
824
|
+
path: { type: "string", description: 'Logical unit path (e.g. "packages/ui", "wp-content/themes/bna")' },
|
|
825
|
+
total_functions: { type: "number", description: "Total public functions/methods" },
|
|
826
|
+
documented_functions: { type: "number", description: "Functions with doc comments" },
|
|
827
|
+
total_types: { type: "number", description: "Total classes/interfaces/types" },
|
|
828
|
+
documented_types: { type: "number", description: "Types with doc comments" },
|
|
829
|
+
total_endpoints: { type: "number", description: "Total API/REST/hook endpoints" },
|
|
830
|
+
documented_endpoints: { type: "number", description: "Endpoints with documentation" }
|
|
831
|
+
},
|
|
832
|
+
required: ["path", "total_functions", "documented_functions"]
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
required: ["repo_slug", "refront_project_id", "entries"]
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: "agent-report-finding",
|
|
841
|
+
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.",
|
|
842
|
+
inputSchema: {
|
|
843
|
+
type: "object",
|
|
844
|
+
properties: {
|
|
845
|
+
repo_slug: {
|
|
846
|
+
type: "string",
|
|
847
|
+
description: "Repository slug"
|
|
848
|
+
},
|
|
849
|
+
refront_project_id: {
|
|
850
|
+
type: "string",
|
|
851
|
+
description: "Refront project UUID linked to this repository"
|
|
852
|
+
},
|
|
853
|
+
category: {
|
|
854
|
+
type: "string",
|
|
855
|
+
enum: ["scan_findings", "perf_audit"],
|
|
856
|
+
description: 'Finding category: "scan_findings" for doc issues, "perf_audit" for performance issues'
|
|
857
|
+
},
|
|
858
|
+
findings: {
|
|
859
|
+
type: "array",
|
|
860
|
+
description: "Array of findings to report",
|
|
861
|
+
items: {
|
|
862
|
+
type: "object",
|
|
863
|
+
properties: {
|
|
864
|
+
type: {
|
|
865
|
+
type: "string",
|
|
866
|
+
enum: [...VALID_FINDING_TYPES],
|
|
867
|
+
description: "Finding type"
|
|
868
|
+
},
|
|
869
|
+
severity: {
|
|
870
|
+
type: "string",
|
|
871
|
+
enum: ["info", "warning", "critical"],
|
|
872
|
+
description: "Severity level"
|
|
873
|
+
},
|
|
874
|
+
description: {
|
|
875
|
+
type: "string",
|
|
876
|
+
description: "Clear description of the issue (max 2000 chars)"
|
|
877
|
+
},
|
|
878
|
+
file_path: {
|
|
879
|
+
type: "string",
|
|
880
|
+
description: "Relative file path where the issue was found"
|
|
881
|
+
},
|
|
882
|
+
suggested_fix: {
|
|
883
|
+
type: "string",
|
|
884
|
+
description: "Suggested fix with code example (max 5000 chars)"
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
required: ["type", "severity", "description"]
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
required: ["repo_slug", "refront_project_id", "category", "findings"]
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
name: "agent-save-documentation",
|
|
896
|
+
description: "Save or update a documentation record for a repository. Upserts by repo_slug + scope + path combination.",
|
|
897
|
+
inputSchema: {
|
|
898
|
+
type: "object",
|
|
899
|
+
properties: {
|
|
900
|
+
repo_slug: { type: "string", description: "Repository slug" },
|
|
901
|
+
refront_project_id: { type: "string", description: "Refront project UUID" },
|
|
902
|
+
scope: {
|
|
903
|
+
type: "string",
|
|
904
|
+
enum: [...VALID_SCOPES],
|
|
905
|
+
description: "Documentation scope"
|
|
906
|
+
},
|
|
907
|
+
path: {
|
|
908
|
+
type: "string",
|
|
909
|
+
description: 'Path within the repo (e.g. "packages/ui", "__perf_audit__")'
|
|
910
|
+
},
|
|
911
|
+
title: { type: "string", description: "Document title" },
|
|
912
|
+
content: { type: "string", description: "Documentation content (markdown)" },
|
|
913
|
+
review_status: {
|
|
914
|
+
type: "string",
|
|
915
|
+
enum: [...VALID_REVIEW_STATUSES],
|
|
916
|
+
description: 'Review status (default: "pending")'
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
required: ["repo_slug", "refront_project_id", "scope", "path", "title", "content"]
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
name: "agent-list-findings",
|
|
924
|
+
description: "List existing findings (doc_suggestion records) for a repository. Use this to check what has already been reported before submitting new findings.",
|
|
925
|
+
inputSchema: {
|
|
926
|
+
type: "object",
|
|
927
|
+
properties: {
|
|
928
|
+
repo_slug: { type: "string", description: "Repository slug" },
|
|
929
|
+
type: {
|
|
930
|
+
type: "string",
|
|
931
|
+
enum: [...VALID_FINDING_TYPES],
|
|
932
|
+
description: "Filter by finding type"
|
|
933
|
+
},
|
|
934
|
+
severity: {
|
|
935
|
+
type: "string",
|
|
936
|
+
enum: ["info", "warning", "critical"],
|
|
937
|
+
description: "Filter by severity"
|
|
938
|
+
},
|
|
939
|
+
status: {
|
|
940
|
+
type: "string",
|
|
941
|
+
enum: ["open", "accepted", "rejected", "fixed"],
|
|
942
|
+
description: "Filter by status (default: all)"
|
|
943
|
+
},
|
|
944
|
+
limit: {
|
|
945
|
+
type: "number",
|
|
946
|
+
description: "Max results to return (default: 50, max: 200)"
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
required: ["repo_slug"]
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
name: "agent-get-documentation",
|
|
954
|
+
description: "Retrieve existing documentation records for a repository. Use this to read current docs before generating or reviewing.",
|
|
955
|
+
inputSchema: {
|
|
956
|
+
type: "object",
|
|
957
|
+
properties: {
|
|
958
|
+
repo_slug: { type: "string", description: "Repository slug" },
|
|
959
|
+
scope: {
|
|
960
|
+
type: "string",
|
|
961
|
+
enum: [...VALID_SCOPES],
|
|
962
|
+
description: "Filter by scope"
|
|
963
|
+
},
|
|
964
|
+
path: { type: "string", description: "Filter by exact path" },
|
|
965
|
+
limit: {
|
|
966
|
+
type: "number",
|
|
967
|
+
description: "Max results to return (default: 20, max: 100)"
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
required: ["repo_slug"]
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
];
|
|
974
|
+
var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
|
|
975
|
+
var AGENT_TOOL_MODULE_MAP = {
|
|
976
|
+
"agent-report-coverage": "agent_reporting",
|
|
977
|
+
"agent-report-finding": "agent_reporting",
|
|
978
|
+
"agent-save-documentation": "agent_reporting",
|
|
979
|
+
"agent-list-findings": "agent_reporting",
|
|
980
|
+
"agent-get-documentation": "agent_reporting"
|
|
981
|
+
};
|
|
982
|
+
function clamp(val, min, max) {
|
|
983
|
+
return Math.max(min, Math.min(max, val));
|
|
984
|
+
}
|
|
985
|
+
function sanitizeString(val, maxLen) {
|
|
986
|
+
return String(val ?? "").slice(0, maxLen);
|
|
987
|
+
}
|
|
988
|
+
async function getOrCreateParentDoc(supabase2, repoSlug, refrontProjectId, category) {
|
|
989
|
+
const path = category === "perf_audit" ? "__perf_audit__" : "__scan_findings__";
|
|
990
|
+
const title = category === "perf_audit" ? `Performance Audit: ${repoSlug}` : `Scan Findings: ${repoSlug}`;
|
|
991
|
+
const { data: existing } = await supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug).eq("path", path).maybeSingle();
|
|
992
|
+
if (existing) return existing.id;
|
|
993
|
+
const { data: inserted, error } = await supabase2.from("project_documentation").insert({
|
|
994
|
+
refront_project_id: refrontProjectId,
|
|
995
|
+
repo_slug: repoSlug,
|
|
996
|
+
scope: "architecture",
|
|
997
|
+
path,
|
|
998
|
+
title,
|
|
999
|
+
content: `Auto-generated parent record for ${category.replace("_", " ")} suggestions.`,
|
|
1000
|
+
generated_by: `${category}-agent-v1`,
|
|
1001
|
+
review_status: "pending"
|
|
1002
|
+
}).select("id").single();
|
|
1003
|
+
if (error || !inserted) {
|
|
1004
|
+
throw new Error(`Failed to create parent doc record: ${error?.message}`);
|
|
1005
|
+
}
|
|
1006
|
+
return inserted.id;
|
|
1007
|
+
}
|
|
1008
|
+
async function handleAgentTool(name, args2, deps) {
|
|
1009
|
+
const { supabase: supabase2 } = deps;
|
|
1010
|
+
switch (name) {
|
|
1011
|
+
// -----------------------------------------------------------------
|
|
1012
|
+
case "agent-report-coverage": {
|
|
1013
|
+
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1014
|
+
const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
|
|
1015
|
+
const entries = Array.isArray(args2.entries) ? args2.entries : [];
|
|
1016
|
+
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1017
|
+
if (!refrontProjectId) throw new Error("refront_project_id is required");
|
|
1018
|
+
if (entries.length === 0) throw new Error("entries array must not be empty");
|
|
1019
|
+
const wsId = deps.workspaceId;
|
|
1020
|
+
const scanCommit = wsId ? `agent-scan-${wsId.slice(0, 8)}` : `agent-scan-${Date.now().toString(36)}`;
|
|
1021
|
+
let upserted = 0;
|
|
1022
|
+
let errors = 0;
|
|
1023
|
+
for (const entry of entries) {
|
|
1024
|
+
const { error } = await supabase2.from("doc_coverage").upsert(
|
|
1025
|
+
{
|
|
1026
|
+
refront_project_id: refrontProjectId,
|
|
1027
|
+
repo_slug: repoSlug,
|
|
1028
|
+
path: sanitizeString(entry.path, 500),
|
|
1029
|
+
total_functions: clamp(Number(entry.total_functions) || 0, 0, 99999),
|
|
1030
|
+
documented_functions: clamp(Number(entry.documented_functions) || 0, 0, 99999),
|
|
1031
|
+
total_types: clamp(Number(entry.total_types) || 0, 0, 99999),
|
|
1032
|
+
documented_types: clamp(Number(entry.documented_types) || 0, 0, 99999),
|
|
1033
|
+
total_endpoints: clamp(Number(entry.total_endpoints) || 0, 0, 99999),
|
|
1034
|
+
documented_endpoints: clamp(Number(entry.documented_endpoints) || 0, 0, 99999),
|
|
1035
|
+
scan_commit: scanCommit,
|
|
1036
|
+
scanned_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1037
|
+
},
|
|
1038
|
+
{ onConflict: "repo_slug,path" }
|
|
1039
|
+
);
|
|
1040
|
+
if (error) errors++;
|
|
1041
|
+
else upserted++;
|
|
406
1042
|
}
|
|
407
1043
|
return {
|
|
408
1044
|
content: [{
|
|
409
1045
|
type: "text",
|
|
410
|
-
text:
|
|
411
|
-
|
|
1046
|
+
text: `Coverage reported: ${upserted} entries upserted${errors > 0 ? `, ${errors} errors` : ""}`
|
|
1047
|
+
}]
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
// -----------------------------------------------------------------
|
|
1051
|
+
case "agent-report-finding": {
|
|
1052
|
+
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1053
|
+
const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
|
|
1054
|
+
const category = args2.category;
|
|
1055
|
+
const findings = Array.isArray(args2.findings) ? args2.findings : [];
|
|
1056
|
+
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1057
|
+
if (!refrontProjectId) throw new Error("refront_project_id is required");
|
|
1058
|
+
if (!category || !["scan_findings", "perf_audit"].includes(category)) {
|
|
1059
|
+
throw new Error('category must be "scan_findings" or "perf_audit"');
|
|
1060
|
+
}
|
|
1061
|
+
if (findings.length === 0) throw new Error("findings array must not be empty");
|
|
1062
|
+
const parentDocId = await getOrCreateParentDoc(supabase2, repoSlug, refrontProjectId, category);
|
|
1063
|
+
let inserted = 0;
|
|
1064
|
+
let errors = 0;
|
|
1065
|
+
for (const f of findings) {
|
|
1066
|
+
const findingType = VALID_FINDING_TYPES.has(f.type) ? f.type : "improvement";
|
|
1067
|
+
const severity = VALID_SEVERITIES.has(f.severity) ? f.severity : "info";
|
|
1068
|
+
const description = sanitizeString(f.description, 2e3);
|
|
1069
|
+
if (!description) continue;
|
|
1070
|
+
const { error } = await supabase2.from("doc_suggestion").insert({
|
|
1071
|
+
documentation_id: parentDocId,
|
|
1072
|
+
type: findingType,
|
|
1073
|
+
severity,
|
|
1074
|
+
description,
|
|
1075
|
+
file_path: f.file_path ? sanitizeString(f.file_path, 500) : null,
|
|
1076
|
+
suggested_fix: f.suggested_fix ? sanitizeString(f.suggested_fix, 5e3) : null,
|
|
1077
|
+
status: "open"
|
|
1078
|
+
});
|
|
1079
|
+
if (error) errors++;
|
|
1080
|
+
else inserted++;
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
content: [{
|
|
1084
|
+
type: "text",
|
|
1085
|
+
text: `Findings reported: ${inserted} inserted under ${category}${errors > 0 ? `, ${errors} errors` : ""}`
|
|
412
1086
|
}]
|
|
413
1087
|
};
|
|
414
1088
|
}
|
|
415
1089
|
// -----------------------------------------------------------------
|
|
1090
|
+
case "agent-save-documentation": {
|
|
1091
|
+
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1092
|
+
const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
|
|
1093
|
+
const scope = VALID_SCOPES.has(args2.scope) ? args2.scope : "module";
|
|
1094
|
+
const path = sanitizeString(args2.path, 500);
|
|
1095
|
+
const title = sanitizeString(args2.title, 500);
|
|
1096
|
+
const content = sanitizeString(args2.content, 1e5);
|
|
1097
|
+
const reviewStatus = VALID_REVIEW_STATUSES.has(args2.review_status) ? args2.review_status : "pending";
|
|
1098
|
+
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1099
|
+
if (!refrontProjectId) throw new Error("refront_project_id is required");
|
|
1100
|
+
if (!path) throw new Error("path is required");
|
|
1101
|
+
if (!title) throw new Error("title is required");
|
|
1102
|
+
if (!content) throw new Error("content is required");
|
|
1103
|
+
const { data: existing } = await supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug).eq("scope", scope).eq("path", path).maybeSingle();
|
|
1104
|
+
if (existing) {
|
|
1105
|
+
const { error: error2 } = await supabase2.from("project_documentation").update({
|
|
1106
|
+
title,
|
|
1107
|
+
content,
|
|
1108
|
+
review_status: reviewStatus,
|
|
1109
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1110
|
+
}).eq("id", existing.id);
|
|
1111
|
+
if (error2) throw new Error(`Failed to update documentation: ${error2.message}`);
|
|
1112
|
+
return { content: [{ type: "text", text: `Documentation updated: ${title} (${scope}/${path})` }] };
|
|
1113
|
+
}
|
|
1114
|
+
const generatedBy = deps.workspaceId ? `agent-${deps.workspaceId.slice(0, 8)}` : "agent-mcp";
|
|
1115
|
+
const { error } = await supabase2.from("project_documentation").insert({
|
|
1116
|
+
refront_project_id: refrontProjectId,
|
|
1117
|
+
repo_slug: repoSlug,
|
|
1118
|
+
scope,
|
|
1119
|
+
path,
|
|
1120
|
+
title,
|
|
1121
|
+
content,
|
|
1122
|
+
generated_by: generatedBy,
|
|
1123
|
+
review_status: reviewStatus
|
|
1124
|
+
});
|
|
1125
|
+
if (error) throw new Error(`Failed to save documentation: ${error.message}`);
|
|
1126
|
+
return { content: [{ type: "text", text: `Documentation saved: ${title} (${scope}/${path})` }] };
|
|
1127
|
+
}
|
|
1128
|
+
// -----------------------------------------------------------------
|
|
1129
|
+
case "agent-list-findings": {
|
|
1130
|
+
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1131
|
+
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1132
|
+
const limit = clamp(Number(args2.limit) || 50, 1, 200);
|
|
1133
|
+
let docQuery = supabase2.from("project_documentation").select("id").eq("repo_slug", repoSlug);
|
|
1134
|
+
const docIds = await docQuery;
|
|
1135
|
+
if (!docIds.data || docIds.data.length === 0) {
|
|
1136
|
+
return { content: [{ type: "text", text: `No documentation records found for repo "${repoSlug}"` }] };
|
|
1137
|
+
}
|
|
1138
|
+
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);
|
|
1139
|
+
if (args2.type && VALID_FINDING_TYPES.has(args2.type)) {
|
|
1140
|
+
query = query.eq("type", args2.type);
|
|
1141
|
+
}
|
|
1142
|
+
if (args2.severity && VALID_SEVERITIES.has(args2.severity)) {
|
|
1143
|
+
query = query.eq("severity", args2.severity);
|
|
1144
|
+
}
|
|
1145
|
+
if (args2.status) {
|
|
1146
|
+
query = query.eq("status", args2.status);
|
|
1147
|
+
}
|
|
1148
|
+
const { data: findings, error } = await query;
|
|
1149
|
+
if (error) throw new Error(`Failed to query findings: ${error.message}`);
|
|
1150
|
+
if (!findings || findings.length === 0) {
|
|
1151
|
+
return { content: [{ type: "text", text: `No findings found for repo "${repoSlug}"` }] };
|
|
1152
|
+
}
|
|
1153
|
+
const summary = findings.map(
|
|
1154
|
+
(f) => `[${f.severity}] ${f.type}: ${String(f.description).slice(0, 120)}${f.file_path ? ` (${f.file_path})` : ""} \u2014 ${f.status}`
|
|
1155
|
+
).join("\n");
|
|
1156
|
+
return {
|
|
1157
|
+
content: [{
|
|
1158
|
+
type: "text",
|
|
1159
|
+
text: `${findings.length} findings for "${repoSlug}":
|
|
1160
|
+
|
|
1161
|
+
${summary}`
|
|
1162
|
+
}]
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
// -----------------------------------------------------------------
|
|
1166
|
+
case "agent-get-documentation": {
|
|
1167
|
+
const repoSlug = sanitizeString(args2.repo_slug, 200);
|
|
1168
|
+
if (!repoSlug) throw new Error("repo_slug is required");
|
|
1169
|
+
const limit = clamp(Number(args2.limit) || 20, 1, 100);
|
|
1170
|
+
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);
|
|
1171
|
+
if (args2.scope && VALID_SCOPES.has(args2.scope)) {
|
|
1172
|
+
query = query.eq("scope", args2.scope);
|
|
1173
|
+
}
|
|
1174
|
+
if (args2.path) {
|
|
1175
|
+
query = query.eq("path", sanitizeString(args2.path, 500));
|
|
1176
|
+
}
|
|
1177
|
+
const { data: docs, error } = await query;
|
|
1178
|
+
if (error) throw new Error(`Failed to query documentation: ${error.message}`);
|
|
1179
|
+
if (!docs || docs.length === 0) {
|
|
1180
|
+
return { content: [{ type: "text", text: `No documentation found for repo "${repoSlug}"` }] };
|
|
1181
|
+
}
|
|
1182
|
+
const output = docs.map((d) => {
|
|
1183
|
+
const contentPreview = String(d.content || "").slice(0, 500);
|
|
1184
|
+
return [
|
|
1185
|
+
`## ${d.title} (${d.scope}/${d.path})`,
|
|
1186
|
+
`Status: ${d.review_status} | By: ${d.generated_by || "unknown"}`,
|
|
1187
|
+
`Updated: ${d.updated_at || d.created_at}`,
|
|
1188
|
+
"",
|
|
1189
|
+
contentPreview + (String(d.content || "").length > 500 ? "\n...(truncated)" : ""),
|
|
1190
|
+
""
|
|
1191
|
+
].join("\n");
|
|
1192
|
+
}).join("\n---\n\n");
|
|
1193
|
+
return {
|
|
1194
|
+
content: [{ type: "text", text: `${docs.length} docs for "${repoSlug}":
|
|
1195
|
+
|
|
1196
|
+
${output}` }]
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
// -----------------------------------------------------------------
|
|
416
1200
|
default:
|
|
417
|
-
return { content: [{ type: "text", text: `Unknown
|
|
1201
|
+
return { content: [{ type: "text", text: `Unknown agent tool: ${name}` }] };
|
|
418
1202
|
}
|
|
419
1203
|
}
|
|
420
1204
|
|
|
@@ -428,6 +1212,7 @@ var supabaseUrl = getArg("supabase-url") || process.env.SUPABASE_URL;
|
|
|
428
1212
|
var supabaseKey = getArg("supabase-key") || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
429
1213
|
var encryptionKey = getArg("encryption-key") || process.env.ENCRYPTION_KEY;
|
|
430
1214
|
var mijnhostApiKey = getArg("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
|
|
1215
|
+
var agentWorkspaceId = getArg("workspace-id") || process.env.AGENT_WORKSPACE_ID || null;
|
|
431
1216
|
if (!apiKey) {
|
|
432
1217
|
console.error("API key is required. Use --api-key=dk_xxx or set MG_DASHBOARD_API_KEY");
|
|
433
1218
|
process.exit(1);
|
|
@@ -484,16 +1269,14 @@ setInterval(() => {
|
|
|
484
1269
|
}, 5 * 60 * 1e3).unref();
|
|
485
1270
|
var MODULE_KEYS = [
|
|
486
1271
|
"users",
|
|
487
|
-
"emails",
|
|
488
|
-
"logs",
|
|
489
1272
|
"ssh_servers",
|
|
490
1273
|
"supabase",
|
|
491
1274
|
"wiki",
|
|
492
1275
|
"ci_cd",
|
|
493
1276
|
"source_control",
|
|
494
1277
|
"domains",
|
|
495
|
-
"
|
|
496
|
-
"
|
|
1278
|
+
"settings",
|
|
1279
|
+
"agent_reporting"
|
|
497
1280
|
];
|
|
498
1281
|
var FULL_PERMISSIONS = {
|
|
499
1282
|
modules: Object.fromEntries(MODULE_KEYS.map((k) => [k, true])),
|
|
@@ -532,28 +1315,18 @@ function intersectServerAccess(keyServerIds, permissionServerIds) {
|
|
|
532
1315
|
}
|
|
533
1316
|
var TOOL_MODULE_MAP = {
|
|
534
1317
|
"list-servers": "ssh_servers",
|
|
535
|
-
"server-status": "ssh_servers",
|
|
536
1318
|
"ssh-execute": "ssh_servers",
|
|
537
|
-
"server-reboot": "ssh_servers",
|
|
538
|
-
"server-restart-service": "ssh_servers",
|
|
539
1319
|
"sftp-list": "ssh_servers",
|
|
540
1320
|
"sftp-read": "ssh_servers",
|
|
541
1321
|
"sftp-write": "ssh_servers",
|
|
542
1322
|
"sftp-delete": "ssh_servers",
|
|
543
1323
|
"docker-list": "ssh_servers",
|
|
544
|
-
"docker-action": "ssh_servers",
|
|
545
1324
|
"docker-logs": "ssh_servers",
|
|
546
1325
|
"db-discover": "ssh_servers",
|
|
547
1326
|
"db-tables": "ssh_servers",
|
|
548
1327
|
"db-describe": "ssh_servers",
|
|
549
1328
|
"db-query": "ssh_servers",
|
|
550
1329
|
"cache-purge": "ssh_servers",
|
|
551
|
-
"log-list": "ssh_servers",
|
|
552
|
-
"log-read": "ssh_servers",
|
|
553
|
-
"cron-list": "ssh_servers",
|
|
554
|
-
"cron-add": "ssh_servers",
|
|
555
|
-
"cron-remove": "ssh_servers",
|
|
556
|
-
"cron-toggle": "ssh_servers",
|
|
557
1330
|
"env-list": "ci_cd",
|
|
558
1331
|
"env-get": "ci_cd",
|
|
559
1332
|
"env-store": "ci_cd",
|
|
@@ -563,7 +1336,8 @@ var TOOL_MODULE_MAP = {
|
|
|
563
1336
|
"dns-create": "domains",
|
|
564
1337
|
"dns-update": "domains",
|
|
565
1338
|
"dns-delete": "domains",
|
|
566
|
-
...TRIGGER_TOOL_MODULE_MAP
|
|
1339
|
+
...TRIGGER_TOOL_MODULE_MAP,
|
|
1340
|
+
...AGENT_TOOL_MODULE_MAP
|
|
567
1341
|
};
|
|
568
1342
|
var authContext = null;
|
|
569
1343
|
async function validateApiKey(key) {
|
|
@@ -705,6 +1479,11 @@ function stageToVercelTargets(stageType, customEnvId) {
|
|
|
705
1479
|
if (customEnvId) return { customEnvironmentIds: [customEnvId] };
|
|
706
1480
|
return { target: ["preview"] };
|
|
707
1481
|
}
|
|
1482
|
+
function getVercelEnvSyncTargetings(stageType, customEnvId) {
|
|
1483
|
+
const primary = stageToVercelTargets(stageType, customEnvId);
|
|
1484
|
+
if (stageType !== "dev") return [primary];
|
|
1485
|
+
return [primary, { target: ["development"] }];
|
|
1486
|
+
}
|
|
708
1487
|
async function syncEnvVarsToVercel(token, projectId, envVars) {
|
|
709
1488
|
if (envVars.length === 0) return { created: 0, error: null };
|
|
710
1489
|
const res = await fetch(
|
|
@@ -780,18 +1559,24 @@ async function attemptVercelSync(appName, environment, knownStageId) {
|
|
|
780
1559
|
syncResults.push(`${app.label}: empty config`);
|
|
781
1560
|
continue;
|
|
782
1561
|
}
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1562
|
+
const targetings = getVercelEnvSyncTargetings(stageType, app.vercelCustomEnvId);
|
|
1563
|
+
let createdTotal = 0;
|
|
1564
|
+
let lastErr = null;
|
|
1565
|
+
for (const targeting of targetings) {
|
|
1566
|
+
const envVars = keys.map((key) => ({
|
|
1567
|
+
key,
|
|
1568
|
+
value: pairs[key],
|
|
1569
|
+
type: "plain",
|
|
1570
|
+
...targeting
|
|
1571
|
+
}));
|
|
1572
|
+
const { created, error } = await syncEnvVarsToVercel(token, app.vercelProjectId, envVars);
|
|
1573
|
+
if (error) lastErr = error;
|
|
1574
|
+
else createdTotal += created;
|
|
1575
|
+
}
|
|
1576
|
+
if (lastErr) {
|
|
1577
|
+
syncResults.push(`${app.label}: FAILED - ${lastErr}`);
|
|
793
1578
|
} else {
|
|
794
|
-
syncResults.push(`${app.label}: ${
|
|
1579
|
+
syncResults.push(`${app.label}: ${createdTotal} var upsert(s) synced`);
|
|
795
1580
|
}
|
|
796
1581
|
}
|
|
797
1582
|
return `Vercel sync: ${syncResults.join("; ")}`;
|
|
@@ -1260,16 +2045,6 @@ function assertSafeCommand(command) {
|
|
|
1260
2045
|
}
|
|
1261
2046
|
}
|
|
1262
2047
|
}
|
|
1263
|
-
var ALLOWED_LOG_PREFIXES = ["/var/log/", "/usr/local/lsws/logs/", "/var/www/"];
|
|
1264
|
-
function assertAllowedLogPath(filePath) {
|
|
1265
|
-
const safe = sanitizePath(filePath);
|
|
1266
|
-
if (!safe.endsWith(".log")) {
|
|
1267
|
-
throw new Error("Only .log files can be read with log-read");
|
|
1268
|
-
}
|
|
1269
|
-
if (!ALLOWED_LOG_PREFIXES.some((prefix) => safe.startsWith(prefix))) {
|
|
1270
|
-
throw new Error(`Path not allowed. Must be under: ${ALLOWED_LOG_PREFIXES.join(", ")}`);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
2048
|
async function discoverSiteDatabases(conn, proxy) {
|
|
1274
2049
|
const script = `
|
|
1275
2050
|
check_dir() {
|
|
@@ -1435,17 +2210,6 @@ var TOOLS = [
|
|
|
1435
2210
|
description: "List all SSH servers you have access to. Returns id, name, hostname, and tags for each server.",
|
|
1436
2211
|
inputSchema: { type: "object", properties: {}, required: [] }
|
|
1437
2212
|
},
|
|
1438
|
-
{
|
|
1439
|
-
name: "server-status",
|
|
1440
|
-
description: "Get server status including uptime, disk usage, memory, and load average.",
|
|
1441
|
-
inputSchema: {
|
|
1442
|
-
type: "object",
|
|
1443
|
-
properties: {
|
|
1444
|
-
serverId: { type: "string", description: "UUID of the SSH server" }
|
|
1445
|
-
},
|
|
1446
|
-
required: ["serverId"]
|
|
1447
|
-
}
|
|
1448
|
-
},
|
|
1449
2213
|
{
|
|
1450
2214
|
name: "ssh-execute",
|
|
1451
2215
|
description: "Execute a shell command on a remote server via SSH. Some dangerous commands are blocked for safety.",
|
|
@@ -1459,29 +2223,6 @@ var TOOLS = [
|
|
|
1459
2223
|
required: ["serverId", "command"]
|
|
1460
2224
|
}
|
|
1461
2225
|
},
|
|
1462
|
-
{
|
|
1463
|
-
name: "server-reboot",
|
|
1464
|
-
description: "Reboot a remote server. This will cause downtime. The server will be unavailable until it restarts.",
|
|
1465
|
-
inputSchema: {
|
|
1466
|
-
type: "object",
|
|
1467
|
-
properties: {
|
|
1468
|
-
serverId: { type: "string", description: "UUID of the SSH server to reboot" }
|
|
1469
|
-
},
|
|
1470
|
-
required: ["serverId"]
|
|
1471
|
-
}
|
|
1472
|
-
},
|
|
1473
|
-
{
|
|
1474
|
-
name: "server-restart-service",
|
|
1475
|
-
description: "Restart a systemd service on a remote server using systemctl restart.",
|
|
1476
|
-
inputSchema: {
|
|
1477
|
-
type: "object",
|
|
1478
|
-
properties: {
|
|
1479
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
1480
|
-
serviceName: { type: "string", description: "Name of the systemd service (e.g. nginx, docker, lsws)" }
|
|
1481
|
-
},
|
|
1482
|
-
required: ["serverId", "serviceName"]
|
|
1483
|
-
}
|
|
1484
|
-
},
|
|
1485
2226
|
{
|
|
1486
2227
|
name: "sftp-list",
|
|
1487
2228
|
description: "List files and directories at a given path on a remote server via SFTP.",
|
|
@@ -1542,19 +2283,6 @@ var TOOLS = [
|
|
|
1542
2283
|
required: ["serverId"]
|
|
1543
2284
|
}
|
|
1544
2285
|
},
|
|
1545
|
-
{
|
|
1546
|
-
name: "docker-action",
|
|
1547
|
-
description: "Perform an action on a Docker container: start, stop, restart, or remove.",
|
|
1548
|
-
inputSchema: {
|
|
1549
|
-
type: "object",
|
|
1550
|
-
properties: {
|
|
1551
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
1552
|
-
containerName: { type: "string", description: "Container name or ID" },
|
|
1553
|
-
action: { type: "string", enum: ["start", "stop", "restart", "remove"], description: "Action to perform" }
|
|
1554
|
-
},
|
|
1555
|
-
required: ["serverId", "containerName", "action"]
|
|
1556
|
-
}
|
|
1557
|
-
},
|
|
1558
2286
|
{
|
|
1559
2287
|
name: "docker-logs",
|
|
1560
2288
|
description: "Get recent logs from a Docker container.",
|
|
@@ -1666,86 +2394,6 @@ var TOOLS = [
|
|
|
1666
2394
|
required: ["serverId"]
|
|
1667
2395
|
}
|
|
1668
2396
|
},
|
|
1669
|
-
{
|
|
1670
|
-
name: "log-list",
|
|
1671
|
-
description: "Discover available log files on a server. Scans LiteSpeed logs, PHP error logs, syslog, and per-site application logs (WordPress debug.log, PrestaShop var/logs, Laravel storage/logs). Returns paths with file sizes and last modified dates.",
|
|
1672
|
-
inputSchema: {
|
|
1673
|
-
type: "object",
|
|
1674
|
-
properties: {
|
|
1675
|
-
serverId: { type: "string", description: "UUID of the SSH server" }
|
|
1676
|
-
},
|
|
1677
|
-
required: ["serverId"]
|
|
1678
|
-
}
|
|
1679
|
-
},
|
|
1680
|
-
{
|
|
1681
|
-
name: "log-read",
|
|
1682
|
-
description: "Read the last N lines from a specific log file on a server. Use log-list first to discover available files. Optionally filter lines with a grep pattern.",
|
|
1683
|
-
inputSchema: {
|
|
1684
|
-
type: "object",
|
|
1685
|
-
properties: {
|
|
1686
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
1687
|
-
path: { type: "string", description: "Absolute path to the log file (must end in .log)" },
|
|
1688
|
-
lines: { type: "number", description: "Number of lines to read (default: 100, max: 500)" },
|
|
1689
|
-
filter: { type: "string", description: "Optional grep pattern to filter lines before tailing" }
|
|
1690
|
-
},
|
|
1691
|
-
required: ["serverId", "path"]
|
|
1692
|
-
}
|
|
1693
|
-
},
|
|
1694
|
-
// ----- Cron Jobs -----
|
|
1695
|
-
{
|
|
1696
|
-
name: "cron-list",
|
|
1697
|
-
description: "List all cron jobs on a server. Shows root crontab, system cron directories (/etc/cron.d/), and www-data crontab. Each entry shows the schedule, command, user, and source. Disabled (commented) entries are marked.",
|
|
1698
|
-
inputSchema: {
|
|
1699
|
-
type: "object",
|
|
1700
|
-
properties: {
|
|
1701
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
1702
|
-
user: { type: "string", description: "Specific user crontab to list (default: lists root + www-data + /etc/cron.d/)" }
|
|
1703
|
-
},
|
|
1704
|
-
required: ["serverId"]
|
|
1705
|
-
}
|
|
1706
|
-
},
|
|
1707
|
-
{
|
|
1708
|
-
name: "cron-add",
|
|
1709
|
-
description: 'Add a new cron job to a user crontab. Provide a standard cron schedule expression and command. Examples: "0 2 * * * /usr/bin/backup.sh" (daily at 2am), "*/5 * * * * curl http://example.com/cron.php" (every 5 min).',
|
|
1710
|
-
inputSchema: {
|
|
1711
|
-
type: "object",
|
|
1712
|
-
properties: {
|
|
1713
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
1714
|
-
schedule: { type: "string", description: 'Cron schedule expression (e.g. "0 2 * * *" or "@daily")' },
|
|
1715
|
-
command: { type: "string", description: "Command to execute" },
|
|
1716
|
-
user: { type: "string", description: "User whose crontab to edit (default: root)" },
|
|
1717
|
-
comment: { type: "string", description: "Optional comment to add above the cron entry (for identification)" }
|
|
1718
|
-
},
|
|
1719
|
-
required: ["serverId", "schedule", "command"]
|
|
1720
|
-
}
|
|
1721
|
-
},
|
|
1722
|
-
{
|
|
1723
|
-
name: "cron-remove",
|
|
1724
|
-
description: "Remove a cron job from a user crontab by matching the command string (exact or partial match). Use cron-list first to see existing jobs.",
|
|
1725
|
-
inputSchema: {
|
|
1726
|
-
type: "object",
|
|
1727
|
-
properties: {
|
|
1728
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
1729
|
-
commandMatch: { type: "string", description: "Full or partial command string to match for removal" },
|
|
1730
|
-
user: { type: "string", description: "User whose crontab to edit (default: root)" }
|
|
1731
|
-
},
|
|
1732
|
-
required: ["serverId", "commandMatch"]
|
|
1733
|
-
}
|
|
1734
|
-
},
|
|
1735
|
-
{
|
|
1736
|
-
name: "cron-toggle",
|
|
1737
|
-
description: "Enable or disable a cron job by commenting/uncommenting it. Matches by command string. Use cron-list to find the job first.",
|
|
1738
|
-
inputSchema: {
|
|
1739
|
-
type: "object",
|
|
1740
|
-
properties: {
|
|
1741
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
1742
|
-
commandMatch: { type: "string", description: "Full or partial command string to match" },
|
|
1743
|
-
enable: { type: "boolean", description: "true to enable (uncomment), false to disable (comment out)" },
|
|
1744
|
-
user: { type: "string", description: "User whose crontab to edit (default: root)" }
|
|
1745
|
-
},
|
|
1746
|
-
required: ["serverId", "commandMatch", "enable"]
|
|
1747
|
-
}
|
|
1748
|
-
},
|
|
1749
2397
|
// ----- Domains (mijn.host) -----
|
|
1750
2398
|
{
|
|
1751
2399
|
name: "domain-list",
|
|
@@ -1820,7 +2468,11 @@ var TOOLS = [
|
|
|
1820
2468
|
}
|
|
1821
2469
|
},
|
|
1822
2470
|
// ----- Trigger.dev -----
|
|
1823
|
-
...TRIGGER_TOOLS
|
|
2471
|
+
...TRIGGER_TOOLS,
|
|
2472
|
+
// ----- Scripts -----
|
|
2473
|
+
...SCRIPT_TOOLS,
|
|
2474
|
+
// ----- Agent Reporting -----
|
|
2475
|
+
...AGENT_TOOLS
|
|
1824
2476
|
];
|
|
1825
2477
|
var server = new Server(
|
|
1826
2478
|
{ name: "mg-dashboard-mcp", version: "2.2.0" },
|
|
@@ -1866,15 +2518,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1866
2518
|
});
|
|
1867
2519
|
return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No servers found" }] };
|
|
1868
2520
|
}
|
|
1869
|
-
case "server-status": {
|
|
1870
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1871
|
-
const cmd = 'echo "=== UPTIME ===" && uptime && echo "=== DISK ===" && df -h --total && echo "=== MEMORY ===" && free -h && echo "=== LOAD ===" && cat /proc/loadavg';
|
|
1872
|
-
const result = await sshExec(conn, cmd, proxy);
|
|
1873
|
-
const output = result.exitCode === 0 ? result.stdout : `Exit ${result.exitCode}
|
|
1874
|
-
${result.stdout}
|
|
1875
|
-
${result.stderr}`;
|
|
1876
|
-
return { content: [{ type: "text", text: output }] };
|
|
1877
|
-
}
|
|
1878
2521
|
// ----- SSH -----
|
|
1879
2522
|
case "ssh-execute": {
|
|
1880
2523
|
const command = String(a.command);
|
|
@@ -1889,21 +2532,6 @@ ${result.stdout}`);
|
|
|
1889
2532
|
${result.stderr}`);
|
|
1890
2533
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
1891
2534
|
}
|
|
1892
|
-
case "server-reboot": {
|
|
1893
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1894
|
-
const result = await sshExec(conn, "sudo reboot", proxy);
|
|
1895
|
-
return { content: [{ type: "text", text: result.exitCode === 0 ? "Reboot command sent. Server will be unavailable shortly." : `Reboot failed: ${result.stderr}` }] };
|
|
1896
|
-
}
|
|
1897
|
-
case "server-restart-service": {
|
|
1898
|
-
const service = String(a.serviceName).replace(/[^a-zA-Z0-9._@-]/g, "");
|
|
1899
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1900
|
-
const result = await sshExec(conn, `sudo systemctl restart ${service}`, proxy);
|
|
1901
|
-
if (result.exitCode === 0) {
|
|
1902
|
-
const status = await sshExec(conn, `sudo systemctl is-active ${service}`, proxy);
|
|
1903
|
-
return { content: [{ type: "text", text: `Service "${service}" restarted. Status: ${status.stdout.trim()}` }] };
|
|
1904
|
-
}
|
|
1905
|
-
return { content: [{ type: "text", text: `Failed to restart "${service}": ${result.stderr}` }] };
|
|
1906
|
-
}
|
|
1907
2535
|
// ----- SFTP (no proxy support yet — direct connection only) -----
|
|
1908
2536
|
case "sftp-list": {
|
|
1909
2537
|
const { conn } = await getServerConnection(String(a.serverId));
|
|
@@ -1931,17 +2559,6 @@ ${result.stderr}`);
|
|
|
1931
2559
|
const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"', proxy);
|
|
1932
2560
|
return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
|
|
1933
2561
|
}
|
|
1934
|
-
case "docker-action": {
|
|
1935
|
-
const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
|
|
1936
|
-
const action = String(a.action);
|
|
1937
|
-
if (!["start", "stop", "restart", "remove"].includes(action)) {
|
|
1938
|
-
throw new Error(`Invalid action: ${action}. Use start, stop, restart, or remove.`);
|
|
1939
|
-
}
|
|
1940
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1941
|
-
const dockerCmd = action === "remove" ? `docker rm -f ${container}` : `docker ${action} ${container}`;
|
|
1942
|
-
const result = await sshExec(conn, dockerCmd, proxy);
|
|
1943
|
-
return { content: [{ type: "text", text: result.exitCode === 0 ? `Container "${container}" ${action}ed successfully` : `Error: ${result.stderr}` }] };
|
|
1944
|
-
}
|
|
1945
2562
|
case "docker-logs": {
|
|
1946
2563
|
const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
|
|
1947
2564
|
const lines = Number(a.lines) || 100;
|
|
@@ -2165,157 +2782,6 @@ echo -e "$R"
|
|
|
2165
2782
|
const output = (result.stdout || "").trim();
|
|
2166
2783
|
return { content: [{ type: "text", text: output || "Cache purge completed (no output)" }] };
|
|
2167
2784
|
}
|
|
2168
|
-
// ----- Log Reading -----
|
|
2169
|
-
case "log-list": {
|
|
2170
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2171
|
-
const script = `
|
|
2172
|
-
{
|
|
2173
|
-
[ -d /usr/local/lsws/logs ] && find /usr/local/lsws/logs -maxdepth 2 -name "*.log" -type f 2>/dev/null
|
|
2174
|
-
for f in /var/log/syslog /var/log/messages /var/log/auth.log /var/log/kern.log; do [ -f "$f" ] && echo "$f"; done
|
|
2175
|
-
find /var/log -maxdepth 2 \\( -name "php*.log" -o -name "lsphp*.log" \\) -type f 2>/dev/null
|
|
2176
|
-
[ -d /var/log/mg-monitoring ] && find /var/log/mg-monitoring -maxdepth 1 -name "*.log" -type f 2>/dev/null
|
|
2177
|
-
for dir in /var/www/*/; do
|
|
2178
|
-
[ -d "$dir" ] || continue
|
|
2179
|
-
for root in "$dir" "\${dir}html" "\${dir}public_html" "\${dir}public" "\${dir}httpdocs"; do
|
|
2180
|
-
[ -d "$root" ] || continue
|
|
2181
|
-
[ -f "$root/wp-content/debug.log" ] && echo "$root/wp-content/debug.log"
|
|
2182
|
-
[ -d "$root/var/logs" ] && find "$root/var/logs" -maxdepth 1 -name "*.log" -type f 2>/dev/null
|
|
2183
|
-
[ -d "$root/log" ] && find "$root/log" -maxdepth 1 -name "*.log" -type f 2>/dev/null
|
|
2184
|
-
[ -d "$root/storage/logs" ] && find "$root/storage/logs" -maxdepth 1 -name "*.log" -type f 2>/dev/null
|
|
2185
|
-
done
|
|
2186
|
-
done
|
|
2187
|
-
} | sort -u | while IFS= read -r f; do
|
|
2188
|
-
SZ=$(stat -c%s "$f" 2>/dev/null || echo 0)
|
|
2189
|
-
MOD=$(stat -c%y "$f" 2>/dev/null | cut -d. -f1)
|
|
2190
|
-
if [ "$SZ" -ge 1048576 ] 2>/dev/null; then HR="$(( SZ / 1048576 ))MB"
|
|
2191
|
-
elif [ "$SZ" -ge 1024 ] 2>/dev/null; then HR="$(( SZ / 1024 ))KB"
|
|
2192
|
-
else HR="\${SZ}B"; fi
|
|
2193
|
-
echo "$f $HR $MOD"
|
|
2194
|
-
done
|
|
2195
|
-
`.trim();
|
|
2196
|
-
const result = await sshExec(conn, script, proxy);
|
|
2197
|
-
return { content: [{ type: "text", text: result.stdout || "No log files found" }] };
|
|
2198
|
-
}
|
|
2199
|
-
case "log-read": {
|
|
2200
|
-
const logPath = sanitizePath(String(a.path));
|
|
2201
|
-
assertAllowedLogPath(logPath);
|
|
2202
|
-
const lineCount = Math.min(Math.max(Number(a.lines) || 100, 1), 500);
|
|
2203
|
-
const filter = a.filter ? String(a.filter).replace(/'/g, "'\\''") : "";
|
|
2204
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2205
|
-
const cmd = filter ? `grep '${filter}' '${logPath}' 2>/dev/null | tail -n ${lineCount}` : `tail -n ${lineCount} '${logPath}' 2>/dev/null`;
|
|
2206
|
-
const result = await sshExec(conn, cmd, proxy);
|
|
2207
|
-
if (result.exitCode !== 0 && !result.stdout) {
|
|
2208
|
-
throw new Error(result.stderr || `Failed to read log: ${logPath}`);
|
|
2209
|
-
}
|
|
2210
|
-
return { content: [{ type: "text", text: result.stdout || "(empty log file)" }] };
|
|
2211
|
-
}
|
|
2212
|
-
// ----- Cron Jobs -----
|
|
2213
|
-
case "cron-list": {
|
|
2214
|
-
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
2215
|
-
const user = a.user ? String(a.user) : null;
|
|
2216
|
-
const parts = [];
|
|
2217
|
-
if (user) {
|
|
2218
|
-
const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'`, proxy);
|
|
2219
|
-
parts.push(`## Crontab for ${user}
|
|
2220
|
-
${result.stdout}`);
|
|
2221
|
-
} else {
|
|
2222
|
-
const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'`, proxy);
|
|
2223
|
-
parts.push(`## Root crontab
|
|
2224
|
-
${rootCron.stdout}`);
|
|
2225
|
-
const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'`, proxy);
|
|
2226
|
-
parts.push(`## www-data crontab
|
|
2227
|
-
${wwwCron.stdout}`);
|
|
2228
|
-
const cronD = await sshExec(conn, `for f in /etc/cron.d/*; do [ -f "$f" ] && echo "--- $f ---" && cat "$f" && echo; done 2>/dev/null || echo '(no files in /etc/cron.d/)'`, proxy);
|
|
2229
|
-
parts.push(`## /etc/cron.d/
|
|
2230
|
-
${cronD.stdout}`);
|
|
2231
|
-
const summary = await sshExec(conn, [
|
|
2232
|
-
`echo "## Scheduled directories"`,
|
|
2233
|
-
`echo "cron.hourly: $(ls /etc/cron.hourly/ 2>/dev/null | wc -l) scripts"`,
|
|
2234
|
-
`echo "cron.daily: $(ls /etc/cron.daily/ 2>/dev/null | wc -l) scripts"`,
|
|
2235
|
-
`echo "cron.weekly: $(ls /etc/cron.weekly/ 2>/dev/null | wc -l) scripts"`,
|
|
2236
|
-
`echo "cron.monthly: $(ls /etc/cron.monthly/ 2>/dev/null | wc -l) scripts"`
|
|
2237
|
-
].join(" && "), proxy);
|
|
2238
|
-
parts.push(summary.stdout);
|
|
2239
|
-
}
|
|
2240
|
-
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
2241
|
-
}
|
|
2242
|
-
case "cron-add": {
|
|
2243
|
-
if (!a.schedule || !a.command) {
|
|
2244
|
-
throw new Error("Both schedule and command are required");
|
|
2245
|
-
}
|
|
2246
|
-
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
2247
|
-
const user = a.user ? String(a.user) : "root";
|
|
2248
|
-
const schedule = String(a.schedule).trim();
|
|
2249
|
-
const command = String(a.command).trim();
|
|
2250
|
-
const commentText = a.comment ? String(a.comment).trim() : "";
|
|
2251
|
-
const entry = commentText ? `# ${commentText}
|
|
2252
|
-
${schedule} ${command}` : `${schedule} ${command}`;
|
|
2253
|
-
const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`, proxy);
|
|
2254
|
-
if (result.exitCode !== 0) {
|
|
2255
|
-
throw new Error(result.stderr || "Failed to add cron job");
|
|
2256
|
-
}
|
|
2257
|
-
return { content: [{ type: "text", text: `Cron job added for ${user}:
|
|
2258
|
-
${schedule} ${command}` }] };
|
|
2259
|
-
}
|
|
2260
|
-
case "cron-remove": {
|
|
2261
|
-
if (!a.commandMatch) {
|
|
2262
|
-
throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
|
|
2263
|
-
}
|
|
2264
|
-
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
2265
|
-
const user = a.user ? String(a.user) : "root";
|
|
2266
|
-
const match = String(a.commandMatch).trim();
|
|
2267
|
-
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
2268
|
-
const lines = current.stdout.split("\n");
|
|
2269
|
-
const before = lines.length;
|
|
2270
|
-
const filtered = lines.filter((line) => {
|
|
2271
|
-
const trimmed = line.replace(/^#\s*/, "");
|
|
2272
|
-
return !trimmed.includes(match);
|
|
2273
|
-
});
|
|
2274
|
-
const removed = before - filtered.length;
|
|
2275
|
-
if (removed === 0) {
|
|
2276
|
-
return { content: [{ type: "text", text: `No cron entries found matching "${match}"` }] };
|
|
2277
|
-
}
|
|
2278
|
-
const escapedCrontab = filtered.join("\n").replace(/'/g, "'\\''");
|
|
2279
|
-
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
|
|
2280
|
-
if (result.exitCode !== 0) {
|
|
2281
|
-
throw new Error(result.stderr || "Failed to update crontab");
|
|
2282
|
-
}
|
|
2283
|
-
return { content: [{ type: "text", text: `Removed ${removed} cron entry/entries matching "${match}" from ${user} crontab` }] };
|
|
2284
|
-
}
|
|
2285
|
-
case "cron-toggle": {
|
|
2286
|
-
if (!a.commandMatch) {
|
|
2287
|
-
throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
|
|
2288
|
-
}
|
|
2289
|
-
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
2290
|
-
const user = a.user ? String(a.user) : "root";
|
|
2291
|
-
const match = String(a.commandMatch).trim();
|
|
2292
|
-
const enable = Boolean(a.enable);
|
|
2293
|
-
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
2294
|
-
const lines = current.stdout.split("\n");
|
|
2295
|
-
let toggled = 0;
|
|
2296
|
-
const updated = lines.map((line) => {
|
|
2297
|
-
if (enable && line.startsWith("#") && line.replace(/^#\s*/, "").includes(match)) {
|
|
2298
|
-
toggled++;
|
|
2299
|
-
return line.replace(/^#\s*/, "");
|
|
2300
|
-
}
|
|
2301
|
-
if (!enable && !line.startsWith("#") && line.includes(match)) {
|
|
2302
|
-
toggled++;
|
|
2303
|
-
return `# ${line}`;
|
|
2304
|
-
}
|
|
2305
|
-
return line;
|
|
2306
|
-
});
|
|
2307
|
-
if (toggled === 0) {
|
|
2308
|
-
const state = enable ? "disabled" : "enabled";
|
|
2309
|
-
return { content: [{ type: "text", text: `No ${state} cron entries found matching "${match}"` }] };
|
|
2310
|
-
}
|
|
2311
|
-
const escapedCrontab = updated.join("\n").replace(/'/g, "'\\''");
|
|
2312
|
-
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
|
|
2313
|
-
if (result.exitCode !== 0) {
|
|
2314
|
-
throw new Error(result.stderr || "Failed to update crontab");
|
|
2315
|
-
}
|
|
2316
|
-
const action = enable ? "Enabled" : "Disabled";
|
|
2317
|
-
return { content: [{ type: "text", text: `${action} ${toggled} cron entry/entries matching "${match}" in ${user} crontab` }] };
|
|
2318
|
-
}
|
|
2319
2785
|
// ----- Domains (mijn.host) -----
|
|
2320
2786
|
case "domain-list": {
|
|
2321
2787
|
const res = await mijnhostFetch("/domains");
|
|
@@ -2457,6 +2923,12 @@ ${lines.join("\n")}` }] };
|
|
|
2457
2923
|
if (TRIGGER_TOOL_NAMES.has(name)) {
|
|
2458
2924
|
return handleTriggerTool(name, a, { sshExec, getServerConnection });
|
|
2459
2925
|
}
|
|
2926
|
+
if (SCRIPT_TOOL_NAMES.has(name)) {
|
|
2927
|
+
return handleScriptTool(name, a);
|
|
2928
|
+
}
|
|
2929
|
+
if (AGENT_TOOL_NAMES.has(name)) {
|
|
2930
|
+
return handleAgentTool(name, a, { supabase, workspaceId: agentWorkspaceId });
|
|
2931
|
+
}
|
|
2460
2932
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
2461
2933
|
}
|
|
2462
2934
|
} catch (err) {
|