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.
@@ -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(path5) {
515
- return TEST_PATTERNS.some((re) => re.test(path5));
540
+ function isTestFile(path6) {
541
+ return TEST_PATTERNS.some((re) => re.test(path6));
516
542
  }
517
- function isNonCodeFile(path5) {
518
- const ext = path5.slice(path5.lastIndexOf("."));
543
+ function isNonCodeFile(path6) {
544
+ const ext = path6.slice(path6.lastIndexOf("."));
519
545
  return NON_CODE_EXTENSIONS.has(ext);
520
546
  }
521
- function isConfigFile(path5) {
522
- return CONFIG_PATTERNS.some((re) => re.test(path5));
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 wss = new WebSocketServer({ port });
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
- wss.on("connection", (ws) => {
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 wss.clients) {
790
+ for (const ws of wss2.clients) {
765
791
  ws.close();
766
792
  }
767
- wss.close();
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 wss = new WebSocketServer2({ server: httpServer });
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
- wss.on("connection", (ws) => {
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 wss.clients) {
1185
+ for (const ws of wss2.clients) {
1160
1186
  ws.close();
1161
1187
  }
1162
- wss.close();
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((f) => f.path);
1207
+ return newDiffSet.files.map(fileKey);
1179
1208
  }
1180
1209
  const oldFiles = new Map(
1181
- oldDiffSet.files.map((f) => [f.path, 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 oldFile = oldFiles.get(newFile.path);
1214
+ const key = fileKey(newFile);
1215
+ const oldFile = oldFiles.get(key);
1186
1216
  if (!oldFile) {
1187
- changed.push(newFile.path);
1217
+ changed.push(key);
1188
1218
  } else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
1189
- changed.push(newFile.path);
1219
+ changed.push(key);
1190
1220
  }
1191
1221
  }
1192
1222
  for (const oldFile of oldDiffSet.files) {
1193
- if (!newDiffSet.files.some((f) => f.path === oldFile.path)) {
1194
- changed.push(oldFile.path);
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
  };
@@ -3,7 +3,7 @@ import {
3
3
  readReviewResult,
4
4
  readWatchFile,
5
5
  startReview
6
- } from "./chunk-QB2PKDLU.js";
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.13.8" : "0.0.0-dev"
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
- async () => {
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.13.8",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {