diffprism 0.33.0 → 0.34.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
@@ -13,7 +13,7 @@ import {
13
13
  startReview,
14
14
  startWatch,
15
15
  submitGitHubReview
16
- } from "./chunk-ZLIUNVTW.js";
16
+ } from "./chunk-VASCXEMN.js";
17
17
 
18
18
  // cli/src/index.ts
19
19
  import { Command } from "commander";
@@ -1001,7 +1001,7 @@ async function serverStop() {
1001
1001
 
1002
1002
  // cli/src/index.ts
1003
1003
  var program = new Command();
1004
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.33.0" : "0.0.0-dev");
1004
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.34.1" : "0.0.0-dev");
1005
1005
  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);
1006
1006
  program.command("review-pr <pr>").description("Review a GitHub pull request in DiffPrism").option("-t, --title <title>", "Override review title").option("--reasoning <text>", "Agent reasoning about the PR").option("--dev", "Use Vite dev server with HMR instead of static files").option("--post-to-github", "Automatically post review back to GitHub without prompting").action(reviewPr);
1007
1007
  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);
@@ -1155,6 +1155,45 @@ function consumeReviewResult(cwd) {
1155
1155
  import getPort from "get-port";
1156
1156
  import open from "open";
1157
1157
 
1158
+ // packages/core/src/review-history.ts
1159
+ import fs2 from "fs";
1160
+ import path4 from "path";
1161
+ import { randomUUID } from "crypto";
1162
+ function generateEntryId() {
1163
+ return randomUUID();
1164
+ }
1165
+ function getHistoryPath(projectDir) {
1166
+ return path4.join(projectDir, ".diffprism", "history", "reviews.json");
1167
+ }
1168
+ function readHistory(projectDir) {
1169
+ const filePath = getHistoryPath(projectDir);
1170
+ if (!fs2.existsSync(filePath)) {
1171
+ return { version: 1, entries: [] };
1172
+ }
1173
+ try {
1174
+ const raw = fs2.readFileSync(filePath, "utf-8");
1175
+ const parsed = JSON.parse(raw);
1176
+ return parsed;
1177
+ } catch {
1178
+ return { version: 1, entries: [] };
1179
+ }
1180
+ }
1181
+ function appendHistory(projectDir, entry) {
1182
+ const filePath = getHistoryPath(projectDir);
1183
+ const dir = path4.dirname(filePath);
1184
+ if (!fs2.existsSync(dir)) {
1185
+ fs2.mkdirSync(dir, { recursive: true });
1186
+ }
1187
+ const history = readHistory(projectDir);
1188
+ history.entries.push(entry);
1189
+ history.entries.sort((a, b) => a.timestamp - b.timestamp);
1190
+ fs2.writeFileSync(filePath, JSON.stringify(history, null, 2) + "\n");
1191
+ }
1192
+ function getRecentHistory(projectDir, limit = 50) {
1193
+ const history = readHistory(projectDir);
1194
+ return history.entries.slice(-limit);
1195
+ }
1196
+
1158
1197
  // packages/core/src/watch-bridge.ts
1159
1198
  import http from "http";
1160
1199
  import { WebSocketServer, WebSocket } from "ws";
@@ -1478,8 +1517,8 @@ function updateSession(id, update) {
1478
1517
 
1479
1518
  // packages/core/src/ui-server.ts
1480
1519
  import http2 from "http";
1481
- import fs2 from "fs";
1482
- import path4 from "path";
1520
+ import fs3 from "fs";
1521
+ import path5 from "path";
1483
1522
  import { fileURLToPath } from "url";
1484
1523
  var MIME_TYPES = {
1485
1524
  ".html": "text/html",
@@ -1494,14 +1533,14 @@ var MIME_TYPES = {
1494
1533
  };
1495
1534
  function resolveUiDist() {
1496
1535
  const thisFile = fileURLToPath(import.meta.url);
1497
- const thisDir = path4.dirname(thisFile);
1498
- const publishedUiDist = path4.resolve(thisDir, "..", "ui-dist");
1499
- if (fs2.existsSync(path4.join(publishedUiDist, "index.html"))) {
1536
+ const thisDir = path5.dirname(thisFile);
1537
+ const publishedUiDist = path5.resolve(thisDir, "..", "ui-dist");
1538
+ if (fs3.existsSync(path5.join(publishedUiDist, "index.html"))) {
1500
1539
  return publishedUiDist;
1501
1540
  }
1502
- const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
1503
- const devUiDist = path4.join(workspaceRoot, "packages", "ui", "dist");
1504
- if (fs2.existsSync(path4.join(devUiDist, "index.html"))) {
1541
+ const workspaceRoot = path5.resolve(thisDir, "..", "..", "..");
1542
+ const devUiDist = path5.join(workspaceRoot, "packages", "ui", "dist");
1543
+ if (fs3.existsSync(path5.join(devUiDist, "index.html"))) {
1505
1544
  return devUiDist;
1506
1545
  }
1507
1546
  throw new Error(
@@ -1510,10 +1549,10 @@ function resolveUiDist() {
1510
1549
  }
1511
1550
  function resolveUiRoot() {
1512
1551
  const thisFile = fileURLToPath(import.meta.url);
1513
- const thisDir = path4.dirname(thisFile);
1514
- const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
1515
- const uiRoot = path4.join(workspaceRoot, "packages", "ui");
1516
- if (fs2.existsSync(path4.join(uiRoot, "index.html"))) {
1552
+ const thisDir = path5.dirname(thisFile);
1553
+ const workspaceRoot = path5.resolve(thisDir, "..", "..", "..");
1554
+ const uiRoot = path5.join(workspaceRoot, "packages", "ui");
1555
+ if (fs3.existsSync(path5.join(uiRoot, "index.html"))) {
1517
1556
  return uiRoot;
1518
1557
  }
1519
1558
  throw new Error(
@@ -1533,14 +1572,14 @@ async function startViteDevServer(uiRoot, port, silent) {
1533
1572
  function createStaticServer(distPath, port) {
1534
1573
  const server = http2.createServer((req, res) => {
1535
1574
  const urlPath = req.url?.split("?")[0] ?? "/";
1536
- let filePath = path4.join(distPath, urlPath === "/" ? "index.html" : urlPath);
1537
- if (!fs2.existsSync(filePath)) {
1538
- filePath = path4.join(distPath, "index.html");
1575
+ let filePath = path5.join(distPath, urlPath === "/" ? "index.html" : urlPath);
1576
+ if (!fs3.existsSync(filePath)) {
1577
+ filePath = path5.join(distPath, "index.html");
1539
1578
  }
1540
- const ext = path4.extname(filePath);
1579
+ const ext = path5.extname(filePath);
1541
1580
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
1542
1581
  try {
1543
- const content = fs2.readFileSync(filePath);
1582
+ const content = fs3.readFileSync(filePath);
1544
1583
  res.writeHead(200, { "Content-Type": contentType });
1545
1584
  res.end(content);
1546
1585
  } catch {
@@ -1737,6 +1776,24 @@ DiffPrism Review: ${title ?? briefing.summary}`);
1737
1776
  const result = await bridge.waitForResult();
1738
1777
  poller?.stop();
1739
1778
  updateSession(session.id, { status: "completed", result });
1779
+ try {
1780
+ const projectDir = cwd ?? process.cwd();
1781
+ const entry = {
1782
+ id: generateEntryId(),
1783
+ timestamp: Date.now(),
1784
+ diffRef,
1785
+ decision: result.decision,
1786
+ filesReviewed: diffSet.files.length,
1787
+ additions: diffSet.files.reduce((sum, f) => sum + f.additions, 0),
1788
+ deletions: diffSet.files.reduce((sum, f) => sum + f.deletions, 0),
1789
+ commentCount: result.comments.length,
1790
+ branch: metadata.currentBranch,
1791
+ title: metadata.title,
1792
+ summary: result.summary ?? briefing.summary
1793
+ };
1794
+ appendHistory(projectDir, entry);
1795
+ } catch {
1796
+ }
1740
1797
  return result;
1741
1798
  } finally {
1742
1799
  poller?.stop();
@@ -1752,14 +1809,14 @@ DiffPrism Review: ${title ?? briefing.summary}`);
1752
1809
  }
1753
1810
 
1754
1811
  // packages/core/src/server-file.ts
1755
- import fs3 from "fs";
1756
- import path5 from "path";
1812
+ import fs4 from "fs";
1813
+ import path6 from "path";
1757
1814
  import os from "os";
1758
1815
  function serverDir() {
1759
- return path5.join(os.homedir(), ".diffprism");
1816
+ return path6.join(os.homedir(), ".diffprism");
1760
1817
  }
1761
1818
  function serverFilePath() {
1762
- return path5.join(serverDir(), "server.json");
1819
+ return path6.join(serverDir(), "server.json");
1763
1820
  }
1764
1821
  function isPidAlive2(pid) {
1765
1822
  try {
@@ -1771,21 +1828,21 @@ function isPidAlive2(pid) {
1771
1828
  }
1772
1829
  function writeServerFile(info) {
1773
1830
  const dir = serverDir();
1774
- if (!fs3.existsSync(dir)) {
1775
- fs3.mkdirSync(dir, { recursive: true });
1831
+ if (!fs4.existsSync(dir)) {
1832
+ fs4.mkdirSync(dir, { recursive: true });
1776
1833
  }
1777
- fs3.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
1834
+ fs4.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
1778
1835
  }
1779
1836
  function readServerFile() {
1780
1837
  const filePath = serverFilePath();
1781
- if (!fs3.existsSync(filePath)) {
1838
+ if (!fs4.existsSync(filePath)) {
1782
1839
  return null;
1783
1840
  }
1784
1841
  try {
1785
- const raw = fs3.readFileSync(filePath, "utf-8");
1842
+ const raw = fs4.readFileSync(filePath, "utf-8");
1786
1843
  const info = JSON.parse(raw);
1787
1844
  if (!isPidAlive2(info.pid)) {
1788
- fs3.unlinkSync(filePath);
1845
+ fs4.unlinkSync(filePath);
1789
1846
  return null;
1790
1847
  }
1791
1848
  return info;
@@ -1796,8 +1853,8 @@ function readServerFile() {
1796
1853
  function removeServerFile() {
1797
1854
  try {
1798
1855
  const filePath = serverFilePath();
1799
- if (fs3.existsSync(filePath)) {
1800
- fs3.unlinkSync(filePath);
1856
+ if (fs4.existsSync(filePath)) {
1857
+ fs4.unlinkSync(filePath);
1801
1858
  }
1802
1859
  } catch {
1803
1860
  }
@@ -1998,7 +2055,7 @@ Review submitted: ${result.decision}`);
1998
2055
 
1999
2056
  // packages/core/src/global-server.ts
2000
2057
  import http3 from "http";
2001
- import { randomUUID } from "crypto";
2058
+ import { randomUUID as randomUUID2 } from "crypto";
2002
2059
  import getPort3 from "get-port";
2003
2060
  import open3 from "open";
2004
2061
  import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
@@ -2171,6 +2228,27 @@ function broadcastSessionList() {
2171
2228
  const summaries = Array.from(sessions2.values()).map(toSummary);
2172
2229
  broadcastToAll({ type: "session:list", payload: summaries });
2173
2230
  }
2231
+ function recordReviewHistory(session, result) {
2232
+ if (session.projectPath.startsWith("github:")) return;
2233
+ try {
2234
+ const { payload } = session;
2235
+ const entry = {
2236
+ id: generateEntryId(),
2237
+ timestamp: Date.now(),
2238
+ diffRef: session.diffRef ?? "unknown",
2239
+ decision: result.decision,
2240
+ filesReviewed: payload.diffSet.files.length,
2241
+ additions: payload.diffSet.files.reduce((sum, f) => sum + f.additions, 0),
2242
+ deletions: payload.diffSet.files.reduce((sum, f) => sum + f.deletions, 0),
2243
+ commentCount: result.comments.length,
2244
+ branch: payload.metadata.currentBranch,
2245
+ title: payload.metadata.title,
2246
+ summary: result.summary ?? payload.briefing.summary
2247
+ };
2248
+ appendHistory(session.projectPath, entry);
2249
+ } catch {
2250
+ }
2251
+ }
2174
2252
  async function handleApiRequest(req, res) {
2175
2253
  const method = req.method ?? "GET";
2176
2254
  const url = (req.url ?? "/").split("?")[0];
@@ -2234,7 +2312,7 @@ async function handleApiRequest(req, res) {
2234
2312
  reopenBrowserIfNeeded?.();
2235
2313
  jsonResponse(res, 200, { sessionId });
2236
2314
  } else {
2237
- const sessionId = `session-${randomUUID().slice(0, 8)}`;
2315
+ const sessionId = `session-${randomUUID2().slice(0, 8)}`;
2238
2316
  payload.reviewId = sessionId;
2239
2317
  if (diffRef) {
2240
2318
  payload.watchMode = true;
@@ -2295,6 +2373,7 @@ async function handleApiRequest(req, res) {
2295
2373
  const result = JSON.parse(body);
2296
2374
  session.result = result;
2297
2375
  session.status = "submitted";
2376
+ recordReviewHistory(session, result);
2298
2377
  if (result.decision === "dismissed") {
2299
2378
  broadcastSessionRemoved(postResultParams.id);
2300
2379
  } else {
@@ -2360,7 +2439,7 @@ async function handleApiRequest(req, res) {
2360
2439
  const body = await readBody(req);
2361
2440
  const { file, line, body: annotationBody, type, confidence, category, source } = JSON.parse(body);
2362
2441
  const annotation = {
2363
- id: randomUUID(),
2442
+ id: randomUUID2(),
2364
2443
  sessionId: session.id,
2365
2444
  file,
2366
2445
  line,
@@ -2405,6 +2484,10 @@ async function handleApiRequest(req, res) {
2405
2484
  return true;
2406
2485
  }
2407
2486
  annotation.dismissed = true;
2487
+ sendToSessionClients(dismissAnnotationParams.id, {
2488
+ type: "annotation:dismissed",
2489
+ payload: { annotationId: dismissAnnotationParams.annotationId }
2490
+ });
2408
2491
  jsonResponse(res, 200, { ok: true });
2409
2492
  return true;
2410
2493
  }
@@ -2426,6 +2509,10 @@ async function handleApiRequest(req, res) {
2426
2509
  jsonResponse(res, 404, { error: "Session not found" });
2427
2510
  return true;
2428
2511
  }
2512
+ if (session.projectPath.startsWith("github:")) {
2513
+ jsonResponse(res, 400, { error: "Ref listing not available for GitHub PRs" });
2514
+ return true;
2515
+ }
2429
2516
  try {
2430
2517
  const branches = listBranches({ cwd: session.projectPath });
2431
2518
  const commits = listCommits({ cwd: session.projectPath });
@@ -2443,6 +2530,10 @@ async function handleApiRequest(req, res) {
2443
2530
  jsonResponse(res, 404, { error: "Session not found" });
2444
2531
  return true;
2445
2532
  }
2533
+ if (session.projectPath.startsWith("github:")) {
2534
+ jsonResponse(res, 400, { error: "Ref comparison not available for GitHub PRs" });
2535
+ return true;
2536
+ }
2446
2537
  try {
2447
2538
  const body = await readBody(req);
2448
2539
  const { ref } = JSON.parse(body);
@@ -2484,6 +2575,38 @@ async function handleApiRequest(req, res) {
2484
2575
  }
2485
2576
  return true;
2486
2577
  }
2578
+ const getSessionHistoryParams = matchRoute(method, url, "GET", "/api/reviews/:id/history");
2579
+ if (getSessionHistoryParams) {
2580
+ const session = sessions2.get(getSessionHistoryParams.id);
2581
+ if (!session) {
2582
+ jsonResponse(res, 404, { error: "Session not found" });
2583
+ return true;
2584
+ }
2585
+ if (session.projectPath.startsWith("github:")) {
2586
+ jsonResponse(res, 200, { history: [] });
2587
+ return true;
2588
+ }
2589
+ const history = getRecentHistory(session.projectPath);
2590
+ jsonResponse(res, 200, { history });
2591
+ return true;
2592
+ }
2593
+ if (method === "GET" && req.url) {
2594
+ const parsedUrl = new URL(req.url, "http://localhost");
2595
+ if (parsedUrl.pathname === "/api/history") {
2596
+ const projectPath = parsedUrl.searchParams.get("project");
2597
+ if (!projectPath) {
2598
+ jsonResponse(res, 400, { error: "Missing required query parameter: project" });
2599
+ return true;
2600
+ }
2601
+ if (projectPath.startsWith("github:")) {
2602
+ jsonResponse(res, 200, { history: [] });
2603
+ return true;
2604
+ }
2605
+ const history = getRecentHistory(projectPath);
2606
+ jsonResponse(res, 200, { history });
2607
+ return true;
2608
+ }
2609
+ }
2487
2610
  jsonResponse(res, 404, { error: "Not found" });
2488
2611
  return true;
2489
2612
  }
@@ -2580,6 +2703,7 @@ async function startGlobalServer(options = {}) {
2580
2703
  if (session) {
2581
2704
  session.result = msg.payload;
2582
2705
  session.status = "submitted";
2706
+ recordReviewHistory(session, msg.payload);
2583
2707
  if (msg.payload.decision === "dismissed") {
2584
2708
  broadcastSessionRemoved(sid);
2585
2709
  } else {
@@ -2738,11 +2862,6 @@ Waiting for reviews...
2738
2862
  return { httpPort, wsPort, stop };
2739
2863
  }
2740
2864
 
2741
- // packages/core/src/review-history.ts
2742
- import fs4 from "fs";
2743
- import path6 from "path";
2744
- import { randomUUID as randomUUID2 } from "crypto";
2745
-
2746
2865
  // packages/github/src/auth.ts
2747
2866
  import { execSync as execSync3 } from "child_process";
2748
2867
  import fs5 from "fs";
@@ -15,7 +15,7 @@ import {
15
15
  resolveGitHubToken,
16
16
  startReview,
17
17
  submitGitHubReview
18
- } from "./chunk-ZLIUNVTW.js";
18
+ } from "./chunk-VASCXEMN.js";
19
19
 
20
20
  // packages/mcp-server/src/index.ts
21
21
  import fs from "fs";
@@ -113,11 +113,11 @@ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
113
113
  async function startMcpServer() {
114
114
  const server = new McpServer({
115
115
  name: "diffprism",
116
- version: true ? "0.33.0" : "0.0.0-dev"
116
+ version: true ? "0.34.1" : "0.0.0-dev"
117
117
  });
118
118
  server.tool(
119
119
  "open_review",
120
- "Open a browser-based code review for local git changes. Blocks until the engineer submits their review decision.",
120
+ "Open a browser-based code review for local git changes. Blocks until the engineer submits their review decision. The result may include a `postReviewAction` field ('commit' or 'commit_and_pr') if the reviewer requested a post-review action.",
121
121
  {
122
122
  diff_ref: z.string().describe(
123
123
  'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), or a ref range like "HEAD~3..HEAD"'
@@ -781,7 +781,7 @@ async function startMcpServer() {
781
781
  );
782
782
  server.tool(
783
783
  "review_pr",
784
- "Open a browser-based code review for a GitHub pull request. Fetches the PR diff, runs DiffPrism analysis, and opens the review UI. Blocks until the engineer submits their review decision. Optionally posts the review back to GitHub.",
784
+ "Open a browser-based code review for a GitHub pull request. Fetches the PR diff, runs DiffPrism analysis, and opens the review UI. Blocks until the engineer submits their review decision. Optionally posts the review back to GitHub. The result may include a `postReviewAction` field ('commit' or 'commit_and_pr') if the reviewer requested a post-review action.",
785
785
  {
786
786
  pr: z.string().describe(
787
787
  'GitHub PR reference: "owner/repo#123" or "https://github.com/owner/repo/pull/123"'
@@ -867,7 +867,7 @@ async function startMcpServer() {
867
867
  injectedPayload: payload
868
868
  });
869
869
  }
870
- if (post_to_github && result.decision !== "dismissed") {
870
+ if ((post_to_github || result.postToGithub) && result.decision !== "dismissed") {
871
871
  const posted = await submitGitHubReview(client, owner, repo, number, result);
872
872
  if (posted) {
873
873
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {