diffprism 0.16.0 → 0.17.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/README.md CHANGED
@@ -15,6 +15,8 @@ DiffPrism gives you a visual review step for AI-written code — stage your chan
15
15
  - **Keyboard shortcuts** — `j`/`k` navigate files, `s` cycles file status
16
16
  - **Three-way decisions** — approve, request changes, or approve with comments
17
17
  - **Branch display** — current git branch shown in the review header
18
+ - **Global server mode** — `diffprism server` runs a persistent multi-session review server, multiple agents post reviews to one browser tab
19
+ - **Multi-session UI** — session list with status badges, branch info, file counts, and change stats when running in server mode
18
20
 
19
21
  ## Quick Start
20
22
 
@@ -85,6 +87,32 @@ When `diffprism watch` is running:
85
87
 
86
88
  Stop the watcher with `Ctrl+C`.
87
89
 
90
+ ### Global Server Mode (multi-session)
91
+
92
+ Run a persistent server that accepts reviews from multiple Claude Code sessions and displays them in one browser tab:
93
+
94
+ ```bash
95
+ # Start the global server (auto-runs global setup if needed)
96
+ diffprism server
97
+
98
+ # Check status and list active sessions
99
+ diffprism server status
100
+
101
+ # Stop the server
102
+ diffprism server stop
103
+ ```
104
+
105
+ When the global server is running, MCP tools automatically detect it and route reviews there instead of opening ephemeral browser tabs. Each review appears as a session in the multi-session UI — click to switch between them.
106
+
107
+ **Global setup** (optional, `diffprism server` runs this automatically):
108
+
109
+ ```bash
110
+ # Configure skill + permissions globally (no git repo required)
111
+ diffprism setup --global
112
+ ```
113
+
114
+ Per-project MCP registration (`.mcp.json`) is still needed via `diffprism setup` in each project.
115
+
88
116
  ## MCP Tool Reference
89
117
 
90
118
  The MCP server exposes three tools:
@@ -162,12 +190,12 @@ npx tsc --noEmit -p packages/core/tsconfig.json # Type-check a package
162
190
  ### Project Structure
163
191
 
164
192
  ```
165
- packages/core — Shared types, pipeline orchestrator, WebSocket bridge
193
+ packages/core — Shared types, pipeline orchestrator, WebSocket bridge, global server
166
194
  packages/git — Git diff extraction + unified diff parser
167
195
  packages/analysis — Deterministic review briefing (complexity, test gaps, patterns)
168
- packages/ui — React 19 + Vite 6 + Tailwind + Zustand diff viewer
169
- packages/mcp-server — MCP tool server (open_review)
170
- cli/ — Commander CLI entry point
196
+ packages/ui — React 19 + Vite 6 + Tailwind + Zustand diff viewer + session list
197
+ packages/mcp-server — MCP tool server, auto-routes to global server when available
198
+ cli/ — Commander CLI (review, serve, setup, server commands)
171
199
  ```
172
200
 
173
201
  ### Requirements
package/dist/bin.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  startGlobalServer,
7
7
  startReview,
8
8
  startWatch
9
- } from "./chunk-4WN4FIY4.js";
9
+ } from "./chunk-4VXA6GCO.js";
10
10
 
11
11
  // cli/src/index.ts
12
12
  import { Command } from "commander";
@@ -111,7 +111,13 @@ The tool blocks until the user submits their review in the browser. When it retu
111
111
  ### 5. Error Handling
112
112
 
113
113
  If the \`mcp__diffprism__open_review\` tool is not available:
114
- - Tell the user: "The DiffPrism MCP server isn't configured. Run \`npx diffprism start\` to set it up, then restart Claude Code."
114
+ - Tell the user: "The DiffPrism MCP server isn't configured. Run \`npx diffprism setup\` to set it up, then restart Claude Code."
115
+
116
+ ## Global Server Mode
117
+
118
+ When a global DiffPrism server is running (\`diffprism server\`), the MCP tools automatically detect it and route reviews there instead of opening a new browser tab each time. The review appears in the server's multi-session UI at the existing browser tab.
119
+
120
+ This is transparent \u2014 the same \`open_review\`, \`update_review_context\`, and \`get_review_result\` tools work the same way. No changes to the workflow are needed.
115
121
 
116
122
  ## Watch Mode: Waiting for Review Feedback
117
123
 
@@ -179,8 +185,8 @@ function setupMcpJson(gitRoot, force) {
179
185
  writeJsonFile(filePath, { ...existing, mcpServers: servers });
180
186
  return { action, filePath };
181
187
  }
182
- function setupClaudeSettings(gitRoot, force) {
183
- const filePath = path.join(gitRoot, ".claude", "settings.json");
188
+ function setupClaudeSettings(baseDir, force) {
189
+ const filePath = path.join(baseDir, ".claude", "settings.json");
184
190
  const existing = readJsonFile(filePath);
185
191
  const permissions = existing.permissions ?? {};
186
192
  const allow = permissions.allow ?? [];
@@ -324,54 +330,59 @@ async function setupGitignore(gitRoot) {
324
330
  return { action: "created", filePath };
325
331
  }
326
332
  async function setup(flags) {
333
+ const force = flags.force ?? false;
334
+ const global = flags.global ?? false;
335
+ const quiet = flags.quiet ?? false;
336
+ const result = { created: [], updated: [], skipped: [] };
337
+ const home = os.homedir();
338
+ if (global) {
339
+ if (!quiet) {
340
+ console.log("Setting up DiffPrism globally...\n");
341
+ }
342
+ const skill2 = setupSkill("", true, force);
343
+ result[skill2.action].push(skill2.filePath);
344
+ const settings2 = setupClaudeSettings(home, force);
345
+ result[settings2.action].push(settings2.filePath);
346
+ if (!quiet) {
347
+ printSummary(result, home);
348
+ console.log("\n\u2713 DiffPrism configured globally.\n");
349
+ console.log("Next steps:");
350
+ console.log(" 1. Run `diffprism server` to start the global review server");
351
+ console.log(" 2. In each project, run `diffprism setup` to register the MCP server");
352
+ console.log(" 3. Use /review in Claude Code to review your changes\n");
353
+ }
354
+ return result;
355
+ }
327
356
  const gitRoot = findGitRoot(process.cwd());
328
357
  if (!gitRoot) {
329
358
  console.error(
330
359
  "Error: Not in a git repository. Run this command from inside a git project."
331
360
  );
361
+ console.error(
362
+ "Tip: Use `diffprism setup --global` to configure DiffPrism globally without a git repo."
363
+ );
332
364
  process.exit(1);
333
365
  return { created: [], updated: [], skipped: [] };
334
366
  }
335
- const force = flags.force ?? false;
336
- const global = flags.global ?? false;
337
- const quiet = flags.quiet ?? false;
338
367
  if (!quiet) {
339
368
  console.log("Setting up DiffPrism for Claude Code...\n");
340
369
  }
341
- const result = { created: [], updated: [], skipped: [] };
342
370
  const gitignore = await setupGitignore(gitRoot);
343
371
  result[gitignore.action].push(gitignore.filePath);
344
372
  const mcp = setupMcpJson(gitRoot, force);
345
- result[mcp.action === "skipped" ? "skipped" : mcp.action === "created" ? "created" : "updated"].push(mcp.filePath);
373
+ result[mcp.action].push(mcp.filePath);
346
374
  const settings = setupClaudeSettings(gitRoot, force);
347
- result[settings.action === "skipped" ? "skipped" : settings.action === "created" ? "created" : "updated"].push(settings.filePath);
375
+ result[settings.action].push(settings.filePath);
348
376
  const cleaned = cleanDiffprismHooks(gitRoot);
349
377
  if (cleaned.removed > 0 && !quiet) {
350
378
  console.log(` Cleaned ${cleaned.removed} stale hook(s)`);
351
379
  }
352
380
  const hook = setupStopHook(gitRoot, force);
353
- result[hook.action === "skipped" ? "skipped" : hook.action === "created" ? "created" : "updated"].push(hook.filePath);
354
- const skill = setupSkill(gitRoot, global, force);
355
- result[skill.action === "skipped" ? "skipped" : skill.action === "created" ? "created" : "updated"].push(skill.filePath);
381
+ result[hook.action].push(hook.filePath);
382
+ const skill = setupSkill(gitRoot, false, force);
383
+ result[skill.action].push(skill.filePath);
356
384
  if (!quiet) {
357
- if (result.created.length > 0) {
358
- console.log("Created:");
359
- for (const f of result.created) {
360
- console.log(` + ${path.relative(gitRoot, f)}`);
361
- }
362
- }
363
- if (result.updated.length > 0) {
364
- console.log("Updated:");
365
- for (const f of result.updated) {
366
- console.log(` ~ ${path.relative(gitRoot, f)}`);
367
- }
368
- }
369
- if (result.skipped.length > 0) {
370
- console.log("Skipped (already configured):");
371
- for (const f of result.skipped) {
372
- console.log(` - ${path.relative(gitRoot, f)}`);
373
- }
374
- }
385
+ printSummary(result, gitRoot);
375
386
  console.log("\n\u2713 DiffPrism configured for Claude Code.\n");
376
387
  console.log("Next steps:");
377
388
  console.log(" 1. Restart Claude Code to pick up the MCP configuration");
@@ -380,6 +391,41 @@ async function setup(flags) {
380
391
  }
381
392
  return result;
382
393
  }
394
+ function printSummary(result, baseDir) {
395
+ if (result.created.length > 0) {
396
+ console.log("Created:");
397
+ for (const f of result.created) {
398
+ console.log(` + ${path.relative(baseDir, f) || f}`);
399
+ }
400
+ }
401
+ if (result.updated.length > 0) {
402
+ console.log("Updated:");
403
+ for (const f of result.updated) {
404
+ console.log(` ~ ${path.relative(baseDir, f) || f}`);
405
+ }
406
+ }
407
+ if (result.skipped.length > 0) {
408
+ console.log("Skipped (already configured):");
409
+ for (const f of result.skipped) {
410
+ console.log(` - ${path.relative(baseDir, f) || f}`);
411
+ }
412
+ }
413
+ }
414
+ function isGlobalSetupDone() {
415
+ const home = os.homedir();
416
+ const skillPath = path.join(home, ".claude", "skills", "review", "SKILL.md");
417
+ const settingsPath = path.join(home, ".claude", "settings.json");
418
+ if (!fs.existsSync(skillPath)) return false;
419
+ const settings = readJsonFile(settingsPath);
420
+ const permissions = settings.permissions ?? {};
421
+ const allow = permissions.allow ?? [];
422
+ const toolNames = [
423
+ "mcp__diffprism__open_review",
424
+ "mcp__diffprism__update_review_context",
425
+ "mcp__diffprism__get_review_result"
426
+ ];
427
+ return toolNames.every((t) => allow.includes(t));
428
+ }
383
429
 
384
430
  // cli/src/commands/start.ts
385
431
  async function start(ref, flags) {
@@ -502,6 +548,11 @@ async function server(flags) {
502
548
  process.exit(1);
503
549
  return;
504
550
  }
551
+ if (!isGlobalSetupDone()) {
552
+ console.log("Running global setup...\n");
553
+ await setup({ global: true, quiet: false });
554
+ console.log("");
555
+ }
505
556
  const httpPort = flags.port ? parseInt(flags.port, 10) : void 0;
506
557
  const wsPort = flags.wsPort ? parseInt(flags.wsPort, 10) : void 0;
507
558
  try {
@@ -578,13 +629,13 @@ async function serverStop() {
578
629
 
579
630
  // cli/src/index.ts
580
631
  var program = new Command();
581
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.16.0" : "0.0.0-dev");
632
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.17.1" : "0.0.0-dev");
582
633
  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
634
  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
635
  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);
585
636
  program.command("notify-stop").description("Signal the watch server to refresh (used by Claude Code hooks)").action(notifyStop);
586
637
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
587
- 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) => {
638
+ 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").action((flags) => {
588
639
  setup(flags);
589
640
  });
590
641
  var serverCmd = program.command("server").description("Start the global DiffPrism server for multi-session reviews").option("-p, --port <port>", "HTTP API port (default: 24680)").option("--ws-port <port>", "WebSocket port (default: 24681)").option("--dev", "Use Vite dev server with HMR instead of static files").action(server);
@@ -1447,6 +1447,7 @@ import open3 from "open";
1447
1447
  import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
1448
1448
  var sessions2 = /* @__PURE__ */ new Map();
1449
1449
  var clientSessions = /* @__PURE__ */ new Map();
1450
+ var reopenBrowserIfNeeded = null;
1450
1451
  function toSummary(session) {
1451
1452
  const { payload } = session;
1452
1453
  const fileCount = payload.diffSet.files.length;
@@ -1558,6 +1559,7 @@ async function handleApiRequest(req, res) {
1558
1559
  type: "session:added",
1559
1560
  payload: toSummary(session)
1560
1561
  });
1562
+ reopenBrowserIfNeeded?.();
1561
1563
  jsonResponse(res, 201, { sessionId });
1562
1564
  } catch {
1563
1565
  jsonResponse(res, 400, { error: "Invalid request body" });
@@ -1770,6 +1772,18 @@ Waiting for reviews...
1770
1772
  }
1771
1773
  const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&serverMode=true`;
1772
1774
  await open3(uiUrl);
1775
+ function hasConnectedClients() {
1776
+ if (!wss) return false;
1777
+ for (const client of wss.clients) {
1778
+ if (client.readyState === WebSocket3.OPEN) return true;
1779
+ }
1780
+ return false;
1781
+ }
1782
+ reopenBrowserIfNeeded = () => {
1783
+ if (!hasConnectedClients()) {
1784
+ open3(uiUrl);
1785
+ }
1786
+ };
1773
1787
  async function stop() {
1774
1788
  if (wss) {
1775
1789
  for (const client of wss.clients) {
@@ -1780,6 +1794,7 @@ Waiting for reviews...
1780
1794
  }
1781
1795
  clientSessions.clear();
1782
1796
  sessions2.clear();
1797
+ reopenBrowserIfNeeded = null;
1783
1798
  await new Promise((resolve) => {
1784
1799
  httpServer.close(() => resolve());
1785
1800
  });
@@ -7,7 +7,7 @@ import {
7
7
  readReviewResult,
8
8
  readWatchFile,
9
9
  startReview
10
- } from "./chunk-4WN4FIY4.js";
10
+ } from "./chunk-4VXA6GCO.js";
11
11
 
12
12
  // packages/mcp-server/src/index.ts
13
13
  import fs from "fs";
@@ -76,7 +76,7 @@ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
76
76
  async function startMcpServer() {
77
77
  const server = new McpServer({
78
78
  name: "diffprism",
79
- version: true ? "0.16.0" : "0.0.0-dev"
79
+ version: true ? "0.17.1" : "0.0.0-dev"
80
80
  });
81
81
  server.tool(
82
82
  "open_review",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {