diffprism 0.20.0 → 0.20.1

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 CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  startGlobalServer,
7
7
  startReview,
8
8
  startWatch
9
- } from "./chunk-4VXA6GCO.js";
9
+ } from "./chunk-L2D5SDUV.js";
10
10
 
11
11
  // cli/src/index.ts
12
12
  import { Command } from "commander";
@@ -144,6 +144,12 @@ You can also check for feedback without blocking by calling \`get_review_result\
144
144
  `;
145
145
 
146
146
  // cli/src/commands/setup.ts
147
+ var GITIGNORE_ENTRIES = [
148
+ ".diffprism",
149
+ ".mcp.json",
150
+ ".claude/settings.json",
151
+ ".claude/skills/review/"
152
+ ];
147
153
  function findGitRoot(from) {
148
154
  let dir = path.resolve(from);
149
155
  while (true) {
@@ -306,27 +312,28 @@ async function promptUser(question) {
306
312
  }
307
313
  async function setupGitignore(gitRoot) {
308
314
  const filePath = path.join(gitRoot, ".gitignore");
309
- const entry = ".diffprism";
310
315
  if (fs.existsSync(filePath)) {
311
316
  const content = fs.readFileSync(filePath, "utf-8");
312
317
  const lines = content.split("\n").map((l) => l.trim());
313
- if (lines.includes(entry)) {
318
+ const missing = GITIGNORE_ENTRIES.filter((e) => !lines.includes(e));
319
+ if (missing.length === 0) {
314
320
  return { action: "skipped", filePath };
315
321
  }
316
- const newContent = content.endsWith("\n") ? content + entry + "\n" : content + "\n" + entry + "\n";
322
+ const suffix = missing.map((e) => e + "\n").join("");
323
+ const newContent = content.endsWith("\n") ? content + suffix : content + "\n" + suffix;
317
324
  fs.writeFileSync(filePath, newContent);
318
325
  return { action: "updated", filePath };
319
326
  }
320
327
  const confirmed = await promptUser(
321
- "No .gitignore found. Create one with .diffprism entry? (Y/n) "
328
+ "No .gitignore found. Create one with DiffPrism entries? (Y/n) "
322
329
  );
323
330
  if (!confirmed) {
324
331
  console.log(
325
- " Warning: .diffprism directory will appear in git status and may trigger watch-mode loops."
332
+ " Warning: DiffPrism files will appear in git status and may be accidentally committed."
326
333
  );
327
334
  return { action: "skipped", filePath };
328
335
  }
329
- fs.writeFileSync(filePath, entry + "\n");
336
+ fs.writeFileSync(filePath, GITIGNORE_ENTRIES.map((e) => e + "\n").join(""));
330
337
  return { action: "created", filePath };
331
338
  }
332
339
  async function setup(flags) {
@@ -534,7 +541,8 @@ function teardownGitignore(gitRoot) {
534
541
  }
535
542
  const content = fs2.readFileSync(filePath, "utf-8");
536
543
  const lines = content.split("\n");
537
- const filtered = lines.filter((l) => l.trim() !== ".diffprism");
544
+ const entrySet = new Set(GITIGNORE_ENTRIES);
545
+ const filtered = lines.filter((l) => !entrySet.has(l.trim()));
538
546
  if (filtered.length === lines.length) {
539
547
  return { action: "skipped", filePath };
540
548
  }
@@ -824,7 +832,7 @@ async function serverStop() {
824
832
 
825
833
  // cli/src/index.ts
826
834
  var program = new Command();
827
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.20.0" : "0.0.0-dev");
835
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.20.1" : "0.0.0-dev");
828
836
  program.command("review [ref]").description("Open a browser-based diff review").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--dev", "Use Vite dev server with HMR instead of static files").action(review);
829
837
  program.command("start [ref]").description("Set up DiffPrism and start watching for changes").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(start);
830
838
  program.command("watch [ref]").description("Start a persistent diff watcher with live-updating browser UI").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").action(watch);
@@ -1132,7 +1132,6 @@ async function isServerAlive() {
1132
1132
  }
1133
1133
 
1134
1134
  // packages/core/src/watch.ts
1135
- import { createHash } from "crypto";
1136
1135
  import getPort2 from "get-port";
1137
1136
  import open2 from "open";
1138
1137
 
@@ -1265,7 +1264,8 @@ function createWatchBridge(port, callbacks) {
1265
1264
  });
1266
1265
  }
1267
1266
 
1268
- // packages/core/src/watch.ts
1267
+ // packages/core/src/diff-utils.ts
1268
+ import { createHash } from "crypto";
1269
1269
  function hashDiff(rawDiff) {
1270
1270
  return createHash("sha256").update(rawDiff).digest("hex");
1271
1271
  }
@@ -1296,6 +1296,8 @@ function detectChangedFiles(oldDiffSet, newDiffSet) {
1296
1296
  }
1297
1297
  return changed;
1298
1298
  }
1299
+
1300
+ // packages/core/src/watch.ts
1299
1301
  async function startWatch(options) {
1300
1302
  const {
1301
1303
  diffRef,
@@ -1447,6 +1449,8 @@ import open3 from "open";
1447
1449
  import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
1448
1450
  var sessions2 = /* @__PURE__ */ new Map();
1449
1451
  var clientSessions = /* @__PURE__ */ new Map();
1452
+ var sessionWatchers = /* @__PURE__ */ new Map();
1453
+ var serverPollInterval = 2e3;
1450
1454
  var reopenBrowserIfNeeded = null;
1451
1455
  function toSummary(session) {
1452
1456
  const { payload } = session;
@@ -1466,7 +1470,8 @@ function toSummary(session) {
1466
1470
  additions,
1467
1471
  deletions,
1468
1472
  status: session.status,
1469
- createdAt: session.createdAt
1473
+ createdAt: session.createdAt,
1474
+ hasNewChanges: session.hasNewChanges
1470
1475
  };
1471
1476
  }
1472
1477
  function readBody(req) {
@@ -1517,6 +1522,93 @@ function sendToSessionClients(sessionId, msg) {
1517
1522
  }
1518
1523
  }
1519
1524
  }
1525
+ function hasViewersForSession(sessionId) {
1526
+ for (const [client, sid] of clientSessions.entries()) {
1527
+ if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
1528
+ return true;
1529
+ }
1530
+ }
1531
+ return false;
1532
+ }
1533
+ function startSessionWatcher(sessionId) {
1534
+ if (sessionWatchers.has(sessionId)) return;
1535
+ const session = sessions2.get(sessionId);
1536
+ if (!session?.diffRef) return;
1537
+ const interval = setInterval(() => {
1538
+ const s = sessions2.get(sessionId);
1539
+ if (!s?.diffRef) {
1540
+ stopSessionWatcher(sessionId);
1541
+ return;
1542
+ }
1543
+ try {
1544
+ const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(s.diffRef, {
1545
+ cwd: s.projectPath
1546
+ });
1547
+ const newHash = hashDiff(newRawDiff);
1548
+ if (newHash !== s.lastDiffHash) {
1549
+ const newBriefing = analyze(newDiffSet);
1550
+ const changedFiles = detectChangedFiles(s.lastDiffSet ?? null, newDiffSet);
1551
+ s.payload = {
1552
+ ...s.payload,
1553
+ diffSet: newDiffSet,
1554
+ rawDiff: newRawDiff,
1555
+ briefing: newBriefing
1556
+ };
1557
+ s.lastDiffHash = newHash;
1558
+ s.lastDiffSet = newDiffSet;
1559
+ if (hasViewersForSession(sessionId)) {
1560
+ sendToSessionClients(sessionId, {
1561
+ type: "diff:update",
1562
+ payload: {
1563
+ diffSet: newDiffSet,
1564
+ rawDiff: newRawDiff,
1565
+ briefing: newBriefing,
1566
+ changedFiles,
1567
+ timestamp: Date.now()
1568
+ }
1569
+ });
1570
+ s.hasNewChanges = false;
1571
+ } else {
1572
+ s.hasNewChanges = true;
1573
+ broadcastSessionList();
1574
+ }
1575
+ }
1576
+ } catch {
1577
+ }
1578
+ }, serverPollInterval);
1579
+ sessionWatchers.set(sessionId, interval);
1580
+ }
1581
+ function stopSessionWatcher(sessionId) {
1582
+ const interval = sessionWatchers.get(sessionId);
1583
+ if (interval) {
1584
+ clearInterval(interval);
1585
+ sessionWatchers.delete(sessionId);
1586
+ }
1587
+ }
1588
+ function startAllWatchers() {
1589
+ for (const [id, session] of sessions2.entries()) {
1590
+ if (session.diffRef && !sessionWatchers.has(id)) {
1591
+ startSessionWatcher(id);
1592
+ }
1593
+ }
1594
+ }
1595
+ function stopAllWatchers() {
1596
+ for (const [id, interval] of sessionWatchers.entries()) {
1597
+ clearInterval(interval);
1598
+ }
1599
+ sessionWatchers.clear();
1600
+ }
1601
+ function hasConnectedClients() {
1602
+ if (!wss) return false;
1603
+ for (const client of wss.clients) {
1604
+ if (client.readyState === WebSocket3.OPEN) return true;
1605
+ }
1606
+ return false;
1607
+ }
1608
+ function broadcastSessionList() {
1609
+ const summaries = Array.from(sessions2.values()).map(toSummary);
1610
+ broadcastToAll({ type: "session:list", payload: summaries });
1611
+ }
1520
1612
  async function handleApiRequest(req, res) {
1521
1613
  const method = req.method ?? "GET";
1522
1614
  const url = (req.url ?? "/").split("?")[0];
@@ -1543,18 +1635,28 @@ async function handleApiRequest(req, res) {
1543
1635
  if (method === "POST" && url === "/api/reviews") {
1544
1636
  try {
1545
1637
  const body = await readBody(req);
1546
- const { payload, projectPath } = JSON.parse(body);
1638
+ const { payload, projectPath, diffRef } = JSON.parse(body);
1547
1639
  const sessionId = `session-${randomUUID().slice(0, 8)}`;
1548
1640
  payload.reviewId = sessionId;
1641
+ if (diffRef) {
1642
+ payload.watchMode = true;
1643
+ }
1549
1644
  const session = {
1550
1645
  id: sessionId,
1551
1646
  payload,
1552
1647
  projectPath,
1553
1648
  status: "pending",
1554
1649
  createdAt: Date.now(),
1555
- result: null
1650
+ result: null,
1651
+ diffRef,
1652
+ lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
1653
+ lastDiffSet: diffRef ? payload.diffSet : void 0,
1654
+ hasNewChanges: false
1556
1655
  };
1557
1656
  sessions2.set(sessionId, session);
1657
+ if (diffRef && hasConnectedClients()) {
1658
+ startSessionWatcher(sessionId);
1659
+ }
1558
1660
  broadcastToAll({
1559
1661
  type: "session:added",
1560
1662
  payload: toSummary(session)
@@ -1644,6 +1746,7 @@ async function handleApiRequest(req, res) {
1644
1746
  }
1645
1747
  const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
1646
1748
  if (deleteParams) {
1749
+ stopSessionWatcher(deleteParams.id);
1647
1750
  if (sessions2.delete(deleteParams.id)) {
1648
1751
  jsonResponse(res, 200, { ok: true });
1649
1752
  } else {
@@ -1659,8 +1762,10 @@ async function startGlobalServer(options = {}) {
1659
1762
  httpPort: preferredHttpPort = 24680,
1660
1763
  wsPort: preferredWsPort = 24681,
1661
1764
  silent = false,
1662
- dev = false
1765
+ dev = false,
1766
+ pollInterval = 2e3
1663
1767
  } = options;
1768
+ serverPollInterval = pollInterval;
1664
1769
  const [httpPort, wsPort] = await Promise.all([
1665
1770
  getPort3({ port: preferredHttpPort }),
1666
1771
  getPort3({ port: preferredWsPort })
@@ -1686,6 +1791,7 @@ async function startGlobalServer(options = {}) {
1686
1791
  });
1687
1792
  wss = new WebSocketServer3({ port: wsPort });
1688
1793
  wss.on("connection", (ws, req) => {
1794
+ startAllWatchers();
1689
1795
  const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
1690
1796
  const sessionId = url.searchParams.get("sessionId");
1691
1797
  if (sessionId) {
@@ -1693,6 +1799,7 @@ async function startGlobalServer(options = {}) {
1693
1799
  const session = sessions2.get(sessionId);
1694
1800
  if (session) {
1695
1801
  session.status = "in_review";
1802
+ session.hasNewChanges = false;
1696
1803
  const msg = {
1697
1804
  type: "review:init",
1698
1805
  payload: session.payload
@@ -1711,6 +1818,7 @@ async function startGlobalServer(options = {}) {
1711
1818
  if (session) {
1712
1819
  clientSessions.set(ws, session.id);
1713
1820
  session.status = "in_review";
1821
+ session.hasNewChanges = false;
1714
1822
  ws.send(JSON.stringify({
1715
1823
  type: "review:init",
1716
1824
  payload: session.payload
@@ -1735,17 +1843,32 @@ async function startGlobalServer(options = {}) {
1735
1843
  if (session) {
1736
1844
  clientSessions.set(ws, session.id);
1737
1845
  session.status = "in_review";
1846
+ session.hasNewChanges = false;
1847
+ startSessionWatcher(session.id);
1738
1848
  ws.send(JSON.stringify({
1739
1849
  type: "review:init",
1740
1850
  payload: session.payload
1741
1851
  }));
1742
1852
  }
1853
+ } else if (msg.type === "session:close") {
1854
+ const closedId = msg.payload.sessionId;
1855
+ stopSessionWatcher(closedId);
1856
+ sessions2.delete(closedId);
1857
+ for (const [client, sid] of clientSessions.entries()) {
1858
+ if (sid === closedId) {
1859
+ clientSessions.delete(client);
1860
+ }
1861
+ }
1862
+ broadcastSessionList();
1743
1863
  }
1744
1864
  } catch {
1745
1865
  }
1746
1866
  });
1747
1867
  ws.on("close", () => {
1748
1868
  clientSessions.delete(ws);
1869
+ if (!hasConnectedClients()) {
1870
+ stopAllWatchers();
1871
+ }
1749
1872
  });
1750
1873
  });
1751
1874
  await new Promise((resolve, reject) => {
@@ -1772,19 +1895,13 @@ Waiting for reviews...
1772
1895
  }
1773
1896
  const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&serverMode=true`;
1774
1897
  await open3(uiUrl);
1775
- function hasConnectedClients() {
1776
- if (!wss) return false;
1777
- for (const client of wss.clients) {
1778
- if (client.readyState === WebSocket3.OPEN) return true;
1779
- }
1780
- return false;
1781
- }
1782
1898
  reopenBrowserIfNeeded = () => {
1783
1899
  if (!hasConnectedClients()) {
1784
1900
  open3(uiUrl);
1785
1901
  }
1786
1902
  };
1787
1903
  async function stop() {
1904
+ stopAllWatchers();
1788
1905
  if (wss) {
1789
1906
  for (const client of wss.clients) {
1790
1907
  client.close();
@@ -7,7 +7,7 @@ import {
7
7
  readReviewResult,
8
8
  readWatchFile,
9
9
  startReview
10
- } from "./chunk-4VXA6GCO.js";
10
+ } from "./chunk-L2D5SDUV.js";
11
11
 
12
12
  // packages/mcp-server/src/index.ts
13
13
  import fs from "fs";
@@ -47,7 +47,7 @@ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
47
47
  {
48
48
  method: "POST",
49
49
  headers: { "Content-Type": "application/json" },
50
- body: JSON.stringify({ payload, projectPath: cwd })
50
+ body: JSON.stringify({ payload, projectPath: cwd, diffRef })
51
51
  }
52
52
  );
53
53
  if (!createResponse.ok) {
@@ -76,7 +76,7 @@ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
76
76
  async function startMcpServer() {
77
77
  const server = new McpServer({
78
78
  name: "diffprism",
79
- version: true ? "0.20.0" : "0.0.0-dev"
79
+ version: true ? "0.20.1" : "0.0.0-dev"
80
80
  });
81
81
  server.tool(
82
82
  "open_review",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {