diffprism 0.15.0 → 0.16.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 CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  startGlobalServer,
7
7
  startReview,
8
8
  startWatch
9
- } from "./chunk-NGHUHDAM.js";
9
+ } from "./chunk-4WN4FIY4.js";
10
10
 
11
11
  // cli/src/index.ts
12
12
  import { Command } from "commander";
@@ -578,7 +578,7 @@ async function serverStop() {
578
578
 
579
579
  // cli/src/index.ts
580
580
  var program = new Command();
581
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.15.0" : "0.0.0-dev");
581
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.16.0" : "0.0.0-dev");
582
582
  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);
583
583
  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);
584
584
  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);
@@ -1,7 +1,3 @@
1
- // packages/core/src/pipeline.ts
2
- import getPort from "get-port";
3
- import open from "open";
4
-
5
1
  // packages/git/src/local.ts
6
2
  import { execSync } from "child_process";
7
3
  import { readFileSync } from "fs";
@@ -717,6 +713,10 @@ function analyze(diffSet) {
717
713
  };
718
714
  }
719
715
 
716
+ // packages/core/src/pipeline.ts
717
+ import getPort from "get-port";
718
+ import open from "open";
719
+
720
720
  // packages/core/src/ws-bridge.ts
721
721
  import { WebSocketServer, WebSocket } from "ws";
722
722
  function createWsBridge(port) {
@@ -1061,6 +1061,76 @@ function consumeReviewResult(cwd) {
1061
1061
  }
1062
1062
  }
1063
1063
 
1064
+ // packages/core/src/server-file.ts
1065
+ import fs3 from "fs";
1066
+ import path5 from "path";
1067
+ import os from "os";
1068
+ function serverDir() {
1069
+ return path5.join(os.homedir(), ".diffprism");
1070
+ }
1071
+ function serverFilePath() {
1072
+ return path5.join(serverDir(), "server.json");
1073
+ }
1074
+ function isPidAlive2(pid) {
1075
+ try {
1076
+ process.kill(pid, 0);
1077
+ return true;
1078
+ } catch {
1079
+ return false;
1080
+ }
1081
+ }
1082
+ function writeServerFile(info) {
1083
+ const dir = serverDir();
1084
+ if (!fs3.existsSync(dir)) {
1085
+ fs3.mkdirSync(dir, { recursive: true });
1086
+ }
1087
+ fs3.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
1088
+ }
1089
+ function readServerFile() {
1090
+ const filePath = serverFilePath();
1091
+ if (!fs3.existsSync(filePath)) {
1092
+ return null;
1093
+ }
1094
+ try {
1095
+ const raw = fs3.readFileSync(filePath, "utf-8");
1096
+ const info = JSON.parse(raw);
1097
+ if (!isPidAlive2(info.pid)) {
1098
+ fs3.unlinkSync(filePath);
1099
+ return null;
1100
+ }
1101
+ return info;
1102
+ } catch {
1103
+ return null;
1104
+ }
1105
+ }
1106
+ function removeServerFile() {
1107
+ try {
1108
+ const filePath = serverFilePath();
1109
+ if (fs3.existsSync(filePath)) {
1110
+ fs3.unlinkSync(filePath);
1111
+ }
1112
+ } catch {
1113
+ }
1114
+ }
1115
+ async function isServerAlive() {
1116
+ const info = readServerFile();
1117
+ if (!info) {
1118
+ return null;
1119
+ }
1120
+ try {
1121
+ const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
1122
+ signal: AbortSignal.timeout(2e3)
1123
+ });
1124
+ if (response.ok) {
1125
+ return info;
1126
+ }
1127
+ return null;
1128
+ } catch {
1129
+ removeServerFile();
1130
+ return null;
1131
+ }
1132
+ }
1133
+
1064
1134
  // packages/core/src/watch.ts
1065
1135
  import { createHash } from "crypto";
1066
1136
  import getPort2 from "get-port";
@@ -1375,78 +1445,6 @@ import { randomUUID } from "crypto";
1375
1445
  import getPort3 from "get-port";
1376
1446
  import open3 from "open";
1377
1447
  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
1448
  var sessions2 = /* @__PURE__ */ new Map();
1451
1449
  var clientSessions = /* @__PURE__ */ new Map();
1452
1450
  function toSummary(session) {
@@ -1699,6 +1697,24 @@ async function startGlobalServer(options = {}) {
1699
1697
  };
1700
1698
  ws.send(JSON.stringify(msg));
1701
1699
  }
1700
+ } else {
1701
+ const summaries = Array.from(sessions2.values()).map(toSummary);
1702
+ const msg = {
1703
+ type: "session:list",
1704
+ payload: summaries
1705
+ };
1706
+ ws.send(JSON.stringify(msg));
1707
+ if (summaries.length === 1) {
1708
+ const session = sessions2.get(summaries[0].id);
1709
+ if (session) {
1710
+ clientSessions.set(ws, session.id);
1711
+ session.status = "in_review";
1712
+ ws.send(JSON.stringify({
1713
+ type: "review:init",
1714
+ payload: session.payload
1715
+ }));
1716
+ }
1717
+ }
1702
1718
  }
1703
1719
  ws.on("message", (data) => {
1704
1720
  try {
@@ -1712,6 +1728,16 @@ async function startGlobalServer(options = {}) {
1712
1728
  session.status = "submitted";
1713
1729
  }
1714
1730
  }
1731
+ } else if (msg.type === "session:select") {
1732
+ const session = sessions2.get(msg.payload.sessionId);
1733
+ if (session) {
1734
+ clientSessions.set(ws, session.id);
1735
+ session.status = "in_review";
1736
+ ws.send(JSON.stringify({
1737
+ type: "review:init",
1738
+ payload: session.payload
1739
+ }));
1740
+ }
1715
1741
  }
1716
1742
  } catch {
1717
1743
  }
@@ -1769,6 +1795,9 @@ Waiting for reviews...
1769
1795
  }
1770
1796
 
1771
1797
  export {
1798
+ getCurrentBranch,
1799
+ getDiff,
1800
+ analyze,
1772
1801
  startReview,
1773
1802
  readWatchFile,
1774
1803
  readReviewResult,
@@ -1,9 +1,13 @@
1
1
  import {
2
+ analyze,
2
3
  consumeReviewResult,
4
+ getCurrentBranch,
5
+ getDiff,
6
+ isServerAlive,
3
7
  readReviewResult,
4
8
  readWatchFile,
5
9
  startReview
6
- } from "./chunk-NGHUHDAM.js";
10
+ } from "./chunk-4WN4FIY4.js";
7
11
 
8
12
  // packages/mcp-server/src/index.ts
9
13
  import fs from "fs";
@@ -11,10 +15,68 @@ import path from "path";
11
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
16
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
17
  import { z } from "zod";
18
+ var lastGlobalSessionId = null;
19
+ var lastGlobalServerInfo = null;
20
+ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
21
+ const cwd = options.cwd ?? process.cwd();
22
+ const { diffSet, rawDiff } = getDiff(diffRef, { cwd });
23
+ const currentBranch = getCurrentBranch({ cwd });
24
+ if (diffSet.files.length === 0) {
25
+ return {
26
+ decision: "approved",
27
+ comments: [],
28
+ summary: "No changes to review."
29
+ };
30
+ }
31
+ const briefing = analyze(diffSet);
32
+ const payload = {
33
+ reviewId: "",
34
+ // Server assigns the real ID
35
+ diffSet,
36
+ rawDiff,
37
+ briefing,
38
+ metadata: {
39
+ title: options.title,
40
+ description: options.description,
41
+ reasoning: options.reasoning,
42
+ currentBranch
43
+ }
44
+ };
45
+ const createResponse = await fetch(
46
+ `http://localhost:${serverInfo.httpPort}/api/reviews`,
47
+ {
48
+ method: "POST",
49
+ headers: { "Content-Type": "application/json" },
50
+ body: JSON.stringify({ payload, projectPath: cwd })
51
+ }
52
+ );
53
+ if (!createResponse.ok) {
54
+ throw new Error(`Global server returned ${createResponse.status} on create`);
55
+ }
56
+ const { sessionId } = await createResponse.json();
57
+ lastGlobalSessionId = sessionId;
58
+ lastGlobalServerInfo = serverInfo;
59
+ const pollIntervalMs = 2e3;
60
+ const maxWaitMs = 600 * 1e3;
61
+ const start = Date.now();
62
+ while (Date.now() - start < maxWaitMs) {
63
+ const resultResponse = await fetch(
64
+ `http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/result`
65
+ );
66
+ if (resultResponse.ok) {
67
+ const data = await resultResponse.json();
68
+ if (data.result) {
69
+ return data.result;
70
+ }
71
+ }
72
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
73
+ }
74
+ throw new Error("Review timed out waiting for submission.");
75
+ }
14
76
  async function startMcpServer() {
15
77
  const server = new McpServer({
16
78
  name: "diffprism",
17
- version: true ? "0.15.0" : "0.0.0-dev"
79
+ version: true ? "0.16.0" : "0.0.0-dev"
18
80
  });
19
81
  server.tool(
20
82
  "open_review",
@@ -29,6 +91,23 @@ async function startMcpServer() {
29
91
  },
30
92
  async ({ diff_ref, title, description, reasoning }) => {
31
93
  try {
94
+ const serverInfo = await isServerAlive();
95
+ if (serverInfo) {
96
+ const result2 = await reviewViaGlobalServer(serverInfo, diff_ref, {
97
+ title,
98
+ description,
99
+ reasoning,
100
+ cwd: process.cwd()
101
+ });
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: JSON.stringify(result2, null, 2)
107
+ }
108
+ ]
109
+ };
110
+ }
32
111
  const isDev = fs.existsSync(
33
112
  path.join(process.cwd(), "packages", "ui", "src", "App.tsx")
34
113
  );
@@ -39,7 +118,6 @@ async function startMcpServer() {
39
118
  reasoning,
40
119
  cwd: process.cwd(),
41
120
  silent: true,
42
- // Suppress stdout — MCP uses stdio
43
121
  dev: isDev
44
122
  });
45
123
  return {
@@ -66,7 +144,7 @@ async function startMcpServer() {
66
144
  );
67
145
  server.tool(
68
146
  "update_review_context",
69
- "Push reasoning/context to a running DiffPrism watch session. Non-blocking \u2014 returns immediately. Use this when `diffprism watch` is running to update the review UI with agent reasoning without opening a new review.",
147
+ "Push reasoning/context to a running DiffPrism review session. Non-blocking \u2014 returns immediately. Use this when `diffprism watch` or `diffprism server` is running to update the review UI with agent reasoning without opening a new review.",
70
148
  {
71
149
  reasoning: z.string().optional().describe("Agent reasoning about the current changes"),
72
150
  title: z.string().optional().describe("Updated title for the review"),
@@ -74,21 +152,44 @@ async function startMcpServer() {
74
152
  },
75
153
  async ({ reasoning, title, description }) => {
76
154
  try {
155
+ const payload = {};
156
+ if (reasoning !== void 0) payload.reasoning = reasoning;
157
+ if (title !== void 0) payload.title = title;
158
+ if (description !== void 0) payload.description = description;
159
+ if (lastGlobalSessionId && lastGlobalServerInfo) {
160
+ const serverInfo = await isServerAlive();
161
+ if (serverInfo) {
162
+ const response2 = await fetch(
163
+ `http://localhost:${serverInfo.httpPort}/api/reviews/${lastGlobalSessionId}/context`,
164
+ {
165
+ method: "POST",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify(payload)
168
+ }
169
+ );
170
+ if (response2.ok) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: "Context updated in DiffPrism global server session."
176
+ }
177
+ ]
178
+ };
179
+ }
180
+ }
181
+ }
77
182
  const watchInfo = readWatchFile();
78
183
  if (!watchInfo) {
79
184
  return {
80
185
  content: [
81
186
  {
82
187
  type: "text",
83
- text: "No DiffPrism watch session is running. Start one with `diffprism watch`."
188
+ text: "No DiffPrism session is running. Start one with `diffprism watch` or `diffprism server`."
84
189
  }
85
190
  ]
86
191
  };
87
192
  }
88
- const payload = {};
89
- if (reasoning !== void 0) payload.reasoning = reasoning;
90
- if (title !== void 0) payload.title = title;
91
- if (description !== void 0) payload.description = description;
92
193
  const response = await fetch(
93
194
  `http://localhost:${watchInfo.wsPort}/api/context`,
94
195
  {
@@ -114,7 +215,7 @@ async function startMcpServer() {
114
215
  content: [
115
216
  {
116
217
  type: "text",
117
- text: `Error updating watch context: ${message}`
218
+ text: `Error updating review context: ${message}`
118
219
  }
119
220
  ],
120
221
  isError: true
@@ -124,16 +225,75 @@ async function startMcpServer() {
124
225
  );
125
226
  server.tool(
126
227
  "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. Use wait=true to block until a result is available (recommended after pushing context to a watch session).",
228
+ "Fetch the most recent review result from a DiffPrism 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
229
  {
129
230
  wait: z.boolean().optional().describe("If true, poll until a review result is available (blocks up to timeout)"),
130
231
  timeout: z.number().optional().describe("Max wait time in seconds when wait=true (default: 300, max: 600)")
131
232
  },
132
233
  async ({ wait, timeout }) => {
133
234
  try {
235
+ const maxWaitMs = Math.min(timeout ?? 300, 600) * 1e3;
236
+ const pollIntervalMs = 2e3;
237
+ if (lastGlobalSessionId && lastGlobalServerInfo) {
238
+ const serverInfo = await isServerAlive();
239
+ if (serverInfo) {
240
+ if (wait) {
241
+ const start = Date.now();
242
+ while (Date.now() - start < maxWaitMs) {
243
+ const response2 = await fetch(
244
+ `http://localhost:${serverInfo.httpPort}/api/reviews/${lastGlobalSessionId}/result`
245
+ );
246
+ if (response2.ok) {
247
+ const data2 = await response2.json();
248
+ if (data2.result) {
249
+ return {
250
+ content: [
251
+ {
252
+ type: "text",
253
+ text: JSON.stringify(data2.result, null, 2)
254
+ }
255
+ ]
256
+ };
257
+ }
258
+ }
259
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
260
+ }
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: "No review result received within timeout."
266
+ }
267
+ ]
268
+ };
269
+ }
270
+ const response = await fetch(
271
+ `http://localhost:${serverInfo.httpPort}/api/reviews/${lastGlobalSessionId}/result`
272
+ );
273
+ if (response.ok) {
274
+ const data2 = await response.json();
275
+ if (data2.result) {
276
+ return {
277
+ content: [
278
+ {
279
+ type: "text",
280
+ text: JSON.stringify(data2.result, null, 2)
281
+ }
282
+ ]
283
+ };
284
+ }
285
+ }
286
+ return {
287
+ content: [
288
+ {
289
+ type: "text",
290
+ text: "No pending review result."
291
+ }
292
+ ]
293
+ };
294
+ }
295
+ }
134
296
  if (wait) {
135
- const maxWaitMs = Math.min(timeout ?? 300, 600) * 1e3;
136
- const pollIntervalMs = 2e3;
137
297
  const start = Date.now();
138
298
  while (Date.now() - start < maxWaitMs) {
139
299
  const data2 = readReviewResult();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {