diffprism 0.12.2 → 0.13.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.
@@ -1,10 +1,6 @@
1
1
  // packages/core/src/pipeline.ts
2
- import http from "http";
3
- import fs from "fs";
4
- import path3 from "path";
5
2
  import getPort from "get-port";
6
3
  import open from "open";
7
- import { fileURLToPath } from "url";
8
4
 
9
5
  // packages/git/src/local.ts
10
6
  import { execSync } from "child_process";
@@ -515,15 +511,15 @@ var CONFIG_PATTERNS = [
515
511
  /vite\.config/,
516
512
  /vitest\.config/
517
513
  ];
518
- function isTestFile(path4) {
519
- return TEST_PATTERNS.some((re) => re.test(path4));
514
+ function isTestFile(path5) {
515
+ return TEST_PATTERNS.some((re) => re.test(path5));
520
516
  }
521
- function isNonCodeFile(path4) {
522
- const ext = path4.slice(path4.lastIndexOf("."));
517
+ function isNonCodeFile(path5) {
518
+ const ext = path5.slice(path5.lastIndexOf("."));
523
519
  return NON_CODE_EXTENSIONS.has(ext);
524
520
  }
525
- function isConfigFile(path4) {
526
- return CONFIG_PATTERNS.some((re) => re.test(path4));
521
+ function isConfigFile(path5) {
522
+ return CONFIG_PATTERNS.some((re) => re.test(path5));
527
523
  }
528
524
  function detectTestCoverageGaps(files) {
529
525
  const filePaths = new Set(files.map((f) => f.path));
@@ -794,7 +790,11 @@ function updateSession(id, update) {
794
790
  }
795
791
  }
796
792
 
797
- // packages/core/src/pipeline.ts
793
+ // packages/core/src/ui-server.ts
794
+ import http from "http";
795
+ import fs from "fs";
796
+ import path3 from "path";
797
+ import { fileURLToPath } from "url";
798
798
  var MIME_TYPES = {
799
799
  ".html": "text/html",
800
800
  ".js": "application/javascript",
@@ -867,6 +867,8 @@ function createStaticServer(distPath, port) {
867
867
  server.listen(port, () => resolve(server));
868
868
  });
869
869
  }
870
+
871
+ // packages/core/src/pipeline.ts
870
872
  async function startReview(options) {
871
873
  const { diffRef, title, description, reasoning, cwd, silent, dev } = options;
872
874
  const { diffSet, rawDiff } = getDiff(diffRef, { cwd });
@@ -929,6 +931,415 @@ DiffPrism Review: ${title ?? briefing.summary}`);
929
931
  }
930
932
  }
931
933
 
934
+ // packages/core/src/watch-file.ts
935
+ import fs2 from "fs";
936
+ import path4 from "path";
937
+ import { execSync as execSync2 } from "child_process";
938
+ function findGitRoot(cwd) {
939
+ const root = execSync2("git rev-parse --show-toplevel", {
940
+ cwd: cwd ?? process.cwd(),
941
+ encoding: "utf-8"
942
+ }).trim();
943
+ return root;
944
+ }
945
+ function watchFilePath(cwd) {
946
+ const gitRoot = findGitRoot(cwd);
947
+ return path4.join(gitRoot, ".diffprism", "watch.json");
948
+ }
949
+ function isPidAlive(pid) {
950
+ try {
951
+ process.kill(pid, 0);
952
+ return true;
953
+ } catch {
954
+ return false;
955
+ }
956
+ }
957
+ function writeWatchFile(cwd, info) {
958
+ const filePath = watchFilePath(cwd);
959
+ const dir = path4.dirname(filePath);
960
+ if (!fs2.existsSync(dir)) {
961
+ fs2.mkdirSync(dir, { recursive: true });
962
+ }
963
+ fs2.writeFileSync(filePath, JSON.stringify(info, null, 2) + "\n");
964
+ }
965
+ function readWatchFile(cwd) {
966
+ const filePath = watchFilePath(cwd);
967
+ if (!fs2.existsSync(filePath)) {
968
+ return null;
969
+ }
970
+ try {
971
+ const raw = fs2.readFileSync(filePath, "utf-8");
972
+ const info = JSON.parse(raw);
973
+ if (!isPidAlive(info.pid)) {
974
+ fs2.unlinkSync(filePath);
975
+ return null;
976
+ }
977
+ return info;
978
+ } catch {
979
+ return null;
980
+ }
981
+ }
982
+ function removeWatchFile(cwd) {
983
+ try {
984
+ const filePath = watchFilePath(cwd);
985
+ if (fs2.existsSync(filePath)) {
986
+ fs2.unlinkSync(filePath);
987
+ }
988
+ } catch {
989
+ }
990
+ }
991
+ function reviewResultPath(cwd) {
992
+ const gitRoot = findGitRoot(cwd);
993
+ return path4.join(gitRoot, ".diffprism", "last-review.json");
994
+ }
995
+ function writeReviewResult(cwd, result) {
996
+ const filePath = reviewResultPath(cwd);
997
+ const dir = path4.dirname(filePath);
998
+ if (!fs2.existsSync(dir)) {
999
+ fs2.mkdirSync(dir, { recursive: true });
1000
+ }
1001
+ const data = {
1002
+ result,
1003
+ timestamp: Date.now(),
1004
+ consumed: false
1005
+ };
1006
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1007
+ }
1008
+ function readReviewResult(cwd) {
1009
+ try {
1010
+ const filePath = reviewResultPath(cwd);
1011
+ if (!fs2.existsSync(filePath)) {
1012
+ return null;
1013
+ }
1014
+ const raw = fs2.readFileSync(filePath, "utf-8");
1015
+ const data = JSON.parse(raw);
1016
+ if (data.consumed) {
1017
+ return null;
1018
+ }
1019
+ return data;
1020
+ } catch {
1021
+ return null;
1022
+ }
1023
+ }
1024
+ function consumeReviewResult(cwd) {
1025
+ try {
1026
+ const filePath = reviewResultPath(cwd);
1027
+ if (!fs2.existsSync(filePath)) {
1028
+ return;
1029
+ }
1030
+ const raw = fs2.readFileSync(filePath, "utf-8");
1031
+ const data = JSON.parse(raw);
1032
+ data.consumed = true;
1033
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1034
+ } catch {
1035
+ }
1036
+ }
1037
+
1038
+ // packages/core/src/watch.ts
1039
+ import { createHash } from "crypto";
1040
+ import getPort2 from "get-port";
1041
+ import open2 from "open";
1042
+
1043
+ // packages/core/src/watch-bridge.ts
1044
+ import http2 from "http";
1045
+ import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
1046
+ function createWatchBridge(port, callbacks) {
1047
+ let client = null;
1048
+ let initPayload = null;
1049
+ let pendingInit = null;
1050
+ let closeTimer = null;
1051
+ let submitCallback = null;
1052
+ const httpServer = http2.createServer((req, res) => {
1053
+ res.setHeader("Access-Control-Allow-Origin", "*");
1054
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1055
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1056
+ if (req.method === "OPTIONS") {
1057
+ res.writeHead(204);
1058
+ res.end();
1059
+ return;
1060
+ }
1061
+ if (req.method === "GET" && req.url === "/api/status") {
1062
+ res.writeHead(200, { "Content-Type": "application/json" });
1063
+ res.end(JSON.stringify({ running: true, pid: process.pid }));
1064
+ return;
1065
+ }
1066
+ if (req.method === "POST" && req.url === "/api/context") {
1067
+ let body = "";
1068
+ req.on("data", (chunk) => {
1069
+ body += chunk.toString();
1070
+ });
1071
+ req.on("end", () => {
1072
+ try {
1073
+ const payload = JSON.parse(body);
1074
+ callbacks.onContextUpdate(payload);
1075
+ sendToClient({ type: "context:update", payload });
1076
+ res.writeHead(200, { "Content-Type": "application/json" });
1077
+ res.end(JSON.stringify({ ok: true }));
1078
+ } catch {
1079
+ res.writeHead(400, { "Content-Type": "application/json" });
1080
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
1081
+ }
1082
+ });
1083
+ return;
1084
+ }
1085
+ if (req.method === "POST" && req.url === "/api/refresh") {
1086
+ callbacks.onRefreshRequest();
1087
+ res.writeHead(200, { "Content-Type": "application/json" });
1088
+ res.end(JSON.stringify({ ok: true }));
1089
+ return;
1090
+ }
1091
+ res.writeHead(404);
1092
+ res.end("Not found");
1093
+ });
1094
+ const wss = new WebSocketServer2({ server: httpServer });
1095
+ function sendToClient(msg) {
1096
+ if (client && client.readyState === WebSocket2.OPEN) {
1097
+ client.send(JSON.stringify(msg));
1098
+ }
1099
+ }
1100
+ wss.on("connection", (ws) => {
1101
+ if (closeTimer) {
1102
+ clearTimeout(closeTimer);
1103
+ closeTimer = null;
1104
+ }
1105
+ client = ws;
1106
+ const payload = pendingInit ?? initPayload;
1107
+ if (payload) {
1108
+ sendToClient({ type: "review:init", payload });
1109
+ pendingInit = null;
1110
+ }
1111
+ ws.on("message", (data) => {
1112
+ try {
1113
+ const msg = JSON.parse(data.toString());
1114
+ if (msg.type === "review:submit" && submitCallback) {
1115
+ submitCallback(msg.payload);
1116
+ }
1117
+ } catch {
1118
+ }
1119
+ });
1120
+ ws.on("close", () => {
1121
+ client = null;
1122
+ closeTimer = setTimeout(() => {
1123
+ closeTimer = null;
1124
+ }, 2e3);
1125
+ });
1126
+ });
1127
+ return new Promise((resolve, reject) => {
1128
+ httpServer.on("error", reject);
1129
+ httpServer.listen(port, () => {
1130
+ resolve({
1131
+ port,
1132
+ sendInit(payload) {
1133
+ initPayload = payload;
1134
+ if (client && client.readyState === WebSocket2.OPEN) {
1135
+ sendToClient({ type: "review:init", payload });
1136
+ } else {
1137
+ pendingInit = payload;
1138
+ }
1139
+ },
1140
+ sendDiffUpdate(payload) {
1141
+ sendToClient({ type: "diff:update", payload });
1142
+ },
1143
+ sendContextUpdate(payload) {
1144
+ sendToClient({ type: "context:update", payload });
1145
+ },
1146
+ onSubmit(callback) {
1147
+ submitCallback = callback;
1148
+ },
1149
+ triggerRefresh() {
1150
+ callbacks.onRefreshRequest();
1151
+ },
1152
+ async close() {
1153
+ if (closeTimer) {
1154
+ clearTimeout(closeTimer);
1155
+ }
1156
+ for (const ws of wss.clients) {
1157
+ ws.close();
1158
+ }
1159
+ wss.close();
1160
+ await new Promise((resolve2) => {
1161
+ httpServer.close(() => resolve2());
1162
+ });
1163
+ }
1164
+ });
1165
+ });
1166
+ });
1167
+ }
1168
+
1169
+ // packages/core/src/watch.ts
1170
+ function hashDiff(rawDiff) {
1171
+ return createHash("sha256").update(rawDiff).digest("hex");
1172
+ }
1173
+ function detectChangedFiles(oldDiffSet, newDiffSet) {
1174
+ if (!oldDiffSet) {
1175
+ return newDiffSet.files.map((f) => f.path);
1176
+ }
1177
+ const oldFiles = new Map(
1178
+ oldDiffSet.files.map((f) => [f.path, f])
1179
+ );
1180
+ const changed = [];
1181
+ for (const newFile of newDiffSet.files) {
1182
+ const oldFile = oldFiles.get(newFile.path);
1183
+ if (!oldFile) {
1184
+ changed.push(newFile.path);
1185
+ } else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
1186
+ changed.push(newFile.path);
1187
+ }
1188
+ }
1189
+ for (const oldFile of oldDiffSet.files) {
1190
+ if (!newDiffSet.files.some((f) => f.path === oldFile.path)) {
1191
+ changed.push(oldFile.path);
1192
+ }
1193
+ }
1194
+ return changed;
1195
+ }
1196
+ async function startWatch(options) {
1197
+ const {
1198
+ diffRef,
1199
+ title,
1200
+ description,
1201
+ reasoning,
1202
+ cwd,
1203
+ silent,
1204
+ dev,
1205
+ pollInterval = 1e3
1206
+ } = options;
1207
+ const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
1208
+ const currentBranch = getCurrentBranch({ cwd });
1209
+ const initialBriefing = analyze(initialDiffSet);
1210
+ let lastDiffHash = hashDiff(initialRawDiff);
1211
+ let lastDiffSet = initialDiffSet;
1212
+ const metadata = {
1213
+ title,
1214
+ description,
1215
+ reasoning,
1216
+ currentBranch
1217
+ };
1218
+ const [bridgePort, uiPort] = await Promise.all([
1219
+ getPort2(),
1220
+ getPort2()
1221
+ ]);
1222
+ let refreshRequested = false;
1223
+ const bridge = await createWatchBridge(bridgePort, {
1224
+ onRefreshRequest: () => {
1225
+ refreshRequested = true;
1226
+ },
1227
+ onContextUpdate: (payload) => {
1228
+ if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
1229
+ if (payload.title !== void 0) metadata.title = payload.title;
1230
+ if (payload.description !== void 0) metadata.description = payload.description;
1231
+ }
1232
+ });
1233
+ let httpServer = null;
1234
+ let viteServer = null;
1235
+ if (dev) {
1236
+ const uiRoot = resolveUiRoot();
1237
+ viteServer = await startViteDevServer(uiRoot, uiPort, !!silent);
1238
+ } else {
1239
+ const uiDist = resolveUiDist();
1240
+ httpServer = await createStaticServer(uiDist, uiPort);
1241
+ }
1242
+ writeWatchFile(cwd, {
1243
+ wsPort: bridgePort,
1244
+ uiPort,
1245
+ pid: process.pid,
1246
+ cwd: cwd ?? process.cwd(),
1247
+ diffRef,
1248
+ startedAt: Date.now()
1249
+ });
1250
+ const reviewId = "watch-session";
1251
+ const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&reviewId=${reviewId}`;
1252
+ if (!silent) {
1253
+ console.log(`
1254
+ DiffPrism Watch: ${title ?? `watching ${diffRef}`}`);
1255
+ console.log(`Browser: ${url}`);
1256
+ console.log(`API: http://localhost:${bridgePort}`);
1257
+ console.log(`Polling every ${pollInterval}ms
1258
+ `);
1259
+ }
1260
+ await open2(url);
1261
+ const initPayload = {
1262
+ reviewId,
1263
+ diffSet: initialDiffSet,
1264
+ rawDiff: initialRawDiff,
1265
+ briefing: initialBriefing,
1266
+ metadata,
1267
+ watchMode: true
1268
+ };
1269
+ bridge.sendInit(initPayload);
1270
+ bridge.onSubmit((result) => {
1271
+ if (!silent) {
1272
+ console.log(`
1273
+ Review submitted: ${result.decision}`);
1274
+ if (result.comments.length > 0) {
1275
+ console.log(` ${result.comments.length} comment(s)`);
1276
+ }
1277
+ console.log("Continuing to watch...\n");
1278
+ }
1279
+ writeReviewResult(cwd, result);
1280
+ });
1281
+ let pollRunning = true;
1282
+ const pollLoop = setInterval(() => {
1283
+ if (!pollRunning) return;
1284
+ try {
1285
+ const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(diffRef, { cwd });
1286
+ const newHash = hashDiff(newRawDiff);
1287
+ if (newHash !== lastDiffHash || refreshRequested) {
1288
+ refreshRequested = false;
1289
+ const newBriefing = analyze(newDiffSet);
1290
+ const changedFiles = detectChangedFiles(lastDiffSet, newDiffSet);
1291
+ lastDiffHash = newHash;
1292
+ lastDiffSet = newDiffSet;
1293
+ bridge.sendInit({
1294
+ reviewId,
1295
+ diffSet: newDiffSet,
1296
+ rawDiff: newRawDiff,
1297
+ briefing: newBriefing,
1298
+ metadata,
1299
+ watchMode: true
1300
+ });
1301
+ const updatePayload = {
1302
+ diffSet: newDiffSet,
1303
+ rawDiff: newRawDiff,
1304
+ briefing: newBriefing,
1305
+ changedFiles,
1306
+ timestamp: Date.now()
1307
+ };
1308
+ bridge.sendDiffUpdate(updatePayload);
1309
+ if (!silent && changedFiles.length > 0) {
1310
+ console.log(
1311
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${changedFiles.length} file(s) changed`
1312
+ );
1313
+ }
1314
+ }
1315
+ } catch {
1316
+ }
1317
+ }, pollInterval);
1318
+ async function stop() {
1319
+ pollRunning = false;
1320
+ clearInterval(pollLoop);
1321
+ await bridge.close();
1322
+ if (viteServer) {
1323
+ await viteServer.close();
1324
+ }
1325
+ if (httpServer) {
1326
+ httpServer.close();
1327
+ }
1328
+ removeWatchFile(cwd);
1329
+ }
1330
+ function updateContext(payload) {
1331
+ if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
1332
+ if (payload.title !== void 0) metadata.title = payload.title;
1333
+ if (payload.description !== void 0) metadata.description = payload.description;
1334
+ bridge.sendContextUpdate(payload);
1335
+ }
1336
+ return { stop, updateContext };
1337
+ }
1338
+
932
1339
  export {
933
- startReview
1340
+ startReview,
1341
+ readWatchFile,
1342
+ readReviewResult,
1343
+ consumeReviewResult,
1344
+ startWatch
934
1345
  };
@@ -1,6 +1,9 @@
1
1
  import {
2
+ consumeReviewResult,
3
+ readReviewResult,
4
+ readWatchFile,
2
5
  startReview
3
- } from "./chunk-LOX6GE37.js";
6
+ } from "./chunk-TYUDIWD6.js";
4
7
 
5
8
  // packages/mcp-server/src/index.ts
6
9
  import fs from "fs";
@@ -11,7 +14,7 @@ import { z } from "zod";
11
14
  async function startMcpServer() {
12
15
  const server = new McpServer({
13
16
  name: "diffprism",
14
- version: true ? "0.12.2" : "0.0.0-dev"
17
+ version: true ? "0.13.1" : "0.0.0-dev"
15
18
  });
16
19
  server.tool(
17
20
  "open_review",
@@ -61,6 +64,104 @@ async function startMcpServer() {
61
64
  }
62
65
  }
63
66
  );
67
+ server.tool(
68
+ "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.",
70
+ {
71
+ reasoning: z.string().optional().describe("Agent reasoning about the current changes"),
72
+ title: z.string().optional().describe("Updated title for the review"),
73
+ description: z.string().optional().describe("Updated description of the changes")
74
+ },
75
+ async ({ reasoning, title, description }) => {
76
+ try {
77
+ const watchInfo = readWatchFile();
78
+ if (!watchInfo) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: "No DiffPrism watch session is running. Start one with `diffprism watch`."
84
+ }
85
+ ]
86
+ };
87
+ }
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
+ const response = await fetch(
93
+ `http://localhost:${watchInfo.wsPort}/api/context`,
94
+ {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify(payload)
98
+ }
99
+ );
100
+ if (!response.ok) {
101
+ throw new Error(`Watch server returned ${response.status}`);
102
+ }
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: "Context updated in DiffPrism watch session."
108
+ }
109
+ ]
110
+ };
111
+ } catch (err) {
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: `Error updating watch context: ${message}`
118
+ }
119
+ ],
120
+ isError: true
121
+ };
122
+ }
123
+ }
124
+ );
125
+ server.tool(
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 () => {
130
+ try {
131
+ const data = readReviewResult();
132
+ if (!data) {
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: "No pending review result."
138
+ }
139
+ ]
140
+ };
141
+ }
142
+ consumeReviewResult();
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify(data.result, null, 2)
148
+ }
149
+ ]
150
+ };
151
+ } catch (err) {
152
+ const message = err instanceof Error ? err.message : String(err);
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `Error reading review result: ${message}`
158
+ }
159
+ ],
160
+ isError: true
161
+ };
162
+ }
163
+ }
164
+ );
64
165
  const transport = new StdioServerTransport();
65
166
  await server.connect(transport);
66
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.12.2",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {