diffprism 0.13.8 → 0.14.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
@@ -3,7 +3,7 @@ import {
3
3
  readWatchFile,
4
4
  startReview,
5
5
  startWatch
6
- } from "./chunk-QB2PKDLU.js";
6
+ } from "./chunk-TVXIMP3G.js";
7
7
 
8
8
  // cli/src/index.ts
9
9
  import { Command } from "commander";
@@ -18,7 +18,7 @@ async function review(ref, flags) {
18
18
  } else if (ref) {
19
19
  diffRef = ref;
20
20
  } else {
21
- diffRef = "all";
21
+ diffRef = "working-copy";
22
22
  }
23
23
  try {
24
24
  const result = await startReview({
@@ -66,57 +66,29 @@ Before opening a new review, check if \`diffprism watch\` is already running. Lo
66
66
 
67
67
  - **Do NOT call \`open_review\`** (the browser is already open with live-updating diffs)
68
68
  - Instead, call \`mcp__diffprism__update_review_context\` to push your reasoning to the existing watch session
69
- - Tell the user: "DiffPrism watch is running \u2014 pushed reasoning to the live review."
70
- - Skip the remaining steps
69
+ - Then **immediately** call \`mcp__diffprism__get_review_result\` with \`wait: true\` to block until the developer submits their review
70
+ - Tell the user: "DiffPrism watch is running \u2014 pushed reasoning to the live review. Waiting for your feedback..."
71
+ - When the result comes back, handle it per step 5 below
72
+ - Skip steps 2-4
71
73
 
72
- ### 1b. Check for Pending Review Feedback
74
+ ### 2. Load Configuration
73
75
 
74
- If watch mode is running, call \`mcp__diffprism__get_review_result\` to check for pending review feedback from the developer. If a result is returned:
75
-
76
- - **\`approved\`** \u2014 Acknowledge approval and continue with your current task.
77
- - **\`approved_with_comments\`** \u2014 Note the comments, address any actionable feedback.
78
- - **\`changes_requested\`** \u2014 Read the comments carefully, make the requested changes, then push updated reasoning via \`mcp__diffprism__update_review_context\`.
79
-
80
- If no pending result, continue normally.
81
-
82
- ### 2. Check for Configuration
83
-
84
- Look for \`diffprism.config.json\` at the project root. If it exists, read it for preferences:
76
+ Look for \`diffprism.config.json\` at the project root. If it exists, read it for preferences. If it doesn't exist, use defaults silently \u2014 do not prompt or create the file.
85
77
 
86
78
  \`\`\`json
87
79
  {
88
80
  "reviewTrigger": "ask | before_commit | always",
89
- "defaultDiffScope": "staged | unstaged | all",
81
+ "defaultDiffScope": "staged | unstaged | working-copy",
90
82
  "includeReasoning": true | false
91
83
  }
92
84
  \`\`\`
93
85
 
94
86
  **Defaults** (when fields are missing or file doesn't exist):
95
87
  - \`reviewTrigger\`: \`"ask"\`
96
- - \`defaultDiffScope\`: \`"all"\`
88
+ - \`defaultDiffScope\`: \`"working-copy"\`
97
89
  - \`includeReasoning\`: \`true\`
98
90
 
99
- ### 3. First-Run Onboarding
100
-
101
- If \`diffprism.config.json\` does **not** exist, ask the user these questions before proceeding:
102
-
103
- 1. **"When should I open DiffPrism reviews?"**
104
- - \`"ask"\` \u2014 Only when you explicitly ask (default)
105
- - \`"before_commit"\` \u2014 Automatically before every commit
106
- - \`"always"\` \u2014 After every code change
107
-
108
- 2. **"What should the default diff scope be?"**
109
- - \`"all"\` \u2014 All changes, staged and unstaged (default)
110
- - \`"staged"\` \u2014 Only staged changes
111
- - \`"unstaged"\` \u2014 Only unstaged changes
112
-
113
- 3. **"Should I include my reasoning about the changes in reviews?"**
114
- - Yes (default)
115
- - No
116
-
117
- After collecting answers, create \`diffprism.config.json\` at the project root with the user's choices. Then proceed to open the review.
118
-
119
- ### 4. Open the Review
91
+ ### 3. Open the Review
120
92
 
121
93
  Call \`mcp__diffprism__open_review\` with:
122
94
 
@@ -125,7 +97,7 @@ Call \`mcp__diffprism__open_review\` with:
125
97
  - \`description\`: A brief description of what changed and why.
126
98
  - \`reasoning\`: If \`includeReasoning\` is \`true\`, include your reasoning about the implementation decisions.
127
99
 
128
- ### 5. Handle the Result
100
+ ### 4. Handle the Result
129
101
 
130
102
  The tool blocks until the user submits their review in the browser. When it returns:
131
103
 
@@ -133,21 +105,24 @@ The tool blocks until the user submits their review in the browser. When it retu
133
105
  - **\`approved_with_comments\`** \u2014 Note the comments, address any actionable feedback.
134
106
  - **\`changes_requested\`** \u2014 Read the comments carefully, make the requested changes, and offer to open another review.
135
107
 
136
- ### 6. Error Handling
108
+ ### 5. Error Handling
137
109
 
138
110
  If the \`mcp__diffprism__open_review\` tool is not available:
139
- - Tell the user: "The DiffPrism MCP server isn't configured. Run \`npx diffprism setup\` to set it up, then restart Claude Code."
111
+ - Tell the user: "The DiffPrism MCP server isn't configured. Run \`npx diffprism start\` to set it up, then restart Claude Code."
112
+
113
+ ## Watch Mode: Waiting for Review Feedback
140
114
 
141
- ## Watch Mode: Polling for Review Feedback
115
+ When \`diffprism watch\` is active (detected via \`.diffprism/watch.json\`), the developer can submit reviews at any time in the browser.
142
116
 
143
- When \`diffprism watch\` is active (detected via \`.diffprism/watch.json\`), the developer can submit reviews at any time in the browser. Since there is no push notification, **you must poll for feedback** to close the loop.
117
+ **After pushing context to a watch session**, call \`mcp__diffprism__get_review_result\` with \`wait: true\` to block until the developer submits their review. This polls the result file every 2 seconds and returns as soon as feedback is available (up to 5 minutes by default).
144
118
 
145
- **After pushing context to a watch session**, call \`mcp__diffprism__get_review_result\` to check for pending feedback:
146
- - **Between tasks** \u2014 Before starting a new piece of work, check for feedback.
147
- - **After making changes** \u2014 After addressing requested changes, push updated reasoning via \`update_review_context\`, then check again shortly after.
148
- - **When the user mentions review feedback** \u2014 If the user says they submitted a review or left comments, check immediately.
119
+ Use this pattern:
120
+ 1. Push context via \`update_review_context\`
121
+ 2. Call \`get_review_result\` with \`wait: true\` \u2014 this blocks until the developer submits
122
+ 3. Handle the result (approved, changes_requested, etc.)
123
+ 4. If changes were requested, make fixes, push updated context, and call \`get_review_result\` with \`wait: true\` again
149
124
 
150
- Do not poll in a tight loop. Check at natural breakpoints in your workflow (e.g., after finishing a subtask, before committing, before moving to the next file).
125
+ You can also check for feedback without blocking by calling \`get_review_result\` without \`wait\` at natural breakpoints (between tasks, before committing, etc.).
151
126
 
152
127
  ## Behavior Rules
153
128
 
@@ -156,7 +131,7 @@ Do not poll in a tight loop. Check at natural breakpoints in your workflow (e.g.
156
131
  - \`"ask"\` \u2014 Never auto-review; only review when the user asks.
157
132
  - \`"before_commit"\` \u2014 Open a review before creating any git commit.
158
133
  - \`"always"\` \u2014 Open a review after any code change.
159
- - To re-run onboarding, the user can delete \`diffprism.config.json\` and invoke \`/review\` again.
134
+ - Power users can create \`diffprism.config.json\` manually to customize defaults.
160
135
  `;
161
136
 
162
137
  // cli/src/commands/setup.ts
@@ -352,11 +327,14 @@ async function setup(flags) {
352
327
  "Error: Not in a git repository. Run this command from inside a git project."
353
328
  );
354
329
  process.exit(1);
355
- return;
330
+ return { created: [], updated: [], skipped: [] };
356
331
  }
357
332
  const force = flags.force ?? false;
358
333
  const global = flags.global ?? false;
359
- console.log("Setting up DiffPrism for Claude Code...\n");
334
+ const quiet = flags.quiet ?? false;
335
+ if (!quiet) {
336
+ console.log("Setting up DiffPrism for Claude Code...\n");
337
+ }
360
338
  const result = { created: [], updated: [], skipped: [] };
361
339
  const gitignore = await setupGitignore(gitRoot);
362
340
  result[gitignore.action].push(gitignore.filePath);
@@ -365,40 +343,91 @@ async function setup(flags) {
365
343
  const settings = setupClaudeSettings(gitRoot, force);
366
344
  result[settings.action === "skipped" ? "skipped" : settings.action === "created" ? "created" : "updated"].push(settings.filePath);
367
345
  const cleaned = cleanDiffprismHooks(gitRoot);
368
- if (cleaned.removed > 0) {
346
+ if (cleaned.removed > 0 && !quiet) {
369
347
  console.log(` Cleaned ${cleaned.removed} stale hook(s)`);
370
348
  }
371
349
  const hook = setupStopHook(gitRoot, force);
372
350
  result[hook.action === "skipped" ? "skipped" : hook.action === "created" ? "created" : "updated"].push(hook.filePath);
373
351
  const skill = setupSkill(gitRoot, global, force);
374
352
  result[skill.action === "skipped" ? "skipped" : skill.action === "created" ? "created" : "updated"].push(skill.filePath);
375
- if (result.created.length > 0) {
376
- console.log("Created:");
377
- for (const f of result.created) {
378
- console.log(` + ${path.relative(gitRoot, f)}`);
353
+ if (!quiet) {
354
+ if (result.created.length > 0) {
355
+ console.log("Created:");
356
+ for (const f of result.created) {
357
+ console.log(` + ${path.relative(gitRoot, f)}`);
358
+ }
379
359
  }
380
- }
381
- if (result.updated.length > 0) {
382
- console.log("Updated:");
383
- for (const f of result.updated) {
384
- console.log(` ~ ${path.relative(gitRoot, f)}`);
360
+ if (result.updated.length > 0) {
361
+ console.log("Updated:");
362
+ for (const f of result.updated) {
363
+ console.log(` ~ ${path.relative(gitRoot, f)}`);
364
+ }
365
+ }
366
+ if (result.skipped.length > 0) {
367
+ console.log("Skipped (already configured):");
368
+ for (const f of result.skipped) {
369
+ console.log(` - ${path.relative(gitRoot, f)}`);
370
+ }
385
371
  }
372
+ console.log("\n\u2713 DiffPrism configured for Claude Code.\n");
373
+ console.log("Next steps:");
374
+ console.log(" 1. Restart Claude Code to pick up the MCP configuration");
375
+ console.log(" 2. Use /review in Claude Code to review your changes\n");
376
+ console.log("Tip: Run `diffprism start` to combine setup + live watch mode.");
377
+ }
378
+ return result;
379
+ }
380
+
381
+ // cli/src/commands/start.ts
382
+ async function start(ref, flags) {
383
+ const outcome = await setup({
384
+ global: flags.global,
385
+ force: flags.force,
386
+ quiet: true
387
+ });
388
+ const hasChanges = outcome.created.length > 0 || outcome.updated.length > 0;
389
+ if (hasChanges) {
390
+ console.log("\u2713 DiffPrism configured for Claude Code.");
386
391
  }
387
- if (result.skipped.length > 0) {
388
- console.log("Skipped (already configured):");
389
- for (const f of result.skipped) {
390
- console.log(` - ${path.relative(gitRoot, f)}`);
392
+ let diffRef;
393
+ if (flags.staged) {
394
+ diffRef = "staged";
395
+ } else if (flags.unstaged) {
396
+ diffRef = "unstaged";
397
+ } else if (ref) {
398
+ diffRef = ref;
399
+ } else {
400
+ diffRef = "working-copy";
401
+ }
402
+ const pollInterval = flags.interval ? parseInt(flags.interval, 10) : 1e3;
403
+ try {
404
+ const handle = await startWatch({
405
+ diffRef,
406
+ title: flags.title,
407
+ cwd: process.cwd(),
408
+ dev: flags.dev,
409
+ pollInterval
410
+ });
411
+ console.log("Use /review in Claude Code to send changes for review.");
412
+ if (hasChanges) {
413
+ console.log(
414
+ "If this is your first time, restart Claude Code first to load the MCP server."
415
+ );
391
416
  }
417
+ const shutdown = async () => {
418
+ console.log("\nStopping DiffPrism...");
419
+ await handle.stop();
420
+ process.exit(0);
421
+ };
422
+ process.on("SIGINT", shutdown);
423
+ process.on("SIGTERM", shutdown);
424
+ await new Promise(() => {
425
+ });
426
+ } catch (err) {
427
+ const message = err instanceof Error ? err.message : String(err);
428
+ console.error(`Error: ${message}`);
429
+ process.exit(1);
392
430
  }
393
- console.log(
394
- "\nYou can now use /review in Claude Code to open a DiffPrism review."
395
- );
396
- console.log(
397
- "Or run `diffprism watch --staged` for live-updating reviews."
398
- );
399
- console.log(
400
- "If Claude Code is running, restart it to pick up the new configuration."
401
- );
402
431
  }
403
432
 
404
433
  // cli/src/commands/watch.ts
@@ -411,7 +440,7 @@ async function watch(ref, flags) {
411
440
  } else if (ref) {
412
441
  diffRef = ref;
413
442
  } else {
414
- diffRef = "all";
443
+ diffRef = "working-copy";
415
444
  }
416
445
  const pollInterval = flags.interval ? parseInt(flags.interval, 10) : 1e3;
417
446
  try {
@@ -463,10 +492,13 @@ async function notifyStop() {
463
492
 
464
493
  // cli/src/index.ts
465
494
  var program = new Command();
466
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.13.8" : "0.0.0-dev");
495
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.14.0" : "0.0.0-dev");
467
496
  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);
497
+ 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);
468
498
  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);
469
499
  program.command("notify-stop").description("Signal the watch server to refresh (used by Claude Code hooks)").action(notifyStop);
470
500
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
471
- program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(setup);
501
+ program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action((flags) => {
502
+ setup(flags);
503
+ });
472
504
  program.parse();
@@ -303,6 +303,9 @@ function parseDiff(rawDiff, baseRef, headRef) {
303
303
 
304
304
  // packages/git/src/index.ts
305
305
  function getDiff(ref, options) {
306
+ if (ref === "working-copy") {
307
+ return getWorkingCopyDiff(options);
308
+ }
306
309
  const rawDiff = getGitDiff(ref, options);
307
310
  let baseRef;
308
311
  let headRef;
@@ -323,6 +326,29 @@ function getDiff(ref, options) {
323
326
  const diffSet = parseDiff(rawDiff, baseRef, headRef);
324
327
  return { diffSet, rawDiff };
325
328
  }
329
+ function getWorkingCopyDiff(options) {
330
+ const stagedRaw = getGitDiff("staged", options);
331
+ const unstagedRaw = getGitDiff("unstaged", options);
332
+ const stagedDiffSet = parseDiff(stagedRaw, "HEAD", "staged");
333
+ const unstagedDiffSet = parseDiff(unstagedRaw, "staged", "working tree");
334
+ const stagedFiles = stagedDiffSet.files.map((f) => ({
335
+ ...f,
336
+ stage: "staged"
337
+ }));
338
+ const unstagedFiles = unstagedDiffSet.files.map((f) => ({
339
+ ...f,
340
+ stage: "unstaged"
341
+ }));
342
+ const rawDiff = [stagedRaw, unstagedRaw].filter(Boolean).join("");
343
+ return {
344
+ diffSet: {
345
+ baseRef: "HEAD",
346
+ headRef: "working tree",
347
+ files: [...stagedFiles, ...unstagedFiles]
348
+ },
349
+ rawDiff
350
+ };
351
+ }
326
352
 
327
353
  // packages/analysis/src/deterministic.ts
328
354
  function categorizeFiles(files) {
@@ -1173,25 +1199,29 @@ function createWatchBridge(port, callbacks) {
1173
1199
  function hashDiff(rawDiff) {
1174
1200
  return createHash("sha256").update(rawDiff).digest("hex");
1175
1201
  }
1202
+ function fileKey(file) {
1203
+ return file.stage ? `${file.stage}:${file.path}` : file.path;
1204
+ }
1176
1205
  function detectChangedFiles(oldDiffSet, newDiffSet) {
1177
1206
  if (!oldDiffSet) {
1178
- return newDiffSet.files.map((f) => f.path);
1207
+ return newDiffSet.files.map(fileKey);
1179
1208
  }
1180
1209
  const oldFiles = new Map(
1181
- oldDiffSet.files.map((f) => [f.path, f])
1210
+ oldDiffSet.files.map((f) => [fileKey(f), f])
1182
1211
  );
1183
1212
  const changed = [];
1184
1213
  for (const newFile of newDiffSet.files) {
1185
- const oldFile = oldFiles.get(newFile.path);
1214
+ const key = fileKey(newFile);
1215
+ const oldFile = oldFiles.get(key);
1186
1216
  if (!oldFile) {
1187
- changed.push(newFile.path);
1217
+ changed.push(key);
1188
1218
  } else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
1189
- changed.push(newFile.path);
1219
+ changed.push(key);
1190
1220
  }
1191
1221
  }
1192
1222
  for (const oldFile of oldDiffSet.files) {
1193
- if (!newDiffSet.files.some((f) => f.path === oldFile.path)) {
1194
- changed.push(oldFile.path);
1223
+ if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
1224
+ changed.push(fileKey(oldFile));
1195
1225
  }
1196
1226
  }
1197
1227
  return changed;
@@ -3,7 +3,7 @@ import {
3
3
  readReviewResult,
4
4
  readWatchFile,
5
5
  startReview
6
- } from "./chunk-QB2PKDLU.js";
6
+ } from "./chunk-TVXIMP3G.js";
7
7
 
8
8
  // packages/mcp-server/src/index.ts
9
9
  import fs from "fs";
@@ -14,14 +14,14 @@ import { z } from "zod";
14
14
  async function startMcpServer() {
15
15
  const server = new McpServer({
16
16
  name: "diffprism",
17
- version: true ? "0.13.8" : "0.0.0-dev"
17
+ version: true ? "0.14.0" : "0.0.0-dev"
18
18
  });
19
19
  server.tool(
20
20
  "open_review",
21
21
  "Open a browser-based code review for local git changes. Blocks until the engineer submits their review decision.",
22
22
  {
23
23
  diff_ref: z.string().describe(
24
- 'Git diff reference: "staged", "unstaged", or a ref range like "HEAD~3..HEAD"'
24
+ 'Git diff reference: "staged", "unstaged", "working-copy" (staged+unstaged grouped), or a ref range like "HEAD~3..HEAD"'
25
25
  ),
26
26
  title: z.string().optional().describe("Title for the review"),
27
27
  description: z.string().optional().describe("Description of the changes"),
@@ -124,10 +124,41 @@ async function startMcpServer() {
124
124
  );
125
125
  server.tool(
126
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 () => {
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).",
128
+ {
129
+ wait: z.boolean().optional().describe("If true, poll until a review result is available (blocks up to timeout)"),
130
+ timeout: z.number().optional().describe("Max wait time in seconds when wait=true (default: 300, max: 600)")
131
+ },
132
+ async ({ wait, timeout }) => {
130
133
  try {
134
+ if (wait) {
135
+ const maxWaitMs = Math.min(timeout ?? 300, 600) * 1e3;
136
+ const pollIntervalMs = 2e3;
137
+ const start = Date.now();
138
+ while (Date.now() - start < maxWaitMs) {
139
+ const data2 = readReviewResult();
140
+ if (data2) {
141
+ consumeReviewResult();
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: JSON.stringify(data2.result, null, 2)
147
+ }
148
+ ]
149
+ };
150
+ }
151
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
152
+ }
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: "No review result received within timeout."
158
+ }
159
+ ]
160
+ };
161
+ }
131
162
  const data = readReviewResult();
132
163
  if (!data) {
133
164
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.13.8",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {