diffprism 0.20.2 → 0.21.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 CHANGED
@@ -12,11 +12,12 @@ DiffPrism gives you a visual review step for AI-written code — stage your chan
12
12
  - **Review briefing bar** — summary stats, complexity scoring, test coverage gaps, pattern flags, and dependency tracking
13
13
  - **Agent reasoning panel** — see why the AI made each change
14
14
  - **Dark/light mode** — toggle with theme persistence
15
- - **Keyboard shortcuts** — `j`/`k` navigate files, `s` cycles file status
15
+ - **Keyboard shortcuts** — `j`/`k` navigate files, `s` cycles file status, `Cmd/Ctrl+Enter` saves comments, `?` toggles hotkey guide
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
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
19
+ - **Multi-session UI** — session list with live status badges, branch info, file counts, and change stats; stale sessions auto-expire
20
+ - **One-command setup & teardown** — `diffprism setup` configures Claude Code integration, `diffprism teardown` cleanly reverses it
20
21
 
21
22
  ## Quick Start
22
23
 
@@ -37,6 +38,14 @@ This single command configures everything:
37
38
 
38
39
  After running, restart Claude Code. The first time you use `/review`, Claude will ask your preferences and save them to `diffprism.config.json`.
39
40
 
41
+ To remove DiffPrism from a project:
42
+
43
+ ```bash
44
+ npx diffprism teardown
45
+ ```
46
+
47
+ This reverses all changes made by `setup`: removes DiffPrism entries from `.mcp.json`, permissions, hooks, the skill file, `.gitignore`, and the `.diffprism/` directory. Non-DiffPrism entries are preserved.
48
+
40
49
  See the [full setup guide](docs/claude-setup.md) for manual configuration, Claude Desktop config, troubleshooting, and advanced options.
41
50
 
42
51
  ### Use from the CLI
@@ -64,6 +73,18 @@ diffprism review --staged --title "Add auth middleware"
64
73
 
65
74
  A browser window opens with the diff viewer. Review the changes and click **Approve**, **Request Changes**, or **Approve with Comments**.
66
75
 
76
+ ### Quick Start (setup + watch in one command)
77
+
78
+ ```bash
79
+ # Configure Claude Code integration and start watching in one step
80
+ diffprism start
81
+
82
+ # With flags
83
+ diffprism start --staged --title "Working on auth"
84
+ ```
85
+
86
+ This runs `diffprism setup` (silently, if already configured) then starts watch mode. Ideal for first-time use or when switching projects.
87
+
67
88
  ### Watch Mode (live-updating)
68
89
 
69
90
  Keep a persistent browser tab that auto-refreshes as files change — ideal for reviewing while an agent is working:
@@ -102,13 +123,16 @@ diffprism server status
102
123
  diffprism server stop
103
124
  ```
104
125
 
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.
126
+ 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. Session status badges update live, and submitted sessions auto-expire after 5 minutes.
106
127
 
107
128
  **Global setup** (optional, `diffprism server` runs this automatically):
108
129
 
109
130
  ```bash
110
131
  # Configure skill + permissions globally (no git repo required)
111
132
  diffprism setup --global
133
+
134
+ # Remove global configuration
135
+ diffprism teardown --global
112
136
  ```
113
137
 
114
138
  Per-project MCP registration (`.mcp.json`) is still needed via `diffprism setup` in each project.
@@ -123,14 +147,14 @@ Opens a browser-based code review. Blocks until the engineer submits their decis
123
147
 
124
148
  | Parameter | Required | Description |
125
149
  |---------------|----------|-------------------------------------------------------------------|
126
- | `diff_ref` | Yes | `"staged"`, `"unstaged"`, or a git ref range (e.g. `"HEAD~3..HEAD"`, `"main..feature"`) |
150
+ | `diff_ref` | Yes | `"staged"`, `"unstaged"`, `"working-copy"` (staged+unstaged grouped), or a git ref range (e.g. `"HEAD~3..HEAD"`, `"main..feature"`) |
127
151
  | `title` | No | Title displayed in the review UI |
128
152
  | `description` | No | Description of the changes |
129
153
  | `reasoning` | No | Agent reasoning about why the changes were made (shown in the reasoning panel) |
130
154
 
131
155
  ### `update_review_context`
132
156
 
133
- Pushes reasoning/context to a running `diffprism watch` session. Non-blocking — returns immediately.
157
+ Pushes reasoning/context to a running DiffPrism session (watch mode or global server). Non-blocking — returns immediately.
134
158
 
135
159
  | Parameter | Required | Description |
136
160
  |---------------|----------|------------------------------------------------|
@@ -140,9 +164,12 @@ Pushes reasoning/context to a running `diffprism watch` session. Non-blocking
140
164
 
141
165
  ### `get_review_result`
142
166
 
143
- Fetches the most recent review result from a `diffprism watch` session. Non-blocking returns immediately. The result is marked as consumed after retrieval so it won't be returned again.
167
+ Fetches the most recent review result from a DiffPrism session (watch mode or global server). The result is marked as consumed after retrieval so it won't be returned again.
144
168
 
145
- No parameters.
169
+ | Parameter | Required | Description |
170
+ |-----------|----------|-------------------------------------------------------------------|
171
+ | `wait` | No | If `true`, poll until a result is available (blocks up to timeout) |
172
+ | `timeout` | No | Max wait time in seconds when `wait=true` (default: 300, max: 600) |
146
173
 
147
174
  **Returns (all tools):** A `ReviewResult` JSON object:
148
175
 
@@ -195,7 +222,7 @@ packages/git — Git diff extraction + unified diff parser
195
222
  packages/analysis — Deterministic review briefing (complexity, test gaps, patterns)
196
223
  packages/ui — React 19 + Vite 6 + Tailwind + Zustand diff viewer + session list
197
224
  packages/mcp-server — MCP tool server, auto-routes to global server when available
198
- cli/ — Commander CLI (review, serve, setup, server commands)
225
+ cli/ — Commander CLI (review, start, watch, setup, teardown, server commands)
199
226
  ```
200
227
 
201
228
  ### Requirements
@@ -207,4 +234,5 @@ cli/ — Commander CLI (review, serve, setup, server commands)
207
234
  ## Documentation
208
235
 
209
236
  - [Claude Code / Claude Desktop Setup Guide](docs/claude-setup.md) — detailed MCP configuration, auto-approval, and troubleshooting
237
+ - [Workflows Guide](docs/workflows.md) — ephemeral, watch, and global server modes explained
210
238
  - [UX Design Notes](docs/ux-design-notes.md) — design decisions, CLI defaults rationale, and multi-agent workflow thinking
package/dist/bin.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  startGlobalServer,
7
7
  startReview,
8
8
  startWatch
9
- } from "./chunk-3CQCLFF3.js";
9
+ } from "./chunk-3IVV6O45.js";
10
10
 
11
11
  // cli/src/index.ts
12
12
  import { Command } from "commander";
@@ -832,7 +832,7 @@ async function serverStop() {
832
832
 
833
833
  // cli/src/index.ts
834
834
  var program = new Command();
835
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.20.2" : "0.0.0-dev");
835
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.21.0" : "0.0.0-dev");
836
836
  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);
837
837
  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);
838
838
  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);
@@ -1447,6 +1447,9 @@ import { randomUUID } from "crypto";
1447
1447
  import getPort3 from "get-port";
1448
1448
  import open3 from "open";
1449
1449
  import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
1450
+ var SUBMITTED_TTL_MS = 5 * 60 * 1e3;
1451
+ var ABANDONED_TTL_MS = 60 * 60 * 1e3;
1452
+ var CLEANUP_INTERVAL_MS = 60 * 1e3;
1450
1453
  var sessions2 = /* @__PURE__ */ new Map();
1451
1454
  var clientSessions = /* @__PURE__ */ new Map();
1452
1455
  var sessionWatchers = /* @__PURE__ */ new Map();
@@ -1523,6 +1526,23 @@ function sendToSessionClients(sessionId, msg) {
1523
1526
  }
1524
1527
  }
1525
1528
  }
1529
+ function broadcastSessionUpdate(session) {
1530
+ broadcastToAll({
1531
+ type: "session:updated",
1532
+ payload: toSummary(session)
1533
+ });
1534
+ }
1535
+ function broadcastSessionRemoved(sessionId) {
1536
+ for (const [client, sid] of clientSessions.entries()) {
1537
+ if (sid === sessionId) {
1538
+ clientSessions.delete(client);
1539
+ }
1540
+ }
1541
+ broadcastToAll({
1542
+ type: "session:removed",
1543
+ payload: { sessionId }
1544
+ });
1545
+ }
1526
1546
  function hasViewersForSession(sessionId) {
1527
1547
  for (const [client, sid] of clientSessions.entries()) {
1528
1548
  if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
@@ -1614,7 +1634,7 @@ async function handleApiRequest(req, res) {
1614
1634
  const method = req.method ?? "GET";
1615
1635
  const url = (req.url ?? "/").split("?")[0];
1616
1636
  res.setHeader("Access-Control-Allow-Origin", "*");
1617
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1637
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1618
1638
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1619
1639
  if (method === "OPTIONS") {
1620
1640
  res.writeHead(204);
@@ -1696,7 +1716,7 @@ async function handleApiRequest(req, res) {
1696
1716
  const result = JSON.parse(body);
1697
1717
  session.result = result;
1698
1718
  session.status = "submitted";
1699
- broadcastToAll({ type: "session:updated", payload: toSummary(session) });
1719
+ broadcastSessionUpdate(session);
1700
1720
  jsonResponse(res, 200, { ok: true });
1701
1721
  } catch {
1702
1722
  jsonResponse(res, 400, { error: "Invalid request body" });
@@ -1750,6 +1770,7 @@ async function handleApiRequest(req, res) {
1750
1770
  if (deleteParams) {
1751
1771
  stopSessionWatcher(deleteParams.id);
1752
1772
  if (sessions2.delete(deleteParams.id)) {
1773
+ broadcastSessionRemoved(deleteParams.id);
1753
1774
  jsonResponse(res, 200, { ok: true });
1754
1775
  } else {
1755
1776
  jsonResponse(res, 404, { error: "Session not found" });
@@ -1802,7 +1823,7 @@ async function startGlobalServer(options = {}) {
1802
1823
  if (session) {
1803
1824
  session.status = "in_review";
1804
1825
  session.hasNewChanges = false;
1805
- broadcastToAll({ type: "session:updated", payload: toSummary(session) });
1826
+ broadcastSessionUpdate(session);
1806
1827
  const msg = {
1807
1828
  type: "review:init",
1808
1829
  payload: session.payload
@@ -1822,7 +1843,7 @@ async function startGlobalServer(options = {}) {
1822
1843
  clientSessions.set(ws, session.id);
1823
1844
  session.status = "in_review";
1824
1845
  session.hasNewChanges = false;
1825
- broadcastToAll({ type: "session:updated", payload: toSummary(session) });
1846
+ broadcastSessionUpdate(session);
1826
1847
  ws.send(JSON.stringify({
1827
1848
  type: "review:init",
1828
1849
  payload: session.payload
@@ -1840,7 +1861,7 @@ async function startGlobalServer(options = {}) {
1840
1861
  if (session) {
1841
1862
  session.result = msg.payload;
1842
1863
  session.status = "submitted";
1843
- broadcastToAll({ type: "session:updated", payload: toSummary(session) });
1864
+ broadcastSessionUpdate(session);
1844
1865
  }
1845
1866
  }
1846
1867
  } else if (msg.type === "session:select") {
@@ -1850,7 +1871,7 @@ async function startGlobalServer(options = {}) {
1850
1871
  session.status = "in_review";
1851
1872
  session.hasNewChanges = false;
1852
1873
  startSessionWatcher(session.id);
1853
- broadcastToAll({ type: "session:updated", payload: toSummary(session) });
1874
+ broadcastSessionUpdate(session);
1854
1875
  ws.send(JSON.stringify({
1855
1876
  type: "review:init",
1856
1877
  payload: session.payload
@@ -1881,6 +1902,19 @@ async function startGlobalServer(options = {}) {
1881
1902
  httpServer.on("error", reject);
1882
1903
  httpServer.listen(httpPort, () => resolve());
1883
1904
  });
1905
+ function cleanupExpiredSessions() {
1906
+ const now = Date.now();
1907
+ for (const [id, session] of sessions2.entries()) {
1908
+ const age = now - session.createdAt;
1909
+ const expired = session.status === "submitted" && age > SUBMITTED_TTL_MS || session.status === "pending" && age > ABANDONED_TTL_MS;
1910
+ if (expired) {
1911
+ stopSessionWatcher(id);
1912
+ sessions2.delete(id);
1913
+ broadcastSessionRemoved(id);
1914
+ }
1915
+ }
1916
+ }
1917
+ const cleanupTimer = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
1884
1918
  const serverInfo = {
1885
1919
  httpPort,
1886
1920
  wsPort,
@@ -1907,6 +1941,7 @@ Waiting for reviews...
1907
1941
  }
1908
1942
  };
1909
1943
  async function stop() {
1944
+ clearInterval(cleanupTimer);
1910
1945
  stopAllWatchers();
1911
1946
  if (wss) {
1912
1947
  for (const client of wss.clients) {
@@ -7,7 +7,7 @@ import {
7
7
  readReviewResult,
8
8
  readWatchFile,
9
9
  startReview
10
- } from "./chunk-3CQCLFF3.js";
10
+ } from "./chunk-3IVV6O45.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.20.2" : "0.0.0-dev"
79
+ version: true ? "0.21.0" : "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.20.2",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {