diffprism 0.45.0 → 0.48.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
@@ -102,6 +102,41 @@ diffprism review main..feature # Branch diff
102
102
 
103
103
  Running multiple Claude Code sessions? All reviews appear in one browser dashboard with status badges, branch info, and desktop notifications.
104
104
 
105
+ ## Commit Gate
106
+
107
+ Review is only reliable if something other than memory triggers it. `diffprism hook`
108
+ wires the review into your pre-commit hook, so a substantial change opens a review
109
+ before it can land.
110
+
111
+ ```bash
112
+ diffprism hook install # Add the gate to this repo's pre-commit hook
113
+ diffprism hook uninstall # Remove it
114
+ ```
115
+
116
+ By default a staged diff of **120+ changed lines** opens a review; anything smaller
117
+ commits untouched. A gate that stops every commit is one you learn to skip with
118
+ `--no-verify`, and a skipped gate is worse than none. Tune it per repo:
119
+
120
+ ```bash
121
+ git config diffprism.gate-lines 200
122
+ ```
123
+
124
+ Approve and the commit proceeds. Request changes and the commit is blocked — with
125
+ your comments printed in the output, so the agent that ran `git commit` can read
126
+ what you asked for and fix it without another round trip:
127
+
128
+ ```
129
+ 140 staged lines (gate at 120) — opening DiffPrism review...
130
+
131
+ feature.ts:12 [must_fix] these should be a single exported record
132
+ feature.ts:88 [question] is this range meant to be inclusive?
133
+
134
+ Commit blocked: the review requested changes.
135
+ ```
136
+
137
+ Install adds one line between markers, so uninstall removes exactly that and leaves
138
+ the rest of your hook alone. Repos using `core.hooksPath` are handled.
139
+
105
140
  ## Features
106
141
 
107
142
  - **AI-powered PR review** — Your AI gets full codebase context via 14 MCP tools
@@ -124,6 +159,8 @@ diffprism setup --global # Global setup (no git repo needed)
124
159
  diffprism server # Start the background server
125
160
  diffprism server status # Check server status
126
161
  diffprism server stop # Stop the server
162
+ diffprism hook install # Gate commits on a review
163
+ diffprism hook uninstall # Remove the gate
127
164
  diffprism teardown # Remove configuration
128
165
  ```
129
166
 
package/dist/bin.js CHANGED
@@ -5,15 +5,19 @@ import {
5
5
  } from "./chunk-24B33UN6.js";
6
6
  import {
7
7
  demo
8
- } from "./chunk-RVI74JF5.js";
8
+ } from "./chunk-SWSRPUXO.js";
9
9
  import {
10
+ describeVersion,
10
11
  ensureServer,
12
+ getBuildInfo,
11
13
  isServerAlive,
12
14
  readServerFile,
13
15
  startGlobalServer,
14
16
  submitReviewToServer
15
- } from "./chunk-RICVPPTE.js";
16
- import "./chunk-QGWYCEJN.js";
17
+ } from "./chunk-AJGESVAM.js";
18
+ import {
19
+ getDiff
20
+ } from "./chunk-QGWYCEJN.js";
17
21
  import "./chunk-DHCVZGHE.js";
18
22
  import "./chunk-JSBRDJBE.js";
19
23
 
@@ -39,21 +43,26 @@ async function review(ref, flags) {
39
43
  await reviewLocalFlow(diffRef, flags);
40
44
  }
41
45
  } catch (err) {
42
- const message = err instanceof Error ? err.message : String(err);
43
- console.error(`Error: ${message}`);
46
+ const message2 = err instanceof Error ? err.message : String(err);
47
+ console.error(`Error: ${message2}`);
44
48
  process.exit(1);
45
49
  }
46
50
  }
47
51
  async function reviewLocalFlow(diffRef, flags) {
48
52
  const serverInfo = await ensureServer({ dev: flags.dev });
49
- console.log("Opening review in browser...");
53
+ console.error("Opening review in browser...");
50
54
  const { result } = await submitReviewToServer(serverInfo, diffRef, {
51
55
  title: flags.title,
52
56
  cwd: process.cwd(),
53
57
  diffRef
54
58
  });
55
59
  console.log(JSON.stringify(result, null, 2));
56
- process.exit(0);
60
+ if (!result) {
61
+ console.error("No review result was returned.");
62
+ process.exit(1);
63
+ }
64
+ const approved = result.decision === "approved" || result.decision === "approved_with_comments";
65
+ process.exit(approved ? 0 : 1);
57
66
  }
58
67
  async function reviewPrFlow(pr, flags) {
59
68
  const { owner, repo, number } = parsePrRef(pr);
@@ -370,7 +379,7 @@ async function setupInteractive(flags) {
370
379
  }
371
380
  async function runDemo(dev) {
372
381
  console.log("");
373
- const { demo: demo2 } = await import("./demo-WVNNI6CE.js");
382
+ const { demo: demo2 } = await import("./demo-7GBBKPPB.js");
374
383
  await demo2({ dev });
375
384
  }
376
385
  async function setupBatch(flags) {
@@ -703,9 +712,9 @@ async function server(flags) {
703
712
  await new Promise(() => {
704
713
  });
705
714
  } catch (err) {
706
- const message = err instanceof Error ? err.message : String(err);
715
+ const message2 = err instanceof Error ? err.message : String(err);
707
716
  if (!isDaemon) {
708
- console.error(`Error starting server: ${message}`);
717
+ console.error(`Error starting server: ${message2}`);
709
718
  }
710
719
  process.exit(1);
711
720
  }
@@ -764,6 +773,10 @@ async function serverStatus() {
764
773
  console.log(` PID: ${status.pid}`);
765
774
  console.log(` Sessions: ${status.sessions}`);
766
775
  console.log(` Uptime: ${Math.floor(status.uptime)}s`);
776
+ const build = getBuildInfo();
777
+ if (build.dev) {
778
+ console.log(` Build: dev \u2014 ${build.root}`);
779
+ }
767
780
  if (status.sessions > 0) {
768
781
  const sessionsResponse = await fetch(
769
782
  `http://localhost:${info.httpPort}/api/reviews`,
@@ -778,8 +791,8 @@ async function serverStatus() {
778
791
  }
779
792
  }
780
793
  } catch (err) {
781
- const message = err instanceof Error ? err.message : String(err);
782
- console.error(`Error checking server status: ${message}`);
794
+ const message2 = err instanceof Error ? err.message : String(err);
795
+ console.error(`Error checking server status: ${message2}`);
783
796
  process.exit(1);
784
797
  }
785
798
  }
@@ -813,7 +826,7 @@ async function fetchStatus(httpPort) {
813
826
  return await response.json();
814
827
  }
815
828
  function printStatus(status, httpPort) {
816
- const version = true ? "0.45.0" : "0.0.0-dev";
829
+ const version = true ? "0.48.0" : "0.0.0-dev";
817
830
  console.log(`
818
831
  DiffPrism v${version}
819
832
  `);
@@ -845,19 +858,215 @@ async function defaultAction() {
845
858
  await open(status.uiUrl);
846
859
  }
847
860
  } catch (err) {
848
- const message = err instanceof Error ? err.message : String(err);
849
- console.error(`Error fetching server status: ${message}`);
861
+ const message2 = err instanceof Error ? err.message : String(err);
862
+ console.error(`Error fetching server status: ${message2}`);
850
863
  process.exit(1);
851
864
  }
852
865
  }
853
866
 
867
+ // cli/src/commands/hook.ts
868
+ import { execFileSync } from "child_process";
869
+ import fs4 from "fs";
870
+ import path4 from "path";
871
+ var DEFAULT_MIN_LINES = 120;
872
+ var MARKER_START = "# >>> diffprism >>>";
873
+ var MARKER_END = "# <<< diffprism <<<";
874
+ var HOOK_LINE = "diffprism hook pre-commit || exit 1";
875
+ async function preCommitHook(flags = {}) {
876
+ const cwd = process.cwd();
877
+ const minLines = resolveMinLines(flags, cwd);
878
+ let changedLines;
879
+ try {
880
+ const { diffSet } = getDiff("staged", { cwd });
881
+ changedLines = diffSet.files.reduce(
882
+ (total, file) => total + file.additions + file.deletions,
883
+ 0
884
+ );
885
+ } catch (err) {
886
+ fail(`Could not read the staged diff: ${message(err)}`);
887
+ return;
888
+ }
889
+ if (changedLines === 0) {
890
+ process.exit(0);
891
+ }
892
+ if (changedLines < minLines) {
893
+ process.exit(0);
894
+ }
895
+ console.error(
896
+ `${changedLines} staged lines (gate at ${minLines}) \u2014 opening DiffPrism review...`
897
+ );
898
+ let review2 = null;
899
+ try {
900
+ const serverInfo = await ensureServer({ dev: flags.dev });
901
+ const { result } = await submitReviewToServer(serverInfo, "staged", {
902
+ cwd,
903
+ diffRef: "staged",
904
+ title: "Pre-commit review"
905
+ });
906
+ review2 = result;
907
+ } catch (err) {
908
+ fail(`DiffPrism could not run the review: ${message(err)}`);
909
+ return;
910
+ }
911
+ const decision = review2?.decision;
912
+ if (decision === "approved" || decision === "approved_with_comments") {
913
+ console.error(`Review: ${decision} \u2014 proceeding.`);
914
+ process.exit(0);
915
+ }
916
+ if (decision === "changes_requested") {
917
+ printComments(review2?.comments ?? []);
918
+ fail("Commit blocked: the review requested changes.");
919
+ return;
920
+ }
921
+ if (decision === "dismissed") {
922
+ fail("Commit blocked: the review was dismissed without a decision.");
923
+ return;
924
+ }
925
+ fail(
926
+ `Commit blocked: no review decision was returned (got ${String(decision)}).`
927
+ );
928
+ }
929
+ function printComments(comments) {
930
+ if (comments.length === 0) {
931
+ return;
932
+ }
933
+ console.error("");
934
+ for (const c of comments) {
935
+ console.error(` ${c.file}:${c.line} [${c.type}] ${c.body}`);
936
+ }
937
+ console.error("");
938
+ }
939
+ function installHook() {
940
+ const hookPath = resolveHookPath(process.cwd());
941
+ const existing = fs4.existsSync(hookPath) ? fs4.readFileSync(hookPath, "utf8") : "";
942
+ if (existing.includes(MARKER_START)) {
943
+ console.log(`Already installed in ${hookPath}`);
944
+ return;
945
+ }
946
+ const block = `${MARKER_START}
947
+ ${HOOK_LINE}
948
+ ${MARKER_END}
949
+ `;
950
+ const base = existing === "" ? "#!/bin/sh\n" : ensureTrailingNewline(existing);
951
+ const created = existing === "";
952
+ fs4.mkdirSync(path4.dirname(hookPath), { recursive: true });
953
+ fs4.writeFileSync(hookPath, `${base}
954
+ ${block}`);
955
+ fs4.chmodSync(hookPath, 493);
956
+ console.log(`${created ? "Created" : "Updated"} ${hookPath}`);
957
+ console.log(
958
+ `Staged changes of ${DEFAULT_MIN_LINES}+ lines now open a review before the commit lands.`
959
+ );
960
+ console.log("Tune with: git config diffprism.gate-lines <n>");
961
+ console.log("Remove with: diffprism hook uninstall");
962
+ }
963
+ function uninstallHook() {
964
+ const hookPath = resolveHookPath(process.cwd());
965
+ if (!fs4.existsSync(hookPath)) {
966
+ console.log("Nothing to remove \u2014 no hook file.");
967
+ return;
968
+ }
969
+ const existing = fs4.readFileSync(hookPath, "utf8");
970
+ if (!existing.includes(MARKER_START)) {
971
+ console.log(`Nothing to remove \u2014 no diffprism block in ${hookPath}`);
972
+ return;
973
+ }
974
+ const cleaned = removeMarkedBlock(existing);
975
+ if (/^\s*(#![^\n]*)?\s*$/.test(cleaned)) {
976
+ fs4.rmSync(hookPath);
977
+ console.log(`Removed ${hookPath} (it contained nothing else).`);
978
+ return;
979
+ }
980
+ fs4.writeFileSync(hookPath, cleaned);
981
+ console.log(`Removed the diffprism block from ${hookPath}`);
982
+ }
983
+ function removeMarkedBlock(contents) {
984
+ const lines = contents.split("\n");
985
+ const out = [];
986
+ let inside = false;
987
+ for (const line of lines) {
988
+ if (line.trim() === MARKER_START) {
989
+ inside = true;
990
+ continue;
991
+ }
992
+ if (line.trim() === MARKER_END) {
993
+ inside = false;
994
+ continue;
995
+ }
996
+ if (!inside) {
997
+ out.push(line);
998
+ }
999
+ }
1000
+ return ensureTrailingNewline(out.join("\n").replace(/\n{3,}/g, "\n\n"));
1001
+ }
1002
+ function resolveMinLines(flags, cwd) {
1003
+ const fromFlag = flags.minLines ? Number.parseInt(flags.minLines, 10) : NaN;
1004
+ if (Number.isFinite(fromFlag) && fromFlag > 0) {
1005
+ return fromFlag;
1006
+ }
1007
+ const configured = Number.parseInt(
1008
+ git(["config", "--get", "diffprism.gate-lines"], cwd) ?? "",
1009
+ 10
1010
+ );
1011
+ if (Number.isFinite(configured) && configured > 0) {
1012
+ return configured;
1013
+ }
1014
+ return DEFAULT_MIN_LINES;
1015
+ }
1016
+ function resolveHookPath(cwd) {
1017
+ const configured = git(["config", "--get", "core.hooksPath"], cwd);
1018
+ if (configured) {
1019
+ return path4.resolve(cwd, configured, "pre-commit");
1020
+ }
1021
+ const hooksDir = git(["rev-parse", "--git-path", "hooks"], cwd) ?? ".git/hooks";
1022
+ return path4.resolve(cwd, hooksDir, "pre-commit");
1023
+ }
1024
+ function git(args, cwd) {
1025
+ try {
1026
+ const out = execFileSync("git", args, {
1027
+ cwd,
1028
+ encoding: "utf8",
1029
+ stdio: ["ignore", "pipe", "ignore"]
1030
+ });
1031
+ const trimmed = out.trim();
1032
+ return trimmed === "" ? null : trimmed;
1033
+ } catch {
1034
+ return null;
1035
+ }
1036
+ }
1037
+ function ensureTrailingNewline(text) {
1038
+ return text.endsWith("\n") ? text : `${text}
1039
+ `;
1040
+ }
1041
+ function message(err) {
1042
+ return err instanceof Error ? err.message : String(err);
1043
+ }
1044
+ function fail(text) {
1045
+ console.error(text);
1046
+ process.exit(1);
1047
+ }
1048
+
854
1049
  // cli/src/index.ts
855
1050
  var program = new Command();
856
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.45.0" : "0.0.0-dev");
1051
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(
1052
+ describeVersion(
1053
+ true ? "0.48.0" : "0.0.0-dev"
1054
+ )
1055
+ );
857
1056
  program.action(defaultAction);
858
1057
  program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
859
1058
  program.command("review [ref]").description("Open a browser-based diff review (local git ref or GitHub PR ref like owner/repo#123)").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--reasoning <text>", "Agent reasoning about the changes").option("--dev", "Use Vite dev server with HMR instead of static files").option("--post-to-github", "Automatically post review back to GitHub without prompting").action(review);
860
1059
  program.command("review-pr <pr>", { hidden: true }).action((pr, flags) => review(pr, flags));
1060
+ var hookCmd = program.command("hook").description("Git hook integration \u2014 gate commits on a human review");
1061
+ hookCmd.command("pre-commit").description("Run the review gate for the staged changes (called from a git hook)").option("--min-lines <n>", "Staged lines required to trigger a review (default: 120)").option("--dev", "Use Vite dev server").action((flags) => {
1062
+ void preCommitHook(flags);
1063
+ });
1064
+ hookCmd.command("install").description("Add the diffprism gate to this repo's pre-commit hook").action(() => {
1065
+ installHook();
1066
+ });
1067
+ hookCmd.command("uninstall").description("Remove the diffprism gate from this repo's pre-commit hook").action(() => {
1068
+ uninstallHook();
1069
+ });
861
1070
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
862
1071
  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) => {
863
1072
  setup(flags);
@@ -122,30 +122,45 @@ function buildDefaultSpawnCommand(options) {
122
122
  const thisDir = path2.dirname(thisFile);
123
123
  const workspaceRoot = path2.resolve(thisDir, "..", "..", "..");
124
124
  const devBin = path2.join(workspaceRoot, "cli", "bin", "diffprism.mjs");
125
- let binPath = "diffprism";
126
125
  if (fs2.existsSync(devBin)) {
127
- binPath = devBin;
128
- } else {
129
- let searchDir = thisDir;
130
- while (searchDir !== path2.dirname(searchDir)) {
131
- const candidate = path2.join(
132
- searchDir,
133
- "node_modules",
134
- ".bin",
135
- "diffprism"
126
+ return withDevFlag([process.execPath, devBin, "server", "--_daemon"], options);
127
+ }
128
+ let searchDir = thisDir;
129
+ while (searchDir !== path2.dirname(searchDir)) {
130
+ const ownBin = readOwnBinPath(searchDir);
131
+ if (ownBin) {
132
+ return withDevFlag(
133
+ [process.execPath, ownBin, "server", "--_daemon"],
134
+ options
136
135
  );
137
- if (fs2.existsSync(candidate)) {
138
- binPath = candidate;
139
- break;
140
- }
141
- searchDir = path2.dirname(searchDir);
142
136
  }
137
+ const shim = path2.join(searchDir, "node_modules", ".bin", "diffprism");
138
+ if (fs2.existsSync(shim)) {
139
+ return withDevFlag([shim, "server", "--_daemon"], options);
140
+ }
141
+ searchDir = path2.dirname(searchDir);
143
142
  }
144
- const args = [process.execPath, binPath, "server", "--_daemon"];
145
- if (options.dev) {
146
- args.push("--dev");
143
+ return withDevFlag(["diffprism", "server", "--_daemon"], options);
144
+ }
145
+ function withDevFlag(args, options) {
146
+ return options.dev ? [...args, "--dev"] : args;
147
+ }
148
+ function readOwnBinPath(dir) {
149
+ const manifestPath = path2.join(dir, "package.json");
150
+ if (!fs2.existsSync(manifestPath)) {
151
+ return null;
152
+ }
153
+ try {
154
+ const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
155
+ const entry = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.diffprism;
156
+ if (!entry) {
157
+ return null;
158
+ }
159
+ const resolved = path2.resolve(dir, entry);
160
+ return fs2.existsSync(resolved) ? resolved : null;
161
+ } catch {
162
+ return null;
147
163
  }
148
- return args;
149
164
  }
150
165
  async function submitReviewToServer(serverInfo, diffRef, options = {}) {
151
166
  const cwd = options.cwd ?? process.cwd();
@@ -251,6 +266,27 @@ async function submitReviewToServer(serverInfo, diffRef, options = {}) {
251
266
  throw new Error("Review timed out waiting for submission.");
252
267
  }
253
268
 
269
+ // packages/core/src/build-info.ts
270
+ import fs3 from "fs";
271
+ import path3 from "path";
272
+ import { fileURLToPath as fileURLToPath2 } from "url";
273
+ function getBuildInfo() {
274
+ let dir = path3.dirname(fileURLToPath2(import.meta.url));
275
+ while (dir !== path3.dirname(dir)) {
276
+ if (fs3.existsSync(path3.join(dir, "package.json"))) {
277
+ if (fs3.existsSync(path3.join(dir, ".git"))) {
278
+ return { dev: true, root: dir };
279
+ }
280
+ }
281
+ dir = path3.dirname(dir);
282
+ }
283
+ return { dev: false, root: null };
284
+ }
285
+ function describeVersion(version) {
286
+ const info = getBuildInfo();
287
+ return info.dev ? `${version} (dev build \u2014 ${info.root})` : version;
288
+ }
289
+
254
290
  // packages/core/src/diff-utils.ts
255
291
  import { createHash } from "crypto";
256
292
  function hashDiff(rawDiff) {
@@ -355,13 +391,13 @@ import { randomUUID as randomUUID2 } from "crypto";
355
391
  import getPort from "get-port";
356
392
  import open from "open";
357
393
  import { WebSocketServer, WebSocket } from "ws";
358
- import fs5 from "fs";
394
+ import fs6 from "fs";
359
395
 
360
396
  // packages/core/src/ui-server.ts
361
397
  import http from "http";
362
- import fs3 from "fs";
363
- import path3 from "path";
364
- import { fileURLToPath as fileURLToPath2 } from "url";
398
+ import fs4 from "fs";
399
+ import path4 from "path";
400
+ import { fileURLToPath as fileURLToPath3 } from "url";
365
401
  var MIME_TYPES = {
366
402
  ".html": "text/html",
367
403
  ".js": "application/javascript",
@@ -374,15 +410,15 @@ var MIME_TYPES = {
374
410
  ".woff2": "font/woff2"
375
411
  };
376
412
  function resolveUiDist() {
377
- const thisFile = fileURLToPath2(import.meta.url);
378
- const thisDir = path3.dirname(thisFile);
379
- const publishedUiDist = path3.resolve(thisDir, "..", "ui-dist");
380
- if (fs3.existsSync(path3.join(publishedUiDist, "index.html"))) {
413
+ const thisFile = fileURLToPath3(import.meta.url);
414
+ const thisDir = path4.dirname(thisFile);
415
+ const publishedUiDist = path4.resolve(thisDir, "..", "ui-dist");
416
+ if (fs4.existsSync(path4.join(publishedUiDist, "index.html"))) {
381
417
  return publishedUiDist;
382
418
  }
383
- const workspaceRoot = path3.resolve(thisDir, "..", "..", "..");
384
- const devUiDist = path3.join(workspaceRoot, "packages", "ui", "dist");
385
- if (fs3.existsSync(path3.join(devUiDist, "index.html"))) {
419
+ const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
420
+ const devUiDist = path4.join(workspaceRoot, "packages", "ui", "dist");
421
+ if (fs4.existsSync(path4.join(devUiDist, "index.html"))) {
386
422
  return devUiDist;
387
423
  }
388
424
  throw new Error(
@@ -390,11 +426,11 @@ function resolveUiDist() {
390
426
  );
391
427
  }
392
428
  function resolveUiRoot() {
393
- const thisFile = fileURLToPath2(import.meta.url);
394
- const thisDir = path3.dirname(thisFile);
395
- const workspaceRoot = path3.resolve(thisDir, "..", "..", "..");
396
- const uiRoot = path3.join(workspaceRoot, "packages", "ui");
397
- if (fs3.existsSync(path3.join(uiRoot, "index.html"))) {
429
+ const thisFile = fileURLToPath3(import.meta.url);
430
+ const thisDir = path4.dirname(thisFile);
431
+ const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
432
+ const uiRoot = path4.join(workspaceRoot, "packages", "ui");
433
+ if (fs4.existsSync(path4.join(uiRoot, "index.html"))) {
398
434
  return uiRoot;
399
435
  }
400
436
  throw new Error(
@@ -414,14 +450,14 @@ async function startViteDevServer(uiRoot, port, silent) {
414
450
  function createStaticServer(distPath, port) {
415
451
  const server = http.createServer((req, res) => {
416
452
  const urlPath = req.url?.split("?")[0] ?? "/";
417
- let filePath = path3.join(distPath, urlPath === "/" ? "index.html" : urlPath);
418
- if (!fs3.existsSync(filePath)) {
419
- filePath = path3.join(distPath, "index.html");
453
+ let filePath = path4.join(distPath, urlPath === "/" ? "index.html" : urlPath);
454
+ if (!fs4.existsSync(filePath)) {
455
+ filePath = path4.join(distPath, "index.html");
420
456
  }
421
- const ext = path3.extname(filePath);
457
+ const ext = path4.extname(filePath);
422
458
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
423
459
  try {
424
- const content = fs3.readFileSync(filePath);
460
+ const content = fs4.readFileSync(filePath);
425
461
  res.writeHead(200, { "Content-Type": contentType });
426
462
  res.end(content);
427
463
  } catch {
@@ -436,22 +472,22 @@ function createStaticServer(distPath, port) {
436
472
  }
437
473
 
438
474
  // packages/core/src/review-history.ts
439
- import fs4 from "fs";
440
- import path4 from "path";
475
+ import fs5 from "fs";
476
+ import path5 from "path";
441
477
  import { randomUUID } from "crypto";
442
478
  function generateEntryId() {
443
479
  return randomUUID();
444
480
  }
445
481
  function getHistoryPath(projectDir) {
446
- return path4.join(projectDir, ".diffprism", "history", "reviews.json");
482
+ return path5.join(projectDir, ".diffprism", "history", "reviews.json");
447
483
  }
448
484
  function readHistory(projectDir) {
449
485
  const filePath = getHistoryPath(projectDir);
450
- if (!fs4.existsSync(filePath)) {
486
+ if (!fs5.existsSync(filePath)) {
451
487
  return { version: 1, entries: [] };
452
488
  }
453
489
  try {
454
- const raw = fs4.readFileSync(filePath, "utf-8");
490
+ const raw = fs5.readFileSync(filePath, "utf-8");
455
491
  const parsed = JSON.parse(raw);
456
492
  return parsed;
457
493
  } catch {
@@ -460,14 +496,14 @@ function readHistory(projectDir) {
460
496
  }
461
497
  function appendHistory(projectDir, entry) {
462
498
  const filePath = getHistoryPath(projectDir);
463
- const dir = path4.dirname(filePath);
464
- if (!fs4.existsSync(dir)) {
465
- fs4.mkdirSync(dir, { recursive: true });
499
+ const dir = path5.dirname(filePath);
500
+ if (!fs5.existsSync(dir)) {
501
+ fs5.mkdirSync(dir, { recursive: true });
466
502
  }
467
503
  const history = readHistory(projectDir);
468
504
  history.entries.push(entry);
469
505
  history.entries.sort((a, b) => a.timestamp - b.timestamp);
470
- fs4.writeFileSync(filePath, JSON.stringify(history, null, 2) + "\n");
506
+ fs5.writeFileSync(filePath, JSON.stringify(history, null, 2) + "\n");
471
507
  }
472
508
  function getRecentHistory(projectDir, limit = 50) {
473
509
  const history = readHistory(projectDir);
@@ -671,12 +707,12 @@ function recordReviewHistory(session, result) {
671
707
  function isInsideGitRepo(dirPath) {
672
708
  let current = dirPath;
673
709
  while (current !== "/") {
674
- if (fs5.existsSync(`${current}/.git`)) return true;
710
+ if (fs6.existsSync(`${current}/.git`)) return true;
675
711
  const parent = current.replace(/\/[^/]+$/, "") || "/";
676
712
  if (parent === current) break;
677
713
  current = parent;
678
714
  }
679
- return fs5.existsSync(`${current}/.git`);
715
+ return fs6.existsSync(`${current}/.git`);
680
716
  }
681
717
  async function handleApiRequest(req, res) {
682
718
  const method = req.method ?? "GET";
@@ -787,7 +823,7 @@ async function handleApiRequest(req, res) {
787
823
  return true;
788
824
  }
789
825
  try {
790
- const stat = fs5.statSync(projectPath);
826
+ const stat = fs6.statSync(projectPath);
791
827
  if (!stat.isDirectory()) {
792
828
  jsonResponse(res, 400, { error: "Path is not a directory" });
793
829
  return true;
@@ -946,7 +982,7 @@ async function handleApiRequest(req, res) {
946
982
  if (parsedUrl.pathname === "/api/fs/list") {
947
983
  const dirPath = parsedUrl.searchParams.get("path") || process.cwd();
948
984
  try {
949
- const stat = fs5.statSync(dirPath);
985
+ const stat = fs6.statSync(dirPath);
950
986
  if (!stat.isDirectory()) {
951
987
  jsonResponse(res, 400, { error: "Not a directory" });
952
988
  return true;
@@ -956,13 +992,13 @@ async function handleApiRequest(req, res) {
956
992
  return true;
957
993
  }
958
994
  try {
959
- const entries = fs5.readdirSync(dirPath, { withFileTypes: true });
995
+ const entries = fs6.readdirSync(dirPath, { withFileTypes: true });
960
996
  const dirs = [];
961
997
  for (const entry of entries) {
962
998
  if (!entry.isDirectory()) continue;
963
999
  if (entry.name.startsWith(".")) continue;
964
1000
  const fullPath = `${dirPath}/${entry.name}`;
965
- const isGitRepo2 = fs5.existsSync(`${fullPath}/.git`);
1001
+ const isGitRepo2 = fs6.existsSync(`${fullPath}/.git`);
966
1002
  dirs.push({ name: entry.name, path: fullPath, isGitRepo: isGitRepo2 });
967
1003
  }
968
1004
  dirs.sort((a, b) => {
@@ -1550,5 +1586,7 @@ export {
1550
1586
  isServerAlive,
1551
1587
  startGlobalServer,
1552
1588
  ensureServer,
1553
- submitReviewToServer
1589
+ submitReviewToServer,
1590
+ getBuildInfo,
1591
+ describeVersion
1554
1592
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ensureServer,
3
3
  submitReviewToServer
4
- } from "./chunk-RICVPPTE.js";
4
+ } from "./chunk-AJGESVAM.js";
5
5
  import {
6
6
  parseDiff
7
7
  } from "./chunk-QGWYCEJN.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  demo
3
- } from "./chunk-RVI74JF5.js";
4
- import "./chunk-RICVPPTE.js";
3
+ } from "./chunk-SWSRPUXO.js";
4
+ import "./chunk-AJGESVAM.js";
5
5
  import "./chunk-QGWYCEJN.js";
6
6
  import "./chunk-DHCVZGHE.js";
7
7
  import "./chunk-JSBRDJBE.js";
@@ -6,7 +6,7 @@ import {
6
6
  ensureServer,
7
7
  isServerAlive,
8
8
  submitReviewToServer
9
- } from "./chunk-RICVPPTE.js";
9
+ } from "./chunk-AJGESVAM.js";
10
10
  import {
11
11
  getDiff
12
12
  } from "./chunk-QGWYCEJN.js";
@@ -144,7 +144,7 @@ async function handlePrReview(pr, options) {
144
144
  async function startMcpServer() {
145
145
  const server = new McpServer({
146
146
  name: "diffprism",
147
- version: true ? "0.45.0" : "0.0.0-dev"
147
+ version: true ? "0.48.0" : "0.0.0-dev"
148
148
  });
149
149
  server.tool(
150
150
  "open_review",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.45.0",
3
+ "version": "0.48.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {