diffprism 0.13.8 → 0.15.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/bin.js +199 -78
- package/dist/{chunk-QB2PKDLU.js → chunk-NGHUHDAM.js} +454 -22
- package/dist/mcp-server.js +37 -6
- package/package.json +1 -1
- package/ui-dist/assets/index-CKJwY3F0.js +244 -0
- package/ui-dist/assets/index-D39rVNSs.css +1 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-CEmIfNA9.css +0 -1
- package/ui-dist/assets/index-DiJ_eCA-.js +0 -239
|
@@ -303,6 +303,9 @@ function parseDiff(rawDiff, baseRef, headRef) {
|
|
|
303
303
|
|
|
304
304
|
// packages/git/src/index.ts
|
|
305
305
|
function getDiff(ref, options) {
|
|
306
|
+
if (ref === "working-copy") {
|
|
307
|
+
return getWorkingCopyDiff(options);
|
|
308
|
+
}
|
|
306
309
|
const rawDiff = getGitDiff(ref, options);
|
|
307
310
|
let baseRef;
|
|
308
311
|
let headRef;
|
|
@@ -323,6 +326,29 @@ function getDiff(ref, options) {
|
|
|
323
326
|
const diffSet = parseDiff(rawDiff, baseRef, headRef);
|
|
324
327
|
return { diffSet, rawDiff };
|
|
325
328
|
}
|
|
329
|
+
function getWorkingCopyDiff(options) {
|
|
330
|
+
const stagedRaw = getGitDiff("staged", options);
|
|
331
|
+
const unstagedRaw = getGitDiff("unstaged", options);
|
|
332
|
+
const stagedDiffSet = parseDiff(stagedRaw, "HEAD", "staged");
|
|
333
|
+
const unstagedDiffSet = parseDiff(unstagedRaw, "staged", "working tree");
|
|
334
|
+
const stagedFiles = stagedDiffSet.files.map((f) => ({
|
|
335
|
+
...f,
|
|
336
|
+
stage: "staged"
|
|
337
|
+
}));
|
|
338
|
+
const unstagedFiles = unstagedDiffSet.files.map((f) => ({
|
|
339
|
+
...f,
|
|
340
|
+
stage: "unstaged"
|
|
341
|
+
}));
|
|
342
|
+
const rawDiff = [stagedRaw, unstagedRaw].filter(Boolean).join("");
|
|
343
|
+
return {
|
|
344
|
+
diffSet: {
|
|
345
|
+
baseRef: "HEAD",
|
|
346
|
+
headRef: "working tree",
|
|
347
|
+
files: [...stagedFiles, ...unstagedFiles]
|
|
348
|
+
},
|
|
349
|
+
rawDiff
|
|
350
|
+
};
|
|
351
|
+
}
|
|
326
352
|
|
|
327
353
|
// packages/analysis/src/deterministic.ts
|
|
328
354
|
function categorizeFiles(files) {
|
|
@@ -511,15 +537,15 @@ var CONFIG_PATTERNS = [
|
|
|
511
537
|
/vite\.config/,
|
|
512
538
|
/vitest\.config/
|
|
513
539
|
];
|
|
514
|
-
function isTestFile(
|
|
515
|
-
return TEST_PATTERNS.some((re) => re.test(
|
|
540
|
+
function isTestFile(path6) {
|
|
541
|
+
return TEST_PATTERNS.some((re) => re.test(path6));
|
|
516
542
|
}
|
|
517
|
-
function isNonCodeFile(
|
|
518
|
-
const ext =
|
|
543
|
+
function isNonCodeFile(path6) {
|
|
544
|
+
const ext = path6.slice(path6.lastIndexOf("."));
|
|
519
545
|
return NON_CODE_EXTENSIONS.has(ext);
|
|
520
546
|
}
|
|
521
|
-
function isConfigFile(
|
|
522
|
-
return CONFIG_PATTERNS.some((re) => re.test(
|
|
547
|
+
function isConfigFile(path6) {
|
|
548
|
+
return CONFIG_PATTERNS.some((re) => re.test(path6));
|
|
523
549
|
}
|
|
524
550
|
function detectTestCoverageGaps(files) {
|
|
525
551
|
const filePaths = new Set(files.map((f) => f.path));
|
|
@@ -694,14 +720,14 @@ function analyze(diffSet) {
|
|
|
694
720
|
// packages/core/src/ws-bridge.ts
|
|
695
721
|
import { WebSocketServer, WebSocket } from "ws";
|
|
696
722
|
function createWsBridge(port) {
|
|
697
|
-
const
|
|
723
|
+
const wss2 = new WebSocketServer({ port });
|
|
698
724
|
let client = null;
|
|
699
725
|
let resultResolve = null;
|
|
700
726
|
let resultReject = null;
|
|
701
727
|
let pendingInit = null;
|
|
702
728
|
let initPayload = null;
|
|
703
729
|
let closeTimer = null;
|
|
704
|
-
|
|
730
|
+
wss2.on("connection", (ws) => {
|
|
705
731
|
if (closeTimer) {
|
|
706
732
|
clearTimeout(closeTimer);
|
|
707
733
|
closeTimer = null;
|
|
@@ -761,10 +787,10 @@ function createWsBridge(port) {
|
|
|
761
787
|
});
|
|
762
788
|
},
|
|
763
789
|
close() {
|
|
764
|
-
for (const ws of
|
|
790
|
+
for (const ws of wss2.clients) {
|
|
765
791
|
ws.close();
|
|
766
792
|
}
|
|
767
|
-
|
|
793
|
+
wss2.close();
|
|
768
794
|
}
|
|
769
795
|
};
|
|
770
796
|
}
|
|
@@ -1091,13 +1117,13 @@ function createWatchBridge(port, callbacks) {
|
|
|
1091
1117
|
res.writeHead(404);
|
|
1092
1118
|
res.end("Not found");
|
|
1093
1119
|
});
|
|
1094
|
-
const
|
|
1120
|
+
const wss2 = new WebSocketServer2({ server: httpServer });
|
|
1095
1121
|
function sendToClient(msg) {
|
|
1096
1122
|
if (client && client.readyState === WebSocket2.OPEN) {
|
|
1097
1123
|
client.send(JSON.stringify(msg));
|
|
1098
1124
|
}
|
|
1099
1125
|
}
|
|
1100
|
-
|
|
1126
|
+
wss2.on("connection", (ws) => {
|
|
1101
1127
|
if (closeTimer) {
|
|
1102
1128
|
clearTimeout(closeTimer);
|
|
1103
1129
|
closeTimer = null;
|
|
@@ -1156,10 +1182,10 @@ function createWatchBridge(port, callbacks) {
|
|
|
1156
1182
|
if (closeTimer) {
|
|
1157
1183
|
clearTimeout(closeTimer);
|
|
1158
1184
|
}
|
|
1159
|
-
for (const ws of
|
|
1185
|
+
for (const ws of wss2.clients) {
|
|
1160
1186
|
ws.close();
|
|
1161
1187
|
}
|
|
1162
|
-
|
|
1188
|
+
wss2.close();
|
|
1163
1189
|
await new Promise((resolve2) => {
|
|
1164
1190
|
httpServer.close(() => resolve2());
|
|
1165
1191
|
});
|
|
@@ -1173,25 +1199,29 @@ function createWatchBridge(port, callbacks) {
|
|
|
1173
1199
|
function hashDiff(rawDiff) {
|
|
1174
1200
|
return createHash("sha256").update(rawDiff).digest("hex");
|
|
1175
1201
|
}
|
|
1202
|
+
function fileKey(file) {
|
|
1203
|
+
return file.stage ? `${file.stage}:${file.path}` : file.path;
|
|
1204
|
+
}
|
|
1176
1205
|
function detectChangedFiles(oldDiffSet, newDiffSet) {
|
|
1177
1206
|
if (!oldDiffSet) {
|
|
1178
|
-
return newDiffSet.files.map(
|
|
1207
|
+
return newDiffSet.files.map(fileKey);
|
|
1179
1208
|
}
|
|
1180
1209
|
const oldFiles = new Map(
|
|
1181
|
-
oldDiffSet.files.map((f) => [f
|
|
1210
|
+
oldDiffSet.files.map((f) => [fileKey(f), f])
|
|
1182
1211
|
);
|
|
1183
1212
|
const changed = [];
|
|
1184
1213
|
for (const newFile of newDiffSet.files) {
|
|
1185
|
-
const
|
|
1214
|
+
const key = fileKey(newFile);
|
|
1215
|
+
const oldFile = oldFiles.get(key);
|
|
1186
1216
|
if (!oldFile) {
|
|
1187
|
-
changed.push(
|
|
1217
|
+
changed.push(key);
|
|
1188
1218
|
} else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
|
|
1189
|
-
changed.push(
|
|
1219
|
+
changed.push(key);
|
|
1190
1220
|
}
|
|
1191
1221
|
}
|
|
1192
1222
|
for (const oldFile of oldDiffSet.files) {
|
|
1193
|
-
if (!newDiffSet.files.some((f) => f
|
|
1194
|
-
changed.push(oldFile
|
|
1223
|
+
if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
|
|
1224
|
+
changed.push(fileKey(oldFile));
|
|
1195
1225
|
}
|
|
1196
1226
|
}
|
|
1197
1227
|
return changed;
|
|
@@ -1339,10 +1369,412 @@ Review submitted: ${result.decision}`);
|
|
|
1339
1369
|
return { stop, updateContext };
|
|
1340
1370
|
}
|
|
1341
1371
|
|
|
1372
|
+
// packages/core/src/global-server.ts
|
|
1373
|
+
import http3 from "http";
|
|
1374
|
+
import { randomUUID } from "crypto";
|
|
1375
|
+
import getPort3 from "get-port";
|
|
1376
|
+
import open3 from "open";
|
|
1377
|
+
import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
|
|
1378
|
+
|
|
1379
|
+
// packages/core/src/server-file.ts
|
|
1380
|
+
import fs3 from "fs";
|
|
1381
|
+
import path5 from "path";
|
|
1382
|
+
import os from "os";
|
|
1383
|
+
function serverDir() {
|
|
1384
|
+
return path5.join(os.homedir(), ".diffprism");
|
|
1385
|
+
}
|
|
1386
|
+
function serverFilePath() {
|
|
1387
|
+
return path5.join(serverDir(), "server.json");
|
|
1388
|
+
}
|
|
1389
|
+
function isPidAlive2(pid) {
|
|
1390
|
+
try {
|
|
1391
|
+
process.kill(pid, 0);
|
|
1392
|
+
return true;
|
|
1393
|
+
} catch {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function writeServerFile(info) {
|
|
1398
|
+
const dir = serverDir();
|
|
1399
|
+
if (!fs3.existsSync(dir)) {
|
|
1400
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1401
|
+
}
|
|
1402
|
+
fs3.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
|
|
1403
|
+
}
|
|
1404
|
+
function readServerFile() {
|
|
1405
|
+
const filePath = serverFilePath();
|
|
1406
|
+
if (!fs3.existsSync(filePath)) {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
try {
|
|
1410
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
1411
|
+
const info = JSON.parse(raw);
|
|
1412
|
+
if (!isPidAlive2(info.pid)) {
|
|
1413
|
+
fs3.unlinkSync(filePath);
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
return info;
|
|
1417
|
+
} catch {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function removeServerFile() {
|
|
1422
|
+
try {
|
|
1423
|
+
const filePath = serverFilePath();
|
|
1424
|
+
if (fs3.existsSync(filePath)) {
|
|
1425
|
+
fs3.unlinkSync(filePath);
|
|
1426
|
+
}
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
async function isServerAlive() {
|
|
1431
|
+
const info = readServerFile();
|
|
1432
|
+
if (!info) {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
|
|
1437
|
+
signal: AbortSignal.timeout(2e3)
|
|
1438
|
+
});
|
|
1439
|
+
if (response.ok) {
|
|
1440
|
+
return info;
|
|
1441
|
+
}
|
|
1442
|
+
return null;
|
|
1443
|
+
} catch {
|
|
1444
|
+
removeServerFile();
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// packages/core/src/global-server.ts
|
|
1450
|
+
var sessions2 = /* @__PURE__ */ new Map();
|
|
1451
|
+
var clientSessions = /* @__PURE__ */ new Map();
|
|
1452
|
+
function toSummary(session) {
|
|
1453
|
+
const { payload } = session;
|
|
1454
|
+
const fileCount = payload.diffSet.files.length;
|
|
1455
|
+
let additions = 0;
|
|
1456
|
+
let deletions = 0;
|
|
1457
|
+
for (const file of payload.diffSet.files) {
|
|
1458
|
+
additions += file.additions;
|
|
1459
|
+
deletions += file.deletions;
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
id: session.id,
|
|
1463
|
+
projectPath: session.projectPath,
|
|
1464
|
+
branch: payload.metadata.currentBranch,
|
|
1465
|
+
title: payload.metadata.title,
|
|
1466
|
+
fileCount,
|
|
1467
|
+
additions,
|
|
1468
|
+
deletions,
|
|
1469
|
+
status: session.status,
|
|
1470
|
+
createdAt: session.createdAt
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
function readBody(req) {
|
|
1474
|
+
return new Promise((resolve, reject) => {
|
|
1475
|
+
let body = "";
|
|
1476
|
+
req.on("data", (chunk) => {
|
|
1477
|
+
body += chunk.toString();
|
|
1478
|
+
});
|
|
1479
|
+
req.on("end", () => resolve(body));
|
|
1480
|
+
req.on("error", reject);
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
function jsonResponse(res, status, data) {
|
|
1484
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1485
|
+
res.end(JSON.stringify(data));
|
|
1486
|
+
}
|
|
1487
|
+
function matchRoute(method, url, expectedMethod, pattern) {
|
|
1488
|
+
if (method !== expectedMethod) return null;
|
|
1489
|
+
const patternParts = pattern.split("/");
|
|
1490
|
+
const urlParts = url.split("/");
|
|
1491
|
+
if (patternParts.length !== urlParts.length) return null;
|
|
1492
|
+
const params = {};
|
|
1493
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
1494
|
+
if (patternParts[i].startsWith(":")) {
|
|
1495
|
+
params[patternParts[i].slice(1)] = urlParts[i];
|
|
1496
|
+
} else if (patternParts[i] !== urlParts[i]) {
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return params;
|
|
1501
|
+
}
|
|
1502
|
+
var wss = null;
|
|
1503
|
+
function broadcastToAll(msg) {
|
|
1504
|
+
if (!wss) return;
|
|
1505
|
+
const data = JSON.stringify(msg);
|
|
1506
|
+
for (const client of wss.clients) {
|
|
1507
|
+
if (client.readyState === WebSocket3.OPEN) {
|
|
1508
|
+
client.send(data);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function sendToSessionClients(sessionId, msg) {
|
|
1513
|
+
if (!wss) return;
|
|
1514
|
+
const data = JSON.stringify(msg);
|
|
1515
|
+
for (const [client, sid] of clientSessions.entries()) {
|
|
1516
|
+
if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
|
|
1517
|
+
client.send(data);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async function handleApiRequest(req, res) {
|
|
1522
|
+
const method = req.method ?? "GET";
|
|
1523
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
1524
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1525
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1526
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1527
|
+
if (method === "OPTIONS") {
|
|
1528
|
+
res.writeHead(204);
|
|
1529
|
+
res.end();
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
1532
|
+
if (!url.startsWith("/api/")) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
if (method === "GET" && url === "/api/status") {
|
|
1536
|
+
jsonResponse(res, 200, {
|
|
1537
|
+
running: true,
|
|
1538
|
+
pid: process.pid,
|
|
1539
|
+
sessions: sessions2.size,
|
|
1540
|
+
uptime: process.uptime()
|
|
1541
|
+
});
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
if (method === "POST" && url === "/api/reviews") {
|
|
1545
|
+
try {
|
|
1546
|
+
const body = await readBody(req);
|
|
1547
|
+
const { payload, projectPath } = JSON.parse(body);
|
|
1548
|
+
const sessionId = `session-${randomUUID().slice(0, 8)}`;
|
|
1549
|
+
payload.reviewId = sessionId;
|
|
1550
|
+
const session = {
|
|
1551
|
+
id: sessionId,
|
|
1552
|
+
payload,
|
|
1553
|
+
projectPath,
|
|
1554
|
+
status: "pending",
|
|
1555
|
+
createdAt: Date.now(),
|
|
1556
|
+
result: null
|
|
1557
|
+
};
|
|
1558
|
+
sessions2.set(sessionId, session);
|
|
1559
|
+
broadcastToAll({
|
|
1560
|
+
type: "session:added",
|
|
1561
|
+
payload: toSummary(session)
|
|
1562
|
+
});
|
|
1563
|
+
jsonResponse(res, 201, { sessionId });
|
|
1564
|
+
} catch {
|
|
1565
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1566
|
+
}
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
if (method === "GET" && url === "/api/reviews") {
|
|
1570
|
+
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
1571
|
+
jsonResponse(res, 200, { sessions: summaries });
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
const getReviewParams = matchRoute(method, url, "GET", "/api/reviews/:id");
|
|
1575
|
+
if (getReviewParams) {
|
|
1576
|
+
const session = sessions2.get(getReviewParams.id);
|
|
1577
|
+
if (!session) {
|
|
1578
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
jsonResponse(res, 200, toSummary(session));
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
const postResultParams = matchRoute(method, url, "POST", "/api/reviews/:id/result");
|
|
1585
|
+
if (postResultParams) {
|
|
1586
|
+
const session = sessions2.get(postResultParams.id);
|
|
1587
|
+
if (!session) {
|
|
1588
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1589
|
+
return true;
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
const body = await readBody(req);
|
|
1593
|
+
const result = JSON.parse(body);
|
|
1594
|
+
session.result = result;
|
|
1595
|
+
session.status = "submitted";
|
|
1596
|
+
jsonResponse(res, 200, { ok: true });
|
|
1597
|
+
} catch {
|
|
1598
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1599
|
+
}
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
const getResultParams = matchRoute(method, url, "GET", "/api/reviews/:id/result");
|
|
1603
|
+
if (getResultParams) {
|
|
1604
|
+
const session = sessions2.get(getResultParams.id);
|
|
1605
|
+
if (!session) {
|
|
1606
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
if (session.result) {
|
|
1610
|
+
jsonResponse(res, 200, { result: session.result, status: "submitted" });
|
|
1611
|
+
} else {
|
|
1612
|
+
jsonResponse(res, 200, { result: null, status: session.status });
|
|
1613
|
+
}
|
|
1614
|
+
return true;
|
|
1615
|
+
}
|
|
1616
|
+
const postContextParams = matchRoute(method, url, "POST", "/api/reviews/:id/context");
|
|
1617
|
+
if (postContextParams) {
|
|
1618
|
+
const session = sessions2.get(postContextParams.id);
|
|
1619
|
+
if (!session) {
|
|
1620
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1621
|
+
return true;
|
|
1622
|
+
}
|
|
1623
|
+
try {
|
|
1624
|
+
const body = await readBody(req);
|
|
1625
|
+
const contextPayload = JSON.parse(body);
|
|
1626
|
+
if (contextPayload.reasoning !== void 0) {
|
|
1627
|
+
session.payload.metadata.reasoning = contextPayload.reasoning;
|
|
1628
|
+
}
|
|
1629
|
+
if (contextPayload.title !== void 0) {
|
|
1630
|
+
session.payload.metadata.title = contextPayload.title;
|
|
1631
|
+
}
|
|
1632
|
+
if (contextPayload.description !== void 0) {
|
|
1633
|
+
session.payload.metadata.description = contextPayload.description;
|
|
1634
|
+
}
|
|
1635
|
+
sendToSessionClients(session.id, {
|
|
1636
|
+
type: "context:update",
|
|
1637
|
+
payload: contextPayload
|
|
1638
|
+
});
|
|
1639
|
+
jsonResponse(res, 200, { ok: true });
|
|
1640
|
+
} catch {
|
|
1641
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1642
|
+
}
|
|
1643
|
+
return true;
|
|
1644
|
+
}
|
|
1645
|
+
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
1646
|
+
if (deleteParams) {
|
|
1647
|
+
if (sessions2.delete(deleteParams.id)) {
|
|
1648
|
+
jsonResponse(res, 200, { ok: true });
|
|
1649
|
+
} else {
|
|
1650
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1651
|
+
}
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
jsonResponse(res, 404, { error: "Not found" });
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
async function startGlobalServer(options = {}) {
|
|
1658
|
+
const {
|
|
1659
|
+
httpPort: preferredHttpPort = 24680,
|
|
1660
|
+
wsPort: preferredWsPort = 24681,
|
|
1661
|
+
silent = false,
|
|
1662
|
+
dev = false
|
|
1663
|
+
} = options;
|
|
1664
|
+
const [httpPort, wsPort] = await Promise.all([
|
|
1665
|
+
getPort3({ port: preferredHttpPort }),
|
|
1666
|
+
getPort3({ port: preferredWsPort })
|
|
1667
|
+
]);
|
|
1668
|
+
let uiPort;
|
|
1669
|
+
let uiHttpServer = null;
|
|
1670
|
+
let viteServer = null;
|
|
1671
|
+
if (dev) {
|
|
1672
|
+
uiPort = await getPort3();
|
|
1673
|
+
const uiRoot = resolveUiRoot();
|
|
1674
|
+
viteServer = await startViteDevServer(uiRoot, uiPort, silent);
|
|
1675
|
+
} else {
|
|
1676
|
+
uiPort = await getPort3();
|
|
1677
|
+
const uiDist = resolveUiDist();
|
|
1678
|
+
uiHttpServer = await createStaticServer(uiDist, uiPort);
|
|
1679
|
+
}
|
|
1680
|
+
const httpServer = http3.createServer(async (req, res) => {
|
|
1681
|
+
const handled = await handleApiRequest(req, res);
|
|
1682
|
+
if (!handled) {
|
|
1683
|
+
res.writeHead(404);
|
|
1684
|
+
res.end("Not found");
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
wss = new WebSocketServer3({ port: wsPort });
|
|
1688
|
+
wss.on("connection", (ws, req) => {
|
|
1689
|
+
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
1690
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
1691
|
+
if (sessionId) {
|
|
1692
|
+
clientSessions.set(ws, sessionId);
|
|
1693
|
+
const session = sessions2.get(sessionId);
|
|
1694
|
+
if (session) {
|
|
1695
|
+
session.status = "in_review";
|
|
1696
|
+
const msg = {
|
|
1697
|
+
type: "review:init",
|
|
1698
|
+
payload: session.payload
|
|
1699
|
+
};
|
|
1700
|
+
ws.send(JSON.stringify(msg));
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
ws.on("message", (data) => {
|
|
1704
|
+
try {
|
|
1705
|
+
const msg = JSON.parse(data.toString());
|
|
1706
|
+
if (msg.type === "review:submit") {
|
|
1707
|
+
const sid = clientSessions.get(ws);
|
|
1708
|
+
if (sid) {
|
|
1709
|
+
const session = sessions2.get(sid);
|
|
1710
|
+
if (session) {
|
|
1711
|
+
session.result = msg.payload;
|
|
1712
|
+
session.status = "submitted";
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
} catch {
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
ws.on("close", () => {
|
|
1720
|
+
clientSessions.delete(ws);
|
|
1721
|
+
});
|
|
1722
|
+
});
|
|
1723
|
+
await new Promise((resolve, reject) => {
|
|
1724
|
+
httpServer.on("error", reject);
|
|
1725
|
+
httpServer.listen(httpPort, () => resolve());
|
|
1726
|
+
});
|
|
1727
|
+
const serverInfo = {
|
|
1728
|
+
httpPort,
|
|
1729
|
+
wsPort,
|
|
1730
|
+
pid: process.pid,
|
|
1731
|
+
startedAt: Date.now()
|
|
1732
|
+
};
|
|
1733
|
+
writeServerFile(serverInfo);
|
|
1734
|
+
if (!silent) {
|
|
1735
|
+
console.log(`
|
|
1736
|
+
DiffPrism Global Server`);
|
|
1737
|
+
console.log(` API: http://localhost:${httpPort}`);
|
|
1738
|
+
console.log(` WS: ws://localhost:${wsPort}`);
|
|
1739
|
+
console.log(` UI: http://localhost:${uiPort}`);
|
|
1740
|
+
console.log(` PID: ${process.pid}`);
|
|
1741
|
+
console.log(`
|
|
1742
|
+
Waiting for reviews...
|
|
1743
|
+
`);
|
|
1744
|
+
}
|
|
1745
|
+
const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&serverMode=true`;
|
|
1746
|
+
await open3(uiUrl);
|
|
1747
|
+
async function stop() {
|
|
1748
|
+
if (wss) {
|
|
1749
|
+
for (const client of wss.clients) {
|
|
1750
|
+
client.close();
|
|
1751
|
+
}
|
|
1752
|
+
wss.close();
|
|
1753
|
+
wss = null;
|
|
1754
|
+
}
|
|
1755
|
+
clientSessions.clear();
|
|
1756
|
+
sessions2.clear();
|
|
1757
|
+
await new Promise((resolve) => {
|
|
1758
|
+
httpServer.close(() => resolve());
|
|
1759
|
+
});
|
|
1760
|
+
if (viteServer) {
|
|
1761
|
+
await viteServer.close();
|
|
1762
|
+
}
|
|
1763
|
+
if (uiHttpServer) {
|
|
1764
|
+
uiHttpServer.close();
|
|
1765
|
+
}
|
|
1766
|
+
removeServerFile();
|
|
1767
|
+
}
|
|
1768
|
+
return { httpPort, wsPort, stop };
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1342
1771
|
export {
|
|
1343
1772
|
startReview,
|
|
1344
1773
|
readWatchFile,
|
|
1345
1774
|
readReviewResult,
|
|
1346
1775
|
consumeReviewResult,
|
|
1347
|
-
startWatch
|
|
1776
|
+
startWatch,
|
|
1777
|
+
readServerFile,
|
|
1778
|
+
isServerAlive,
|
|
1779
|
+
startGlobalServer
|
|
1348
1780
|
};
|
package/dist/mcp-server.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
readReviewResult,
|
|
4
4
|
readWatchFile,
|
|
5
5
|
startReview
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-NGHUHDAM.js";
|
|
7
7
|
|
|
8
8
|
// packages/mcp-server/src/index.ts
|
|
9
9
|
import fs from "fs";
|
|
@@ -14,14 +14,14 @@ import { z } from "zod";
|
|
|
14
14
|
async function startMcpServer() {
|
|
15
15
|
const server = new McpServer({
|
|
16
16
|
name: "diffprism",
|
|
17
|
-
version: true ? "0.
|
|
17
|
+
version: true ? "0.15.0" : "0.0.0-dev"
|
|
18
18
|
});
|
|
19
19
|
server.tool(
|
|
20
20
|
"open_review",
|
|
21
21
|
"Open a browser-based code review for local git changes. Blocks until the engineer submits their review decision.",
|
|
22
22
|
{
|
|
23
23
|
diff_ref: z.string().describe(
|
|
24
|
-
'Git diff reference: "staged", "unstaged", or a ref range like "HEAD~3..HEAD"'
|
|
24
|
+
'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), or a ref range like "HEAD~3..HEAD"'
|
|
25
25
|
),
|
|
26
26
|
title: z.string().optional().describe("Title for the review"),
|
|
27
27
|
description: z.string().optional().describe("Description of the changes"),
|
|
@@ -124,10 +124,41 @@ async function startMcpServer() {
|
|
|
124
124
|
);
|
|
125
125
|
server.tool(
|
|
126
126
|
"get_review_result",
|
|
127
|
-
"Fetch the most recent review result from a DiffPrism watch session. Returns the reviewer's decision and comments if a review has been submitted, or a message indicating no pending result. The result is marked as consumed after retrieval so it won't be returned again.",
|
|
128
|
-
{
|
|
129
|
-
|
|
127
|
+
"Fetch the most recent review result from a DiffPrism watch session. Returns the reviewer's decision and comments if a review has been submitted, or a message indicating no pending result. The result is marked as consumed after retrieval so it won't be returned again. Use wait=true to block until a result is available (recommended after pushing context to a watch session).",
|
|
128
|
+
{
|
|
129
|
+
wait: z.boolean().optional().describe("If true, poll until a review result is available (blocks up to timeout)"),
|
|
130
|
+
timeout: z.number().optional().describe("Max wait time in seconds when wait=true (default: 300, max: 600)")
|
|
131
|
+
},
|
|
132
|
+
async ({ wait, timeout }) => {
|
|
130
133
|
try {
|
|
134
|
+
if (wait) {
|
|
135
|
+
const maxWaitMs = Math.min(timeout ?? 300, 600) * 1e3;
|
|
136
|
+
const pollIntervalMs = 2e3;
|
|
137
|
+
const start = Date.now();
|
|
138
|
+
while (Date.now() - start < maxWaitMs) {
|
|
139
|
+
const data2 = readReviewResult();
|
|
140
|
+
if (data2) {
|
|
141
|
+
consumeReviewResult();
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: JSON.stringify(data2.result, null, 2)
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: "No review result received within timeout."
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
131
162
|
const data = readReviewResult();
|
|
132
163
|
if (!data) {
|
|
133
164
|
return {
|