nodebench-mcp 2.21.1 → 2.25.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/README.md +366 -280
- package/dist/__tests__/dynamicLoading.test.js +4 -2
- package/dist/__tests__/dynamicLoading.test.js.map +1 -1
- package/dist/__tests__/multiHopDogfood.test.d.ts +12 -0
- package/dist/__tests__/multiHopDogfood.test.js +303 -0
- package/dist/__tests__/multiHopDogfood.test.js.map +1 -0
- package/dist/__tests__/presetRealWorldBench.test.js +2 -0
- package/dist/__tests__/presetRealWorldBench.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +158 -6
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/__tests__/toolsetGatingEval.test.js +2 -0
- package/dist/__tests__/toolsetGatingEval.test.js.map +1 -1
- package/dist/dashboard/html.d.ts +18 -0
- package/dist/dashboard/html.js +1251 -0
- package/dist/dashboard/html.js.map +1 -0
- package/dist/dashboard/server.d.ts +17 -0
- package/dist/dashboard/server.js +278 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/db.js +108 -0
- package/dist/db.js.map +1 -1
- package/dist/index.js +19 -9
- package/dist/index.js.map +1 -1
- package/dist/tools/prReportTools.d.ts +11 -0
- package/dist/tools/prReportTools.js +911 -0
- package/dist/tools/prReportTools.js.map +1 -0
- package/dist/tools/progressiveDiscoveryTools.js +111 -24
- package/dist/tools/progressiveDiscoveryTools.js.map +1 -1
- package/dist/tools/skillUpdateTools.d.ts +24 -0
- package/dist/tools/skillUpdateTools.js +469 -0
- package/dist/tools/skillUpdateTools.js.map +1 -0
- package/dist/tools/toolRegistry.d.ts +15 -1
- package/dist/tools/toolRegistry.js +379 -11
- package/dist/tools/toolRegistry.js.map +1 -1
- package/dist/tools/uiUxDiveAdvancedTools.js +688 -0
- package/dist/tools/uiUxDiveAdvancedTools.js.map +1 -1
- package/dist/tools/uiUxDiveTools.js +154 -1
- package/dist/tools/uiUxDiveTools.js.map +1 -1
- package/dist/toolsetRegistry.js +4 -0
- package/dist/toolsetRegistry.js.map +1 -1
- package/package.json +2 -2
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "node:fs";
|
|
20
20
|
import { join, basename } from "node:path";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { execSync } from "node:child_process";
|
|
22
23
|
import { createConnection } from "node:net";
|
|
23
24
|
import { getDb } from "../db.js";
|
|
25
|
+
import { getDashboardUrl } from "../dashboard/server.js";
|
|
24
26
|
function genId(prefix) {
|
|
25
27
|
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
26
28
|
}
|
|
@@ -879,5 +881,691 @@ export const uiUxDiveAdvancedTools = [
|
|
|
879
881
|
};
|
|
880
882
|
},
|
|
881
883
|
},
|
|
884
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
885
|
+
// v3 FLYWHEEL TOOLS — Bug→Code→Fix→Verify→Reexplore→Test→Review
|
|
886
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
887
|
+
// ── 8. Locate bug/component in codebase ────────────────────────────────
|
|
888
|
+
{
|
|
889
|
+
name: "dive_code_locate",
|
|
890
|
+
description: "Find the exact source code location for a bug, component, or design issue. Uses grep/ripgrep to search the project codebase for the relevant code. Maps UI bugs to file:line so you know exactly where to fix. Stores the location in the DB linked to the bug/component for the full traceability chain: UI element → bug → source file:line → fix → verify.",
|
|
891
|
+
inputSchema: {
|
|
892
|
+
type: "object",
|
|
893
|
+
properties: {
|
|
894
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
895
|
+
projectPath: { type: "string", description: "Absolute path to the project root" },
|
|
896
|
+
bugId: { type: "string", description: "Bug to locate code for (optional)" },
|
|
897
|
+
componentId: { type: "string", description: "Component to locate code for (optional)" },
|
|
898
|
+
designIssueId: { type: "string", description: "Design issue to locate (optional)" },
|
|
899
|
+
searchQueries: {
|
|
900
|
+
type: "array",
|
|
901
|
+
description: "Strings to grep for in the codebase (e.g. ['NaN%', 'tokenUsage', 'CostDashboard']). Multiple queries are tried in order; first match wins.",
|
|
902
|
+
items: { type: "string" },
|
|
903
|
+
},
|
|
904
|
+
filePatterns: {
|
|
905
|
+
type: "array",
|
|
906
|
+
description: "Glob patterns to limit search scope (e.g. ['*.tsx', '*.ts']). Default: ['*.tsx', '*.ts', '*.jsx', '*.js']",
|
|
907
|
+
items: { type: "string" },
|
|
908
|
+
},
|
|
909
|
+
contextLines: { type: "number", description: "Lines of context around match (default: 3)" },
|
|
910
|
+
},
|
|
911
|
+
required: ["sessionId", "projectPath", "searchQueries"],
|
|
912
|
+
},
|
|
913
|
+
handler: async (args) => {
|
|
914
|
+
const { sessionId, projectPath, bugId, componentId, designIssueId, searchQueries, filePatterns, contextLines, } = args;
|
|
915
|
+
const db = getDb();
|
|
916
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
917
|
+
if (!session)
|
|
918
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
919
|
+
if (!existsSync(projectPath))
|
|
920
|
+
return { error: true, message: `Project path not found: ${projectPath}` };
|
|
921
|
+
const exts = filePatterns ?? ["*.tsx", "*.ts", "*.jsx", "*.js"];
|
|
922
|
+
const ctx = contextLines ?? 3;
|
|
923
|
+
const locations = [];
|
|
924
|
+
for (const query of searchQueries) {
|
|
925
|
+
if (locations.length >= 10)
|
|
926
|
+
break; // cap results
|
|
927
|
+
// Try ripgrep first, fall back to findstr on Windows
|
|
928
|
+
const includeFlags = exts.map(e => `--include="${e}"`).join(" ");
|
|
929
|
+
const cmd = process.platform === "win32"
|
|
930
|
+
? `rg -n -C ${ctx} --no-heading ${includeFlags} "${query.replace(/"/g, '\\"')}" "${projectPath}" 2>nul || findstr /s /n /c:"${query.replace(/"/g, "")}" "${projectPath}\\src\\*.ts" "${projectPath}\\src\\*.tsx" 2>nul`
|
|
931
|
+
: `rg -n -C ${ctx} --no-heading ${includeFlags} "${query.replace(/"/g, '\\"')}" "${projectPath}" 2>/dev/null || grep -rnH --include='*.ts' --include='*.tsx' "${query}" "${projectPath}/src" 2>/dev/null`;
|
|
932
|
+
try {
|
|
933
|
+
const output = execSync(cmd, { encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: 15000 }).trim();
|
|
934
|
+
if (!output)
|
|
935
|
+
continue;
|
|
936
|
+
// Parse ripgrep output: filename:lineNum:content or filename-lineNum-content (context)
|
|
937
|
+
const fileMatches = new Map();
|
|
938
|
+
for (const line of output.split("\n").slice(0, 100)) {
|
|
939
|
+
const m = line.match(/^(.+?)[:\-](\d+)[:\-](.*)$/);
|
|
940
|
+
if (m) {
|
|
941
|
+
const [, file, lineStr, content] = m;
|
|
942
|
+
const lineNum = parseInt(lineStr, 10);
|
|
943
|
+
const normalized = file.replace(/\\/g, "/");
|
|
944
|
+
if (!fileMatches.has(normalized))
|
|
945
|
+
fileMatches.set(normalized, { lines: [], content: [] });
|
|
946
|
+
const entry = fileMatches.get(normalized);
|
|
947
|
+
entry.lines.push(lineNum);
|
|
948
|
+
entry.content.push(`${lineNum}: ${content}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
for (const [file, { lines: lineNums, content }] of fileMatches) {
|
|
952
|
+
if (locations.length >= 10)
|
|
953
|
+
break;
|
|
954
|
+
const lineStart = Math.min(...lineNums);
|
|
955
|
+
const lineEnd = Math.max(...lineNums);
|
|
956
|
+
const snippet = content.slice(0, 15).join("\n");
|
|
957
|
+
locations.push({
|
|
958
|
+
file,
|
|
959
|
+
lineStart,
|
|
960
|
+
lineEnd,
|
|
961
|
+
snippet,
|
|
962
|
+
query,
|
|
963
|
+
confidence: "high",
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
// grep/rg failed or timed out — try next query
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Store in DB
|
|
973
|
+
const storedIds = [];
|
|
974
|
+
for (const loc of locations) {
|
|
975
|
+
const id = genId("cloc");
|
|
976
|
+
db.prepare(`INSERT INTO ui_dive_code_locations (id, session_id, bug_id, component_id, design_issue_id, file_path, line_start, line_end, code_snippet, search_query, confidence)
|
|
977
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, bugId ?? null, componentId ?? null, designIssueId ?? null, loc.file, loc.lineStart, loc.lineEnd, loc.snippet, loc.query, loc.confidence);
|
|
978
|
+
storedIds.push(id);
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
locationsFound: locations.length,
|
|
982
|
+
locations: locations.map((l, i) => ({
|
|
983
|
+
id: storedIds[i],
|
|
984
|
+
file: l.file,
|
|
985
|
+
lines: `${l.lineStart}-${l.lineEnd}`,
|
|
986
|
+
query: l.query,
|
|
987
|
+
confidence: l.confidence,
|
|
988
|
+
snippet: l.snippet.slice(0, 500),
|
|
989
|
+
})),
|
|
990
|
+
linkedTo: { bugId: bugId ?? null, componentId: componentId ?? null, designIssueId: designIssueId ?? null },
|
|
991
|
+
_hint: locations.length > 0
|
|
992
|
+
? `Found ${locations.length} code location(s). Fix the code, then verify with dive_fix_verify({ bugId, route, fixDescription }).`
|
|
993
|
+
: `No matches found. Try different search queries or broader file patterns.`,
|
|
994
|
+
_workflow: [
|
|
995
|
+
"1. Review the code snippets above to understand the root cause",
|
|
996
|
+
"2. Fix the code in your editor",
|
|
997
|
+
"3. Verify: dive_fix_verify({ sessionId, bugId, route, fixDescription, filesChanged })",
|
|
998
|
+
"4. Generate regression test: dive_generate_tests({ sessionId, bugId })",
|
|
999
|
+
"5. Re-explore: dive_reexplore({ sessionId, route }) to check for regressions",
|
|
1000
|
+
],
|
|
1001
|
+
};
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
// ── 9. Fix + Verify flywheel ───────────────────────────────────────────
|
|
1005
|
+
{
|
|
1006
|
+
name: "dive_fix_verify",
|
|
1007
|
+
description: "After fixing a bug, verify the fix by re-navigating to the affected route, comparing before/after state, and updating the bug status + changelog. This is the core flywheel step: Bug tagged → Code located → Code fixed → Fix verified → Changelog updated → Bug marked resolved. The agent should navigate to the route via Playwright, take a new screenshot, and pass the results here.",
|
|
1008
|
+
inputSchema: {
|
|
1009
|
+
type: "object",
|
|
1010
|
+
properties: {
|
|
1011
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
1012
|
+
bugId: { type: "string", description: "Bug ID being fixed" },
|
|
1013
|
+
route: { type: "string", description: "Route to re-navigate to for verification" },
|
|
1014
|
+
fixDescription: { type: "string", description: "What was changed to fix the bug" },
|
|
1015
|
+
filesChanged: {
|
|
1016
|
+
type: "array",
|
|
1017
|
+
description: "Files that were modified",
|
|
1018
|
+
items: { type: "string" },
|
|
1019
|
+
},
|
|
1020
|
+
gitCommit: { type: "string", description: "Git commit hash for the fix (optional)" },
|
|
1021
|
+
beforeScreenshotId: { type: "string", description: "Screenshot ID from before the fix (optional)" },
|
|
1022
|
+
afterScreenshotId: { type: "string", description: "Screenshot ID from after the fix (optional)" },
|
|
1023
|
+
verified: {
|
|
1024
|
+
type: "boolean",
|
|
1025
|
+
description: "Whether the fix was visually/functionally verified (default: false until agent confirms)",
|
|
1026
|
+
},
|
|
1027
|
+
verificationNotes: { type: "string", description: "Notes from the verification (what the agent observed)" },
|
|
1028
|
+
},
|
|
1029
|
+
required: ["sessionId", "bugId", "fixDescription"],
|
|
1030
|
+
},
|
|
1031
|
+
handler: async (args) => {
|
|
1032
|
+
const { sessionId, bugId, route, fixDescription, filesChanged, gitCommit, beforeScreenshotId, afterScreenshotId, verified, verificationNotes, } = args;
|
|
1033
|
+
const db = getDb();
|
|
1034
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
1035
|
+
if (!session)
|
|
1036
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
1037
|
+
const bug = db.prepare("SELECT id, title, severity, component_id, status FROM ui_dive_bugs WHERE id = ?").get(bugId);
|
|
1038
|
+
if (!bug)
|
|
1039
|
+
return { error: true, message: `Bug not found: ${bugId}` };
|
|
1040
|
+
// Create fix verification record
|
|
1041
|
+
const verifyId = genId("fxv");
|
|
1042
|
+
db.prepare(`INSERT INTO ui_dive_fix_verifications (id, session_id, bug_id, route, before_screenshot_id, after_screenshot_id, fix_description, files_changed, git_commit, verified, verification_notes)
|
|
1043
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(verifyId, sessionId, bugId, route ?? null, beforeScreenshotId ?? null, afterScreenshotId ?? null, fixDescription, filesChanged ? JSON.stringify(filesChanged) : null, gitCommit ?? null, verified ? 1 : 0, verificationNotes ?? null);
|
|
1044
|
+
// Auto-create changelog entry
|
|
1045
|
+
const changelogId = genId("chg");
|
|
1046
|
+
db.prepare(`INSERT INTO ui_dive_changelogs (id, session_id, component_id, change_type, description, before_screenshot_id, after_screenshot_id, files_changed, git_commit, metadata)
|
|
1047
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(changelogId, sessionId, bug.component_id ?? null, "bugfix", `[${bug.severity}] ${bug.title}: ${fixDescription}`, beforeScreenshotId ?? null, afterScreenshotId ?? null, filesChanged ? JSON.stringify(filesChanged) : null, gitCommit ?? null, JSON.stringify({ bugId, verificationId: verifyId }));
|
|
1048
|
+
// Update fix verification with changelog link
|
|
1049
|
+
db.prepare("UPDATE ui_dive_fix_verifications SET changelog_id = ? WHERE id = ?").run(changelogId, verifyId);
|
|
1050
|
+
// If verified, update bug status
|
|
1051
|
+
if (verified) {
|
|
1052
|
+
db.prepare("UPDATE ui_dive_bugs SET status = 'resolved' WHERE id = ?").run(bugId);
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
verificationId: verifyId,
|
|
1056
|
+
bugId,
|
|
1057
|
+
bugTitle: bug.title,
|
|
1058
|
+
bugSeverity: bug.severity,
|
|
1059
|
+
verified: verified ?? false,
|
|
1060
|
+
changelogId,
|
|
1061
|
+
filesChanged: filesChanged ?? [],
|
|
1062
|
+
gitCommit: gitCommit ?? null,
|
|
1063
|
+
bugStatus: verified ? "resolved" : "pending_verification",
|
|
1064
|
+
_hint: verified
|
|
1065
|
+
? `Bug "${bug.title}" marked as RESOLVED. Changelog entry created. Next: dive_reexplore to check for regressions, then dive_generate_tests for a regression test.`
|
|
1066
|
+
: `Fix recorded but NOT yet verified. Navigate to ${route ?? "the affected route"} and confirm the fix, then call again with verified: true.`,
|
|
1067
|
+
_flywheel: [
|
|
1068
|
+
"✅ Bug tagged",
|
|
1069
|
+
"✅ Code located",
|
|
1070
|
+
"✅ Code fixed",
|
|
1071
|
+
verified ? "✅ Fix verified" : "⏳ Fix pending verification",
|
|
1072
|
+
"✅ Changelog updated",
|
|
1073
|
+
verified ? "✅ Bug resolved" : "⏳ Bug pending",
|
|
1074
|
+
"→ Next: dive_reexplore({ route }) to check for regressions",
|
|
1075
|
+
"→ Next: dive_generate_tests({ bugId }) for regression test",
|
|
1076
|
+
"→ Next: dive_code_review({ sessionId }) for full review",
|
|
1077
|
+
],
|
|
1078
|
+
};
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
// ── 10. Re-explore a route after changes ───────────────────────────────
|
|
1082
|
+
{
|
|
1083
|
+
name: "dive_reexplore",
|
|
1084
|
+
description: "Re-traverse a route after code changes to detect regressions and verify fixes. Compares the current state against previously registered components, bugs, and test results for that route. The agent should navigate to the route first (via Playwright), take a fresh snapshot/screenshot, then call this tool with what they observe. It diffs against the prior state and flags any new issues or confirms fixes.",
|
|
1085
|
+
inputSchema: {
|
|
1086
|
+
type: "object",
|
|
1087
|
+
properties: {
|
|
1088
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
1089
|
+
route: { type: "string", description: "Route being re-explored (e.g. '/cost')" },
|
|
1090
|
+
currentState: {
|
|
1091
|
+
type: "object",
|
|
1092
|
+
description: "What the agent currently observes",
|
|
1093
|
+
properties: {
|
|
1094
|
+
componentsVisible: {
|
|
1095
|
+
type: "array",
|
|
1096
|
+
description: "Component names still visible on the page",
|
|
1097
|
+
items: { type: "string" },
|
|
1098
|
+
},
|
|
1099
|
+
newIssues: {
|
|
1100
|
+
type: "array",
|
|
1101
|
+
description: "Any new issues noticed (regressions)",
|
|
1102
|
+
items: { type: "string" },
|
|
1103
|
+
},
|
|
1104
|
+
fixedIssues: {
|
|
1105
|
+
type: "array",
|
|
1106
|
+
description: "Previously tagged bugs/issues that are now fixed",
|
|
1107
|
+
items: { type: "string" },
|
|
1108
|
+
},
|
|
1109
|
+
consoleErrors: {
|
|
1110
|
+
type: "array",
|
|
1111
|
+
description: "Console errors observed",
|
|
1112
|
+
items: { type: "string" },
|
|
1113
|
+
},
|
|
1114
|
+
notes: { type: "string", description: "General observations" },
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
afterScreenshotId: { type: "string", description: "Screenshot taken during re-exploration" },
|
|
1118
|
+
},
|
|
1119
|
+
required: ["sessionId", "route", "currentState"],
|
|
1120
|
+
},
|
|
1121
|
+
handler: async (args) => {
|
|
1122
|
+
const { sessionId, route, currentState, afterScreenshotId } = args;
|
|
1123
|
+
const db = getDb();
|
|
1124
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
1125
|
+
if (!session)
|
|
1126
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
1127
|
+
// Find components on this route
|
|
1128
|
+
const allComponents = db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ?").all(sessionId);
|
|
1129
|
+
const routeComponents = allComponents.filter(c => {
|
|
1130
|
+
const meta = c.metadata ? JSON.parse(c.metadata) : {};
|
|
1131
|
+
return meta.route === route;
|
|
1132
|
+
});
|
|
1133
|
+
// Find bugs on this route
|
|
1134
|
+
const routeComponentIds = routeComponents.map(c => c.id);
|
|
1135
|
+
const routeBugs = routeComponentIds.length > 0
|
|
1136
|
+
? db.prepare(`SELECT * FROM ui_dive_bugs WHERE component_id IN (${routeComponentIds.map(() => "?").join(",")}) ORDER BY severity`).all(...routeComponentIds)
|
|
1137
|
+
: [];
|
|
1138
|
+
// Find design issues on this route
|
|
1139
|
+
const routeDesignIssues = db.prepare("SELECT * FROM ui_dive_design_issues WHERE session_id = ? AND route = ?").all(sessionId, route);
|
|
1140
|
+
// Previous fix verifications for bugs on this route
|
|
1141
|
+
const bugIds = routeBugs.map(b => b.id);
|
|
1142
|
+
const verifications = bugIds.length > 0
|
|
1143
|
+
? db.prepare(`SELECT * FROM ui_dive_fix_verifications WHERE bug_id IN (${bugIds.map(() => "?").join(",")}) ORDER BY created_at DESC`).all(...bugIds)
|
|
1144
|
+
: [];
|
|
1145
|
+
// Diff analysis
|
|
1146
|
+
const previousComponents = routeComponents.map(c => c.name);
|
|
1147
|
+
const missingComponents = previousComponents.filter(name => !(currentState.componentsVisible ?? []).includes(name));
|
|
1148
|
+
const newComponents = (currentState.componentsVisible ?? []).filter(name => !previousComponents.includes(name));
|
|
1149
|
+
const openBugs = routeBugs.filter(b => b.status !== "resolved");
|
|
1150
|
+
const resolvedBugs = routeBugs.filter(b => b.status === "resolved");
|
|
1151
|
+
const regressions = [];
|
|
1152
|
+
if (missingComponents.length > 0) {
|
|
1153
|
+
regressions.push(`${missingComponents.length} component(s) disappeared: ${missingComponents.join(", ")}`);
|
|
1154
|
+
}
|
|
1155
|
+
if ((currentState.consoleErrors ?? []).length > 0) {
|
|
1156
|
+
regressions.push(`${currentState.consoleErrors.length} console error(s) detected`);
|
|
1157
|
+
}
|
|
1158
|
+
if ((currentState.newIssues ?? []).length > 0) {
|
|
1159
|
+
regressions.push(...(currentState.newIssues ?? []).map(i => `New issue: ${i}`));
|
|
1160
|
+
}
|
|
1161
|
+
const regressionFree = regressions.length === 0;
|
|
1162
|
+
return {
|
|
1163
|
+
route,
|
|
1164
|
+
diff: {
|
|
1165
|
+
previousComponents: previousComponents.length,
|
|
1166
|
+
currentComponents: (currentState.componentsVisible ?? []).length,
|
|
1167
|
+
missingComponents,
|
|
1168
|
+
newComponents,
|
|
1169
|
+
openBugs: openBugs.length,
|
|
1170
|
+
resolvedBugs: resolvedBugs.length,
|
|
1171
|
+
designIssues: routeDesignIssues.length,
|
|
1172
|
+
fixVerifications: verifications.length,
|
|
1173
|
+
},
|
|
1174
|
+
regressions,
|
|
1175
|
+
regressionFree,
|
|
1176
|
+
fixedIssues: currentState.fixedIssues ?? [],
|
|
1177
|
+
consoleErrors: currentState.consoleErrors ?? [],
|
|
1178
|
+
afterScreenshotId: afterScreenshotId ?? null,
|
|
1179
|
+
_status: regressionFree
|
|
1180
|
+
? `✅ Route ${route} is regression-free. ${openBugs.length} open bug(s) remain.`
|
|
1181
|
+
: `⚠️ ${regressions.length} regression(s) detected on ${route}. Investigate before proceeding.`,
|
|
1182
|
+
_hint: regressionFree
|
|
1183
|
+
? openBugs.length > 0
|
|
1184
|
+
? `Route clean but ${openBugs.length} bug(s) still open: ${openBugs.map(b => b.title).join("; ")}. Fix them and re-verify.`
|
|
1185
|
+
: `Route fully clean! Generate a regression test: dive_generate_tests({ sessionId, route: "${route}" })`
|
|
1186
|
+
: `Regressions found — fix them before generating tests. Tag new bugs with tag_ui_bug.`,
|
|
1187
|
+
};
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1190
|
+
// ── 11. Generate regression tests from bugs/interactions ───────────────
|
|
1191
|
+
{
|
|
1192
|
+
name: "dive_generate_tests",
|
|
1193
|
+
description: "Generate Playwright regression test code from dive findings. Creates test cases from: bugs (verify the fix holds), interaction tests (replay the sequence), design issues (visual regression checks), and component assertions (verify component presence). The generated code can be saved to a test file and run with 'npx playwright test'. This closes the quality loop: UI bug → fix → regression test → CI protection.",
|
|
1194
|
+
inputSchema: {
|
|
1195
|
+
type: "object",
|
|
1196
|
+
properties: {
|
|
1197
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
1198
|
+
bugId: { type: "string", description: "Generate test for a specific bug fix (optional)" },
|
|
1199
|
+
componentId: { type: "string", description: "Generate tests for a specific component (optional)" },
|
|
1200
|
+
testId: { type: "string", description: "Generate from an existing interaction test (optional)" },
|
|
1201
|
+
route: { type: "string", description: "Generate tests for all findings on a route (optional)" },
|
|
1202
|
+
appUrl: { type: "string", description: "App URL for the test (default: session's app_url)" },
|
|
1203
|
+
outputPath: { type: "string", description: "File path to save the generated test (optional)" },
|
|
1204
|
+
framework: {
|
|
1205
|
+
type: "string",
|
|
1206
|
+
description: "Test framework: playwright (default), cypress, vitest",
|
|
1207
|
+
enum: ["playwright", "cypress", "vitest"],
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
required: ["sessionId"],
|
|
1211
|
+
},
|
|
1212
|
+
handler: async (args) => {
|
|
1213
|
+
const { sessionId, bugId, componentId, testId, route, appUrl, outputPath, framework } = args;
|
|
1214
|
+
const db = getDb();
|
|
1215
|
+
const session = db.prepare("SELECT * FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
1216
|
+
if (!session)
|
|
1217
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
1218
|
+
const baseUrl = appUrl ?? session.app_url;
|
|
1219
|
+
const fw = framework ?? "playwright";
|
|
1220
|
+
const testBlocks = [];
|
|
1221
|
+
const covers = [];
|
|
1222
|
+
// Gather bugs to cover
|
|
1223
|
+
let bugs = [];
|
|
1224
|
+
if (bugId) {
|
|
1225
|
+
const bug = db.prepare("SELECT * FROM ui_dive_bugs WHERE id = ?").get(bugId);
|
|
1226
|
+
if (bug)
|
|
1227
|
+
bugs = [bug];
|
|
1228
|
+
}
|
|
1229
|
+
else if (componentId) {
|
|
1230
|
+
bugs = db.prepare("SELECT * FROM ui_dive_bugs WHERE component_id = ?").all(componentId);
|
|
1231
|
+
}
|
|
1232
|
+
else if (route) {
|
|
1233
|
+
const comps = db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ?").all(sessionId);
|
|
1234
|
+
const routeCompIds = comps.filter(c => {
|
|
1235
|
+
const meta = c.metadata ? JSON.parse(c.metadata) : {};
|
|
1236
|
+
return meta.route === route;
|
|
1237
|
+
}).map(c => c.id);
|
|
1238
|
+
if (routeCompIds.length > 0) {
|
|
1239
|
+
bugs = db.prepare(`SELECT * FROM ui_dive_bugs WHERE component_id IN (${routeCompIds.map(() => "?").join(",")})`).all(...routeCompIds);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
bugs = db.prepare("SELECT * FROM ui_dive_bugs WHERE session_id = ?").all(sessionId);
|
|
1244
|
+
}
|
|
1245
|
+
// Generate bug regression tests
|
|
1246
|
+
for (const bug of bugs) {
|
|
1247
|
+
const comp = db.prepare("SELECT * FROM ui_dive_components WHERE id = ?").get(bug.component_id);
|
|
1248
|
+
const meta = comp?.metadata ? JSON.parse(comp.metadata) : {};
|
|
1249
|
+
const bugRoute = meta.route ?? "/";
|
|
1250
|
+
testBlocks.push(` test('regression: ${bug.title.replace(/'/g, "\\'")}', async ({ page }) => {
|
|
1251
|
+
await page.goto('${baseUrl}${bugRoute}');
|
|
1252
|
+
await page.waitForLoadState('networkidle');
|
|
1253
|
+
|
|
1254
|
+
// Bug: ${bug.description ?? bug.title}
|
|
1255
|
+
// Severity: ${bug.severity} | Category: ${bug.category}
|
|
1256
|
+
${bug.expected ? `// Expected: ${bug.expected}` : ""}
|
|
1257
|
+
${bug.actual ? `// Was: ${bug.actual}` : ""}
|
|
1258
|
+
|
|
1259
|
+
// TODO: Add specific assertions to verify the fix holds
|
|
1260
|
+
// Example: await expect(page.locator('.token-percentage')).not.toContainText('NaN');
|
|
1261
|
+
await expect(page).not.toContainText('Something went wrong');
|
|
1262
|
+
});`);
|
|
1263
|
+
covers.push(`bug:${bug.id}:${bug.title}`);
|
|
1264
|
+
}
|
|
1265
|
+
// Generate from interaction tests
|
|
1266
|
+
let interactionTests = [];
|
|
1267
|
+
if (testId) {
|
|
1268
|
+
const t = db.prepare("SELECT * FROM ui_dive_interaction_tests WHERE id = ?").get(testId);
|
|
1269
|
+
if (t)
|
|
1270
|
+
interactionTests = [t];
|
|
1271
|
+
}
|
|
1272
|
+
else if (!bugId && !componentId) {
|
|
1273
|
+
interactionTests = db.prepare("SELECT * FROM ui_dive_interaction_tests WHERE session_id = ?").all(sessionId);
|
|
1274
|
+
}
|
|
1275
|
+
for (const test of interactionTests) {
|
|
1276
|
+
const steps = db.prepare("SELECT * FROM ui_dive_test_steps WHERE test_id = ? ORDER BY step_index").all(test.id);
|
|
1277
|
+
const comp = db.prepare("SELECT * FROM ui_dive_components WHERE id = ?").get(test.component_id);
|
|
1278
|
+
const meta = comp?.metadata ? JSON.parse(comp.metadata) : {};
|
|
1279
|
+
const testRoute = meta.route ?? "/";
|
|
1280
|
+
const stepCode = steps.map(s => {
|
|
1281
|
+
const comment = ` // Step ${s.step_index}: ${s.action} ${s.target ?? ""} → ${s.expected}`;
|
|
1282
|
+
const assertion = s.status === "failed"
|
|
1283
|
+
? ` // FAILED: ${s.actual ?? "no actual recorded"}\n // TODO: Verify this step now passes`
|
|
1284
|
+
: ` // PASSED: ${s.actual ?? ""}`;
|
|
1285
|
+
return `${comment}\n${assertion}`;
|
|
1286
|
+
}).join("\n\n");
|
|
1287
|
+
testBlocks.push(` test('interaction: ${test.test_name.replace(/'/g, "\\'")}', async ({ page }) => {
|
|
1288
|
+
await page.goto('${baseUrl}${testRoute}');
|
|
1289
|
+
await page.waitForLoadState('networkidle');
|
|
1290
|
+
|
|
1291
|
+
${stepCode}
|
|
1292
|
+
});`);
|
|
1293
|
+
covers.push(`test:${test.id}:${test.test_name}`);
|
|
1294
|
+
}
|
|
1295
|
+
// Assemble full test file
|
|
1296
|
+
const testCode = fw === "playwright"
|
|
1297
|
+
? `import { test, expect } from '@playwright/test';
|
|
1298
|
+
|
|
1299
|
+
test.describe('UI Dive Regression Tests — ${session.app_name ?? baseUrl}', () => {
|
|
1300
|
+
${testBlocks.join("\n\n")}
|
|
1301
|
+
});
|
|
1302
|
+
`
|
|
1303
|
+
: `// Generated ${fw} tests — adapt as needed\n${testBlocks.join("\n\n")}`;
|
|
1304
|
+
// Save to file if requested
|
|
1305
|
+
if (outputPath) {
|
|
1306
|
+
try {
|
|
1307
|
+
mkdirSync(join(outputPath, ".."), { recursive: true });
|
|
1308
|
+
writeFileSync(outputPath, testCode, "utf-8");
|
|
1309
|
+
}
|
|
1310
|
+
catch (e) {
|
|
1311
|
+
return { error: true, message: `Failed to write test file: ${e.message}` };
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
// Store in DB
|
|
1315
|
+
const genTestId = genId("gtest");
|
|
1316
|
+
db.prepare(`INSERT INTO ui_dive_generated_tests (id, session_id, bug_id, component_id, test_id, test_framework, test_code, test_file_path, description, covers)
|
|
1317
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(genTestId, sessionId, bugId ?? null, componentId ?? null, testId ?? null, fw, testCode, outputPath ?? null, `Generated ${testBlocks.length} test(s) from dive findings`, JSON.stringify(covers));
|
|
1318
|
+
return {
|
|
1319
|
+
generatedTestId: genTestId,
|
|
1320
|
+
framework: fw,
|
|
1321
|
+
testCount: testBlocks.length,
|
|
1322
|
+
covers,
|
|
1323
|
+
outputPath: outputPath ?? null,
|
|
1324
|
+
testCode: testCode.length > 5000 ? testCode.slice(0, 5000) + "\n// ... truncated" : testCode,
|
|
1325
|
+
_hint: outputPath
|
|
1326
|
+
? `Test file saved to ${outputPath}. Run with: npx playwright test ${outputPath}`
|
|
1327
|
+
: `Test code generated (${testBlocks.length} tests). Save to a file with outputPath parameter, or copy the testCode above.`,
|
|
1328
|
+
};
|
|
1329
|
+
},
|
|
1330
|
+
},
|
|
1331
|
+
// ── 12. Produce structured code review from dive findings ──────────────
|
|
1332
|
+
{
|
|
1333
|
+
name: "dive_code_review",
|
|
1334
|
+
description: "Generate a structured code review report from all dive findings — similar to CodeRabbit or Augment Code Review. Aggregates bugs, design issues, interaction test failures, console errors, missing components, and backend link gaps into a prioritized review with severity, location, impact, and suggested fixes. Produces a score and recommendations. This is the quality gate: the dive findings become actionable review comments that can be posted to PRs or tracked as issues.",
|
|
1335
|
+
inputSchema: {
|
|
1336
|
+
type: "object",
|
|
1337
|
+
properties: {
|
|
1338
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
1339
|
+
reviewType: {
|
|
1340
|
+
type: "string",
|
|
1341
|
+
description: "Review scope: dive_findings (all), bugs_only, design_only, accessibility, performance",
|
|
1342
|
+
enum: ["dive_findings", "bugs_only", "design_only", "accessibility", "performance"],
|
|
1343
|
+
},
|
|
1344
|
+
includeCodeLocations: {
|
|
1345
|
+
type: "boolean",
|
|
1346
|
+
description: "Include code locations in review comments (default: true)",
|
|
1347
|
+
},
|
|
1348
|
+
format: {
|
|
1349
|
+
type: "string",
|
|
1350
|
+
description: "Output: markdown (readable), json (structured), github_comments (PR comment format)",
|
|
1351
|
+
enum: ["markdown", "json", "github_comments"],
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
required: ["sessionId"],
|
|
1355
|
+
},
|
|
1356
|
+
handler: async (args) => {
|
|
1357
|
+
const { sessionId, reviewType, includeCodeLocations, format } = args;
|
|
1358
|
+
const db = getDb();
|
|
1359
|
+
const session = db.prepare("SELECT * FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
1360
|
+
if (!session)
|
|
1361
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
1362
|
+
const bugs = db.prepare("SELECT * FROM ui_dive_bugs WHERE session_id = ? ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END").all(sessionId);
|
|
1363
|
+
const designIssues = db.prepare("SELECT * FROM ui_dive_design_issues WHERE session_id = ? ORDER BY severity").all(sessionId);
|
|
1364
|
+
const tests = db.prepare("SELECT * FROM ui_dive_interaction_tests WHERE session_id = ?").all(sessionId);
|
|
1365
|
+
const components = db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ?").all(sessionId);
|
|
1366
|
+
const codeLocations = (includeCodeLocations !== false)
|
|
1367
|
+
? db.prepare("SELECT * FROM ui_dive_code_locations WHERE session_id = ?").all(sessionId)
|
|
1368
|
+
: [];
|
|
1369
|
+
const verifications = db.prepare("SELECT * FROM ui_dive_fix_verifications WHERE session_id = ?").all(sessionId);
|
|
1370
|
+
const changelogs = db.prepare("SELECT * FROM ui_dive_changelogs WHERE session_id = ?").all(sessionId);
|
|
1371
|
+
// Build findings
|
|
1372
|
+
const findings = [];
|
|
1373
|
+
const type = reviewType ?? "dive_findings";
|
|
1374
|
+
if (type === "dive_findings" || type === "bugs_only") {
|
|
1375
|
+
for (const bug of bugs) {
|
|
1376
|
+
const comp = components.find(c => c.id === bug.component_id);
|
|
1377
|
+
const meta = comp?.metadata ? JSON.parse(comp.metadata) : {};
|
|
1378
|
+
const loc = codeLocations.find(cl => cl.bug_id === bug.id);
|
|
1379
|
+
const verification = verifications.find(v => v.bug_id === bug.id);
|
|
1380
|
+
findings.push({
|
|
1381
|
+
type: "bug",
|
|
1382
|
+
severity: bug.severity,
|
|
1383
|
+
title: bug.title,
|
|
1384
|
+
description: bug.description ?? "",
|
|
1385
|
+
location: meta.route ? `Route: ${meta.route}, Component: ${comp?.name ?? "unknown"}` : undefined,
|
|
1386
|
+
codeFile: loc?.file_path ?? meta.sourceFiles?.[0],
|
|
1387
|
+
codeLine: loc ? `L${loc.line_start}-${loc.line_end}` : undefined,
|
|
1388
|
+
impact: bug.severity === "critical" ? "Blocks user flow entirely"
|
|
1389
|
+
: bug.severity === "high" ? "Major degraded experience"
|
|
1390
|
+
: bug.severity === "medium" ? "Noticeable quality issue"
|
|
1391
|
+
: "Minor polish item",
|
|
1392
|
+
suggestedFix: bug.expected ? `Change from "${bug.actual ?? "current"}" to "${bug.expected}"` : "See description",
|
|
1393
|
+
status: verification?.verified ? "resolved" : (bug.status ?? "open"),
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (type === "dive_findings" || type === "design_only") {
|
|
1398
|
+
for (const issue of designIssues) {
|
|
1399
|
+
findings.push({
|
|
1400
|
+
type: "design",
|
|
1401
|
+
severity: issue.severity,
|
|
1402
|
+
title: issue.title,
|
|
1403
|
+
description: issue.description ?? "",
|
|
1404
|
+
location: issue.route ? `Route: ${issue.route}` : undefined,
|
|
1405
|
+
codeFile: issue.element_selector,
|
|
1406
|
+
impact: issue.severity === "critical" ? "Broken user experience" : "Visual inconsistency",
|
|
1407
|
+
suggestedFix: issue.expected_value ? `Expected: ${issue.expected_value}, Got: ${issue.actual_value}` : "See description",
|
|
1408
|
+
status: "open",
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
// Failed tests
|
|
1413
|
+
if (type === "dive_findings") {
|
|
1414
|
+
const failedTests = tests.filter(t => t.status === "failed");
|
|
1415
|
+
for (const test of failedTests) {
|
|
1416
|
+
const failedSteps = db.prepare("SELECT * FROM ui_dive_test_steps WHERE test_id = ? AND status = 'failed'").all(test.id);
|
|
1417
|
+
findings.push({
|
|
1418
|
+
type: "test_failure",
|
|
1419
|
+
severity: "high",
|
|
1420
|
+
title: `Test failed: ${test.test_name}`,
|
|
1421
|
+
description: failedSteps.map(s => `Step ${s.step_index}: Expected "${s.expected}", Got "${s.actual}"`).join("; "),
|
|
1422
|
+
impact: "Interaction flow broken",
|
|
1423
|
+
suggestedFix: "Fix the underlying component behavior, then re-run the test",
|
|
1424
|
+
status: "open",
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
// Severity counts
|
|
1429
|
+
const severityCounts = {
|
|
1430
|
+
critical: findings.filter(f => f.severity === "critical").length,
|
|
1431
|
+
high: findings.filter(f => f.severity === "high").length,
|
|
1432
|
+
medium: findings.filter(f => f.severity === "medium").length,
|
|
1433
|
+
low: findings.filter(f => f.severity === "low").length,
|
|
1434
|
+
};
|
|
1435
|
+
// Score: 100 - (critical*25 + high*10 + medium*5 + low*1)
|
|
1436
|
+
const rawScore = 100 - (severityCounts.critical * 25 + severityCounts.high * 10 + severityCounts.medium * 5 + severityCounts.low * 1);
|
|
1437
|
+
const score = Math.max(0, Math.min(100, rawScore));
|
|
1438
|
+
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
|
|
1439
|
+
// Recommendations
|
|
1440
|
+
const recommendations = [];
|
|
1441
|
+
if (severityCounts.critical > 0)
|
|
1442
|
+
recommendations.push(`🔴 FIX IMMEDIATELY: ${severityCounts.critical} critical issue(s) blocking users`);
|
|
1443
|
+
if (severityCounts.high > 0)
|
|
1444
|
+
recommendations.push(`🟠 HIGH PRIORITY: ${severityCounts.high} high-severity issue(s)`);
|
|
1445
|
+
const openFindings = findings.filter(f => f.status === "open");
|
|
1446
|
+
const resolvedFindings = findings.filter(f => f.status === "resolved");
|
|
1447
|
+
if (resolvedFindings.length > 0)
|
|
1448
|
+
recommendations.push(`✅ ${resolvedFindings.length} issue(s) already resolved`);
|
|
1449
|
+
if (changelogs.length > 0)
|
|
1450
|
+
recommendations.push(`📋 ${changelogs.length} change(s) tracked in changelog`);
|
|
1451
|
+
const untested = components.filter(c => c.interaction_count === 0);
|
|
1452
|
+
if (untested.length > 0)
|
|
1453
|
+
recommendations.push(`🧪 ${untested.length} component(s) have zero interactions — consider adding tests`);
|
|
1454
|
+
// Build output
|
|
1455
|
+
let reviewOutput;
|
|
1456
|
+
if (format === "github_comments") {
|
|
1457
|
+
reviewOutput = findings.map(f => {
|
|
1458
|
+
const fileRef = f.codeFile ? `\`${f.codeFile}${f.codeLine ? `:${f.codeLine}` : ""}\`` : "";
|
|
1459
|
+
return `### [${f.severity.toUpperCase()}] ${f.title}\n${f.description}\n${fileRef ? `**File:** ${fileRef}` : ""}\n**Impact:** ${f.impact}\n**Suggested fix:** ${f.suggestedFix}\n**Status:** ${f.status}`;
|
|
1460
|
+
}).join("\n\n---\n\n");
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
const lines = [];
|
|
1464
|
+
lines.push(`# Code Review: ${session.app_name ?? session.app_url}`);
|
|
1465
|
+
lines.push(`**Score:** ${score}/100 (${grade})`);
|
|
1466
|
+
lines.push(`**Findings:** ${findings.length} (${openFindings.length} open, ${resolvedFindings.length} resolved)`);
|
|
1467
|
+
lines.push(`**Severity:** ${severityCounts.critical} critical, ${severityCounts.high} high, ${severityCounts.medium} medium, ${severityCounts.low} low\n`);
|
|
1468
|
+
if (recommendations.length > 0) {
|
|
1469
|
+
lines.push("## Recommendations\n");
|
|
1470
|
+
for (const r of recommendations)
|
|
1471
|
+
lines.push(`- ${r}`);
|
|
1472
|
+
lines.push("");
|
|
1473
|
+
}
|
|
1474
|
+
lines.push("## Findings\n");
|
|
1475
|
+
for (const f of findings) {
|
|
1476
|
+
const icon = f.severity === "critical" ? "🔴" : f.severity === "high" ? "🟠" : f.severity === "medium" ? "🟡" : "🔵";
|
|
1477
|
+
lines.push(`### ${icon} [${f.severity.toUpperCase()}] ${f.title}`);
|
|
1478
|
+
lines.push(`- **Type:** ${f.type}`);
|
|
1479
|
+
if (f.location)
|
|
1480
|
+
lines.push(`- **Location:** ${f.location}`);
|
|
1481
|
+
if (f.codeFile)
|
|
1482
|
+
lines.push(`- **File:** \`${f.codeFile}${f.codeLine ? `:${f.codeLine}` : ""}\``);
|
|
1483
|
+
lines.push(`- **Impact:** ${f.impact}`);
|
|
1484
|
+
if (f.description)
|
|
1485
|
+
lines.push(`- **Details:** ${f.description}`);
|
|
1486
|
+
lines.push(`- **Suggested fix:** ${f.suggestedFix}`);
|
|
1487
|
+
lines.push(`- **Status:** ${f.status}\n`);
|
|
1488
|
+
}
|
|
1489
|
+
reviewOutput = lines.join("\n");
|
|
1490
|
+
}
|
|
1491
|
+
// Store in DB
|
|
1492
|
+
const reviewId = genId("rev");
|
|
1493
|
+
db.prepare(`INSERT INTO ui_dive_code_reviews (id, session_id, review_type, severity_counts, findings, summary, recommendations, score)
|
|
1494
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(reviewId, sessionId, type, JSON.stringify(severityCounts), JSON.stringify(findings), `Score: ${score}/100 (${grade}). ${findings.length} findings.`, JSON.stringify(recommendations), score);
|
|
1495
|
+
return {
|
|
1496
|
+
reviewId,
|
|
1497
|
+
score,
|
|
1498
|
+
grade,
|
|
1499
|
+
severityCounts,
|
|
1500
|
+
findingsCount: findings.length,
|
|
1501
|
+
openCount: openFindings.length,
|
|
1502
|
+
resolvedCount: resolvedFindings.length,
|
|
1503
|
+
recommendations,
|
|
1504
|
+
review: format === "json" ? undefined : reviewOutput,
|
|
1505
|
+
findings: format === "json" ? findings : undefined,
|
|
1506
|
+
_hint: `Code review complete: ${score}/100 (${grade}). ${openFindings.length} open finding(s). ${recommendations[0] ?? ""}`,
|
|
1507
|
+
};
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1510
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1511
|
+
// TOOL: open_dive_dashboard — Open the local dashboard in a browser
|
|
1512
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1513
|
+
{
|
|
1514
|
+
name: "open_dive_dashboard",
|
|
1515
|
+
description: "Open the NodeBench UI Dive dashboard in a browser. Shows the full flywheel cycle: " +
|
|
1516
|
+
"explored routes, components, interactions, screenshots, bugs, code locations, fixes, " +
|
|
1517
|
+
"changelogs, generated tests, and code reviews. The dashboard auto-refreshes every 5s " +
|
|
1518
|
+
"so you can watch progress live as the dive runs. Pass a sessionId to deep-link to a " +
|
|
1519
|
+
"specific session, or omit to show the latest.",
|
|
1520
|
+
inputSchema: {
|
|
1521
|
+
type: "object",
|
|
1522
|
+
properties: {
|
|
1523
|
+
sessionId: {
|
|
1524
|
+
type: "string",
|
|
1525
|
+
description: "Optional session ID to deep-link to. Omit for latest session.",
|
|
1526
|
+
},
|
|
1527
|
+
openBrowser: {
|
|
1528
|
+
type: "boolean",
|
|
1529
|
+
description: "Whether to auto-open the URL in the default browser (default: true).",
|
|
1530
|
+
default: true,
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
},
|
|
1534
|
+
handler: async (args) => {
|
|
1535
|
+
const url = getDashboardUrl();
|
|
1536
|
+
if (!url) {
|
|
1537
|
+
return {
|
|
1538
|
+
error: "Dashboard server is not running. It starts automatically with the MCP server on port 6274.",
|
|
1539
|
+
_hint: "Restart the MCP server to start the dashboard.",
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
const fullUrl = args.sessionId
|
|
1543
|
+
? `${url}#session=${encodeURIComponent(args.sessionId)}`
|
|
1544
|
+
: url;
|
|
1545
|
+
// Try to open in default browser
|
|
1546
|
+
if (args.openBrowser !== false) {
|
|
1547
|
+
try {
|
|
1548
|
+
const platform = process.platform;
|
|
1549
|
+
if (platform === "win32") {
|
|
1550
|
+
execSync(`start "" "${fullUrl}"`, { stdio: "ignore" });
|
|
1551
|
+
}
|
|
1552
|
+
else if (platform === "darwin") {
|
|
1553
|
+
execSync(`open "${fullUrl}"`, { stdio: "ignore" });
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
execSync(`xdg-open "${fullUrl}"`, { stdio: "ignore" });
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
catch {
|
|
1560
|
+
// Browser open is best-effort
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
url: fullUrl,
|
|
1565
|
+
dashboardPort: url.split(":").pop(),
|
|
1566
|
+
_hint: `Dashboard is live at ${fullUrl}. It auto-refreshes every 5 seconds.`,
|
|
1567
|
+
};
|
|
1568
|
+
},
|
|
1569
|
+
},
|
|
882
1570
|
];
|
|
883
1571
|
//# sourceMappingURL=uiUxDiveAdvancedTools.js.map
|