diffprism 1.1.0 → 1.3.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/README.md +23 -0
- package/dist/bin.js +70 -19
- package/dist/{chunk-FWPE5EA6.js → chunk-7YL3D42S.js} +1 -1
- package/dist/{chunk-KRHZSUEK.js → chunk-MSNGZDVF.js} +69 -9
- package/dist/{demo-Z2XUXEV3.js → demo-376MCAK4.js} +2 -2
- package/dist/mcp-server.js +1 -1
- package/package.json +1 -1
- package/ui-dist/assets/{index-Zw97bHGn.js → index-CLWtGr9m.js} +1 -1
- package/ui-dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,8 @@ diffprism hook install # Add the gate to this repo's pre-commit hoo
|
|
|
130
130
|
diffprism hook uninstall # Remove it
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
If you ask the agent something while the commit waits, the commit stops and prints each question with the command that answers it (`diffprism reply --session <id> <annotation-id> "…"`). The agent answers, commits again, and the review picks up where it left off — no MCP server needed.
|
|
134
|
+
|
|
133
135
|
It reviews **staged** changes only, where every other entry point defaults to the whole
|
|
134
136
|
working copy: a commit contains exactly the index, so unstaged edits aren't part of what
|
|
135
137
|
is being approved. By default a staged diff of **120+ changed lines** opens a review; anything smaller
|
|
@@ -187,6 +189,7 @@ diffprism server status # Check server status
|
|
|
187
189
|
diffprism server stop # Stop the server
|
|
188
190
|
diffprism hook install # Gate commits on a review
|
|
189
191
|
diffprism hook uninstall # Remove the gate
|
|
192
|
+
diffprism reply --session <id> <annotation-id> "…" # Answer a reviewer's question, as the agent
|
|
190
193
|
diffprism feedback # Share feedback as a prefilled GitHub issue
|
|
191
194
|
diffprism feedback --bug # Report a bug, including the last error
|
|
192
195
|
diffprism teardown # Remove configuration
|
|
@@ -215,6 +218,26 @@ pnpm run build
|
|
|
215
218
|
pnpm cli review --staged # Run CLI from source
|
|
216
219
|
```
|
|
217
220
|
|
|
221
|
+
### Dogfooding a checkout
|
|
222
|
+
|
|
223
|
+
To try unreleased changes in another project without publishing, point your global `diffprism` at a checkout:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
git clone https://github.com/CodeJonesW/diffprism.git diffprism-dogfood
|
|
227
|
+
cd diffprism-dogfood && pnpm install && pnpm build && npm link
|
|
228
|
+
diffprism --version # 1.2.0 (dev build — /path/to/diffprism-dogfood)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Then, for each change you want to try:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
git fetch && git checkout <branch> && git pull && pnpm build
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The next `diffprism` command — a review, the commit gate — replaces the running server with the new build, unless a review is open in it. The dashboard picks up a rebuilt UI on reload. Go back to the release with `npm install -g diffprism@latest`.
|
|
238
|
+
|
|
239
|
+
A server is only ever replaced by a **newer** build. Claude Code keeps `diffprism serve` running on the build it started with until you restart it, so restart Claude Code to put its MCP tools on the new build too.
|
|
240
|
+
|
|
218
241
|
### Project Structure
|
|
219
242
|
|
|
220
243
|
```
|
package/dist/bin.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-EPU4F7WT.js";
|
|
6
6
|
import {
|
|
7
7
|
demo
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-7YL3D42S.js";
|
|
9
9
|
import {
|
|
10
10
|
COMMIT_GATE_DIFF_REF,
|
|
11
11
|
DEFAULT_DIFF_REF,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
recordError,
|
|
27
27
|
startGlobalServer,
|
|
28
28
|
submitReviewToServer
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-MSNGZDVF.js";
|
|
30
30
|
import {
|
|
31
31
|
getDiff
|
|
32
32
|
} from "./chunk-3GMPE2ZR.js";
|
|
@@ -40,6 +40,55 @@ import { Command } from "commander";
|
|
|
40
40
|
import { execFileSync } from "child_process";
|
|
41
41
|
import fs from "fs";
|
|
42
42
|
import path from "path";
|
|
43
|
+
|
|
44
|
+
// cli/src/commands/reply.ts
|
|
45
|
+
function replyCommandFor(sessionId, annotationId) {
|
|
46
|
+
return `diffprism reply --session ${sessionId} ${annotationId} "<your answer>"`;
|
|
47
|
+
}
|
|
48
|
+
async function reply(annotationId, words, flags) {
|
|
49
|
+
const body = words.join(" ").trim();
|
|
50
|
+
if (!body) {
|
|
51
|
+
fail("Nothing to say: pass the reply after the annotation id.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const serverInfo = await isServerAlive();
|
|
55
|
+
if (!serverInfo) {
|
|
56
|
+
fail("No DiffPrism server is running, so there is no open review to reply on.");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(
|
|
62
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${flags.session}/annotations/${annotationId}/replies`,
|
|
63
|
+
{
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ author: "agent", agent: flags.agent ?? "agent", body })
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
recordError("reply", err);
|
|
71
|
+
fail(`Could not reach the DiffPrism server: ${err instanceof Error ? err.message : String(err)}
|
|
72
|
+
${REPORT_HINT}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const data = await response.json().catch(() => ({}));
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
fail(`Reply not posted: ${data.error ?? `server returned ${response.status}`}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const where = data.annotation ? ` on ${data.annotation.file}:${data.annotation.line}` : "";
|
|
81
|
+
console.log(`Replied${where}. It shows in the review now.`);
|
|
82
|
+
console.log(
|
|
83
|
+
"The review is still open. Go back to waiting for the decision now: run the same command that opened it again (`git commit` or `diffprism review`). It returns the decision, or the reviewer's next question. Don't ask them in the terminal."
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
function fail(message2) {
|
|
87
|
+
console.error(message2);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// cli/src/commands/hook.ts
|
|
43
92
|
var DEFAULT_MIN_LINES = 120;
|
|
44
93
|
var RETRY_ADVICE = "The review stays open in DiffPrism \u2014 once the reviewer decides, run git commit again to pick up the decision. Don't open another review or change the staged files meanwhile.";
|
|
45
94
|
var MARKER_START = "# >>> diffprism >>>";
|
|
@@ -57,7 +106,7 @@ async function preCommitHook(flags = {}) {
|
|
|
57
106
|
);
|
|
58
107
|
} catch (err) {
|
|
59
108
|
recordError("hook pre-commit", err);
|
|
60
|
-
|
|
109
|
+
fail2(`Could not read the staged diff: ${message(err)}
|
|
61
110
|
${REPORT_HINT}`);
|
|
62
111
|
return;
|
|
63
112
|
}
|
|
@@ -88,18 +137,18 @@ ${REPORT_HINT}`);
|
|
|
88
137
|
} catch (err) {
|
|
89
138
|
stopWaiting();
|
|
90
139
|
if (err instanceof ReviewerAskedError) {
|
|
91
|
-
printQuestions(err.threads);
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
printQuestions(err.sessionId, err.threads);
|
|
141
|
+
fail2(
|
|
142
|
+
"Commit blocked: the reviewer asked you something before deciding. Answer each question with the command under it \u2014 change the code too if that's what they asked for \u2014 then run git commit again straight away to keep waiting. The review stays open, the reviewer may ask more, and the decision still comes. Don't stop to ask them in the terminal."
|
|
94
143
|
);
|
|
95
144
|
return;
|
|
96
145
|
}
|
|
97
146
|
if (err instanceof ReviewTimeoutError) {
|
|
98
|
-
|
|
147
|
+
fail2(`Commit blocked: no decision after ${Math.round(err.waitedMs / 1e3)}s. ${RETRY_ADVICE}`);
|
|
99
148
|
return;
|
|
100
149
|
}
|
|
101
150
|
recordError("hook pre-commit", err);
|
|
102
|
-
|
|
151
|
+
fail2(`DiffPrism could not run the review: ${message(err)}
|
|
103
152
|
${REPORT_HINT}`);
|
|
104
153
|
return;
|
|
105
154
|
}
|
|
@@ -116,27 +165,28 @@ ${REPORT_HINT}`);
|
|
|
116
165
|
}
|
|
117
166
|
if (decision === "changes_requested") {
|
|
118
167
|
printFeedback(review2);
|
|
119
|
-
|
|
168
|
+
fail2("Commit blocked: the review requested changes.");
|
|
120
169
|
return;
|
|
121
170
|
}
|
|
122
171
|
if (decision === "dismissed") {
|
|
123
|
-
|
|
172
|
+
fail2("Commit blocked: the review was dismissed without a decision.");
|
|
124
173
|
return;
|
|
125
174
|
}
|
|
126
|
-
|
|
175
|
+
fail2(
|
|
127
176
|
`Commit blocked: no review decision was returned (got ${String(decision)}).`
|
|
128
177
|
);
|
|
129
178
|
}
|
|
130
|
-
function printQuestions(threads) {
|
|
179
|
+
function printQuestions(sessionId, threads) {
|
|
131
180
|
console.error("");
|
|
132
181
|
for (const t of threads) {
|
|
133
182
|
const last = t.replies?.at(-1)?.body ?? t.body;
|
|
134
|
-
console.error(` ${t.file}:${t.line}
|
|
183
|
+
console.error(` ${t.file}:${t.line}`);
|
|
135
184
|
for (const line of last.split("\n")) {
|
|
136
185
|
console.error(` ${line}`);
|
|
137
186
|
}
|
|
187
|
+
console.error(` Answer: ${replyCommandFor(sessionId, t.id)}`);
|
|
188
|
+
console.error("");
|
|
138
189
|
}
|
|
139
|
-
console.error("");
|
|
140
190
|
}
|
|
141
191
|
function printFeedback(review2) {
|
|
142
192
|
const summary = review2?.summary?.trim();
|
|
@@ -277,7 +327,7 @@ function ensureTrailingNewline(text) {
|
|
|
277
327
|
function message(err) {
|
|
278
328
|
return err instanceof Error ? err.message : String(err);
|
|
279
329
|
}
|
|
280
|
-
function
|
|
330
|
+
function fail2(text) {
|
|
281
331
|
console.error(text);
|
|
282
332
|
process.exit(1);
|
|
283
333
|
}
|
|
@@ -320,9 +370,9 @@ async function reviewLocalFlow(diffRef, flags) {
|
|
|
320
370
|
}));
|
|
321
371
|
} catch (err) {
|
|
322
372
|
if (err instanceof ReviewerAskedError) {
|
|
323
|
-
printQuestions(err.threads);
|
|
373
|
+
printQuestions(err.sessionId, err.threads);
|
|
324
374
|
console.error(
|
|
325
|
-
|
|
375
|
+
"The reviewer asked something before deciding. Answer each question with the command under it, then run diffprism review again straight away to keep waiting \u2014 the review stays open, and the reviewer may ask more. Don't stop to ask them in the terminal."
|
|
326
376
|
);
|
|
327
377
|
process.exit(1);
|
|
328
378
|
}
|
|
@@ -449,7 +499,7 @@ If the repo has the DiffPrism pre-commit gate installed (\`diffprism hook instal
|
|
|
449
499
|
- Run \`git commit\` with a shell timeout long enough for someone to read the change \u2014 up to 600000 ms \u2014 not the short default.
|
|
450
500
|
- If the commit is interrupted, or reports no decision, the review is still open. Once the reviewer decides, run the **same** \`git commit\` again: their decision is picked up immediately. Don't change the staged files first \u2014 that makes it a new question.
|
|
451
501
|
- If it's blocked with changes requested, the reviewer's summary and comments are printed. Address them, stage, and commit again.
|
|
452
|
-
- If it's blocked because the reviewer asked something,
|
|
502
|
+
- If it's blocked because the reviewer asked something, each question is printed with the command that answers it \u2014 \`diffprism reply --session <id> <annotation-id> "<your answer>"\`. Run it for each, then run the same \`git commit\` again \u2014 the review is still open.
|
|
453
503
|
|
|
454
504
|
## Workflow 3: PR Review
|
|
455
505
|
|
|
@@ -669,7 +719,7 @@ async function setupInteractive(flags) {
|
|
|
669
719
|
}
|
|
670
720
|
async function runDemo(dev) {
|
|
671
721
|
console.log("");
|
|
672
|
-
const { demo: demo2 } = await import("./demo-
|
|
722
|
+
const { demo: demo2 } = await import("./demo-376MCAK4.js");
|
|
673
723
|
await demo2({ dev });
|
|
674
724
|
}
|
|
675
725
|
async function setupBatch(flags) {
|
|
@@ -1184,6 +1234,7 @@ hookCmd.command("uninstall").description("Remove the diffprism gate from this re
|
|
|
1184
1234
|
uninstallHook();
|
|
1185
1235
|
});
|
|
1186
1236
|
program.command("feedback").description("Open a prefilled GitHub issue to share feedback or report a bug \u2014 you review it before anything is sent").option("--bug", "Report a bug, including the last error DiffPrism hit").option("-m, --message <text>", "Start the issue with this text").option("--print", "Print the issue URL instead of opening a browser").action((flags) => feedback(flags));
|
|
1237
|
+
program.command("reply <annotation-id> <message...>").description("Answer a reviewer's question on an open review, as the agent").requiredOption("--session <id>", "The review the question is on").option("--agent <name>", "Name shown on the reply", "agent").action((annotationId, message2, flags) => reply(annotationId, message2, flags));
|
|
1187
1238
|
program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
|
|
1188
1239
|
program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Configure globally (skill + permissions, no git repo required)").option("--force", "Overwrite existing configuration files").option("--dev", "Use Vite dev server").option("--no-demo", "Skip the demo review after setup").action((flags) => {
|
|
1189
1240
|
setup(flags);
|
|
@@ -79,15 +79,13 @@ async function isServerAlive() {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// packages/core/src/diff-scope.ts
|
|
83
|
-
var DEFAULT_DIFF_REF = "working-copy";
|
|
84
|
-
var COMMIT_GATE_DIFF_REF = "staged";
|
|
85
|
-
var DIFF_REF_DESCRIPTION = 'Which changes to review. "working-copy" (the default): everything not yet committed, staged and unstaged shown as separate groups. "staged": only what the next commit would contain. "unstaged": only edits not yet staged. Or a ref range such as "HEAD~3..HEAD" or "main..feature".';
|
|
86
|
-
|
|
87
82
|
// packages/core/src/build-info.ts
|
|
88
83
|
import fs2 from "fs";
|
|
89
84
|
import path2 from "path";
|
|
90
85
|
import { fileURLToPath } from "url";
|
|
86
|
+
function builtAt() {
|
|
87
|
+
return true ? 1789607742599 : null;
|
|
88
|
+
}
|
|
91
89
|
function getBuildInfo() {
|
|
92
90
|
let dir = path2.dirname(fileURLToPath(import.meta.url));
|
|
93
91
|
while (dir !== path2.dirname(dir)) {
|
|
@@ -112,6 +110,11 @@ function describeVersion(version) {
|
|
|
112
110
|
return info.dev ? `${version} (dev build \u2014 ${info.root})` : version;
|
|
113
111
|
}
|
|
114
112
|
|
|
113
|
+
// packages/core/src/diff-scope.ts
|
|
114
|
+
var DEFAULT_DIFF_REF = "working-copy";
|
|
115
|
+
var COMMIT_GATE_DIFF_REF = "staged";
|
|
116
|
+
var DIFF_REF_DESCRIPTION = 'Which changes to review. "working-copy" (the default): everything not yet committed, staged and unstaged shown as separate groups. "staged": only what the next commit would contain. "unstaged": only edits not yet staged. Or a ref range such as "HEAD~3..HEAD" or "main..feature".';
|
|
117
|
+
|
|
115
118
|
// packages/core/src/feedback.ts
|
|
116
119
|
import fs3 from "fs";
|
|
117
120
|
import os2 from "os";
|
|
@@ -119,7 +122,7 @@ import path3 from "path";
|
|
|
119
122
|
var ISSUES_NEW_URL = "https://github.com/CodeJonesW/diffprism/issues/new";
|
|
120
123
|
var MAX_ERROR_CHARS = 1500;
|
|
121
124
|
function currentVersion() {
|
|
122
|
-
return true ? "1.
|
|
125
|
+
return true ? "1.3.0" : "0.0.0-dev";
|
|
123
126
|
}
|
|
124
127
|
function describeEnvironment() {
|
|
125
128
|
return {
|
|
@@ -226,7 +229,22 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
226
229
|
async function ensureServer(options = {}) {
|
|
227
230
|
const existing = await isServerAlive();
|
|
228
231
|
if (existing) {
|
|
229
|
-
|
|
232
|
+
const decision = await decideOnRunningServer(existing, builtAt());
|
|
233
|
+
if (decision.action === "keep") {
|
|
234
|
+
return existing;
|
|
235
|
+
}
|
|
236
|
+
if (decision.action === "keep-busy") {
|
|
237
|
+
if (!options.silent) {
|
|
238
|
+
console.error(
|
|
239
|
+
`The DiffPrism server (PID ${existing.pid}) runs an older build, but ${decision.openReviews} review${decision.openReviews === 1 ? " is" : "s are"} open in it, so it stays up. Finish or close ${decision.openReviews === 1 ? "it" : "them"} and the next command starts the new build \u2014 or run \`diffprism server stop\`.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return existing;
|
|
243
|
+
}
|
|
244
|
+
if (!options.silent) {
|
|
245
|
+
console.error(`Replacing the DiffPrism server (PID ${existing.pid}): it runs an older build than this command.`);
|
|
246
|
+
}
|
|
247
|
+
await stopServer(existing);
|
|
230
248
|
}
|
|
231
249
|
const spawnArgs = options.spawnCommand ?? buildDefaultSpawnCommand(options);
|
|
232
250
|
const logDir = path4.join(os3.homedir(), ".diffprism");
|
|
@@ -256,6 +274,44 @@ async function ensureServer(options = {}) {
|
|
|
256
274
|
`DiffPrism server failed to start within ${timeoutMs / 1e3}s. Check logs at ${logPath}`
|
|
257
275
|
);
|
|
258
276
|
}
|
|
277
|
+
async function decideOnRunningServer(server, ownBuiltAt) {
|
|
278
|
+
if (ownBuiltAt === null) return { action: "keep" };
|
|
279
|
+
if (server.builtAt !== void 0 && server.builtAt >= ownBuiltAt) return { action: "keep" };
|
|
280
|
+
const response = await fetch(`http://localhost:${server.httpPort}/api/reviews`);
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
throw new Error(`Could not list reviews on the running DiffPrism server: it returned ${response.status}`);
|
|
283
|
+
}
|
|
284
|
+
const { sessions: sessions2 } = await response.json();
|
|
285
|
+
const openReviews = sessions2.filter((s) => s.status === "pending" || s.status === "in_review").length;
|
|
286
|
+
return openReviews > 0 ? { action: "keep-busy", openReviews } : { action: "replace" };
|
|
287
|
+
}
|
|
288
|
+
async function stopServer(server, timeoutMs = 5e3) {
|
|
289
|
+
try {
|
|
290
|
+
process.kill(server.pid, "SIGTERM");
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
const start = Date.now();
|
|
294
|
+
while (Date.now() - start < timeoutMs) {
|
|
295
|
+
if (!isProcessRunning(server.pid)) {
|
|
296
|
+
if (readServerFile()?.pid === server.pid) {
|
|
297
|
+
removeServerFile();
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
302
|
+
}
|
|
303
|
+
throw new Error(
|
|
304
|
+
`The old DiffPrism server (PID ${server.pid}) did not stop within ${timeoutMs / 1e3}s. Stop it with \`diffprism server stop\` and try again.`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
function isProcessRunning(pid) {
|
|
308
|
+
try {
|
|
309
|
+
process.kill(pid, 0);
|
|
310
|
+
return true;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
259
315
|
function buildDefaultSpawnCommand(options) {
|
|
260
316
|
const thisFile = fileURLToPath2(import.meta.url);
|
|
261
317
|
const thisDir = path4.dirname(thisFile);
|
|
@@ -1913,6 +1969,10 @@ async function startGlobalServer(options = {}) {
|
|
|
1913
1969
|
pid: process.pid,
|
|
1914
1970
|
startedAt: Date.now()
|
|
1915
1971
|
};
|
|
1972
|
+
const serverBuiltAt = builtAt();
|
|
1973
|
+
if (serverBuiltAt !== null) {
|
|
1974
|
+
serverInfo.builtAt = serverBuiltAt;
|
|
1975
|
+
}
|
|
1916
1976
|
writeServerFile(serverInfo);
|
|
1917
1977
|
if (!silent) {
|
|
1918
1978
|
console.log(`
|
|
@@ -1993,11 +2053,11 @@ function mcpToolPermission(name) {
|
|
|
1993
2053
|
export {
|
|
1994
2054
|
readServerFile,
|
|
1995
2055
|
isServerAlive,
|
|
2056
|
+
getBuildInfo,
|
|
2057
|
+
describeVersion,
|
|
1996
2058
|
DEFAULT_DIFF_REF,
|
|
1997
2059
|
COMMIT_GATE_DIFF_REF,
|
|
1998
2060
|
DIFF_REF_DESCRIPTION,
|
|
1999
|
-
getBuildInfo,
|
|
2000
|
-
describeVersion,
|
|
2001
2061
|
currentVersion,
|
|
2002
2062
|
recordError,
|
|
2003
2063
|
readLastError,
|
package/dist/mcp-server.js
CHANGED