diffprism 0.19.1 → 0.20.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/dist/bin.js +214 -8
- package/dist/{chunk-4VXA6GCO.js → chunk-L2D5SDUV.js} +130 -13
- package/dist/mcp-server.js +3 -3
- package/package.json +1 -1
- package/ui-dist/assets/{index-BfIxxwO-.js → index-C_k3uWX-.js} +69 -64
- package/ui-dist/assets/index-CkUXOfXP.css +1 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-BcuyMNeU.css +0 -1
package/dist/bin.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
startGlobalServer,
|
|
7
7
|
startReview,
|
|
8
8
|
startWatch
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-L2D5SDUV.js";
|
|
10
10
|
|
|
11
11
|
// cli/src/index.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -144,6 +144,12 @@ You can also check for feedback without blocking by calling \`get_review_result\
|
|
|
144
144
|
`;
|
|
145
145
|
|
|
146
146
|
// cli/src/commands/setup.ts
|
|
147
|
+
var GITIGNORE_ENTRIES = [
|
|
148
|
+
".diffprism",
|
|
149
|
+
".mcp.json",
|
|
150
|
+
".claude/settings.json",
|
|
151
|
+
".claude/skills/review/"
|
|
152
|
+
];
|
|
147
153
|
function findGitRoot(from) {
|
|
148
154
|
let dir = path.resolve(from);
|
|
149
155
|
while (true) {
|
|
@@ -306,27 +312,28 @@ async function promptUser(question) {
|
|
|
306
312
|
}
|
|
307
313
|
async function setupGitignore(gitRoot) {
|
|
308
314
|
const filePath = path.join(gitRoot, ".gitignore");
|
|
309
|
-
const entry = ".diffprism";
|
|
310
315
|
if (fs.existsSync(filePath)) {
|
|
311
316
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
312
317
|
const lines = content.split("\n").map((l) => l.trim());
|
|
313
|
-
|
|
318
|
+
const missing = GITIGNORE_ENTRIES.filter((e) => !lines.includes(e));
|
|
319
|
+
if (missing.length === 0) {
|
|
314
320
|
return { action: "skipped", filePath };
|
|
315
321
|
}
|
|
316
|
-
const
|
|
322
|
+
const suffix = missing.map((e) => e + "\n").join("");
|
|
323
|
+
const newContent = content.endsWith("\n") ? content + suffix : content + "\n" + suffix;
|
|
317
324
|
fs.writeFileSync(filePath, newContent);
|
|
318
325
|
return { action: "updated", filePath };
|
|
319
326
|
}
|
|
320
327
|
const confirmed = await promptUser(
|
|
321
|
-
"No .gitignore found. Create one with
|
|
328
|
+
"No .gitignore found. Create one with DiffPrism entries? (Y/n) "
|
|
322
329
|
);
|
|
323
330
|
if (!confirmed) {
|
|
324
331
|
console.log(
|
|
325
|
-
" Warning:
|
|
332
|
+
" Warning: DiffPrism files will appear in git status and may be accidentally committed."
|
|
326
333
|
);
|
|
327
334
|
return { action: "skipped", filePath };
|
|
328
335
|
}
|
|
329
|
-
fs.writeFileSync(filePath,
|
|
336
|
+
fs.writeFileSync(filePath, GITIGNORE_ENTRIES.map((e) => e + "\n").join(""));
|
|
330
337
|
return { action: "created", filePath };
|
|
331
338
|
}
|
|
332
339
|
async function setup(flags) {
|
|
@@ -427,6 +434,202 @@ function isGlobalSetupDone() {
|
|
|
427
434
|
return toolNames.every((t) => allow.includes(t));
|
|
428
435
|
}
|
|
429
436
|
|
|
437
|
+
// cli/src/commands/teardown.ts
|
|
438
|
+
import fs2 from "fs";
|
|
439
|
+
import path2 from "path";
|
|
440
|
+
import os2 from "os";
|
|
441
|
+
function teardownMcpJson(gitRoot) {
|
|
442
|
+
const filePath = path2.join(gitRoot, ".mcp.json");
|
|
443
|
+
if (!fs2.existsSync(filePath)) {
|
|
444
|
+
return { action: "skipped", filePath };
|
|
445
|
+
}
|
|
446
|
+
const existing = readJsonFile(filePath);
|
|
447
|
+
const servers = existing.mcpServers ?? {};
|
|
448
|
+
if (!servers.diffprism) {
|
|
449
|
+
return { action: "skipped", filePath };
|
|
450
|
+
}
|
|
451
|
+
delete servers.diffprism;
|
|
452
|
+
if (Object.keys(servers).length === 0) {
|
|
453
|
+
const { mcpServers: _, ...rest } = existing;
|
|
454
|
+
if (Object.keys(rest).length === 0) {
|
|
455
|
+
fs2.unlinkSync(filePath);
|
|
456
|
+
} else {
|
|
457
|
+
writeJsonFile(filePath, rest);
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
writeJsonFile(filePath, { ...existing, mcpServers: servers });
|
|
461
|
+
}
|
|
462
|
+
return { action: "removed", filePath };
|
|
463
|
+
}
|
|
464
|
+
function teardownClaudePermissions(baseDir) {
|
|
465
|
+
const filePath = path2.join(baseDir, ".claude", "settings.json");
|
|
466
|
+
if (!fs2.existsSync(filePath)) {
|
|
467
|
+
return { action: "skipped", filePath };
|
|
468
|
+
}
|
|
469
|
+
const existing = readJsonFile(filePath);
|
|
470
|
+
const permissions = existing.permissions ?? {};
|
|
471
|
+
const allow = permissions.allow ?? [];
|
|
472
|
+
const toolNames = [
|
|
473
|
+
"mcp__diffprism__open_review",
|
|
474
|
+
"mcp__diffprism__update_review_context",
|
|
475
|
+
"mcp__diffprism__get_review_result"
|
|
476
|
+
];
|
|
477
|
+
const filtered = allow.filter((t) => !toolNames.includes(t));
|
|
478
|
+
if (filtered.length === allow.length) {
|
|
479
|
+
return { action: "skipped", filePath };
|
|
480
|
+
}
|
|
481
|
+
if (filtered.length > 0) {
|
|
482
|
+
permissions.allow = filtered;
|
|
483
|
+
} else {
|
|
484
|
+
delete permissions.allow;
|
|
485
|
+
}
|
|
486
|
+
if (Object.keys(permissions).length === 0) {
|
|
487
|
+
delete existing.permissions;
|
|
488
|
+
} else {
|
|
489
|
+
existing.permissions = permissions;
|
|
490
|
+
}
|
|
491
|
+
writeJsonFile(filePath, existing);
|
|
492
|
+
return { action: "removed", filePath };
|
|
493
|
+
}
|
|
494
|
+
function teardownHooks(gitRoot) {
|
|
495
|
+
const filePath = path2.join(gitRoot, ".claude", "settings.json");
|
|
496
|
+
const result = cleanDiffprismHooks(gitRoot);
|
|
497
|
+
if (result.removed > 0) {
|
|
498
|
+
const existing = readJsonFile(filePath);
|
|
499
|
+
const hooks = existing.hooks;
|
|
500
|
+
if (hooks && Object.keys(hooks).length === 0) {
|
|
501
|
+
delete existing.hooks;
|
|
502
|
+
writeJsonFile(filePath, existing);
|
|
503
|
+
}
|
|
504
|
+
return { action: "removed", filePath: filePath + " (hooks)" };
|
|
505
|
+
}
|
|
506
|
+
return { action: "skipped", filePath: filePath + " (hooks)" };
|
|
507
|
+
}
|
|
508
|
+
function cleanupSettingsFile(baseDir) {
|
|
509
|
+
const filePath = path2.join(baseDir, ".claude", "settings.json");
|
|
510
|
+
if (!fs2.existsSync(filePath)) return;
|
|
511
|
+
const existing = readJsonFile(filePath);
|
|
512
|
+
if (Object.keys(existing).length === 0) {
|
|
513
|
+
fs2.unlinkSync(filePath);
|
|
514
|
+
tryRmdir(path2.join(baseDir, ".claude"));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function tryRmdir(dirPath) {
|
|
518
|
+
try {
|
|
519
|
+
const entries = fs2.readdirSync(dirPath);
|
|
520
|
+
if (entries.length === 0) {
|
|
521
|
+
fs2.rmdirSync(dirPath);
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function teardownSkill(baseDir, global) {
|
|
527
|
+
const skillDir = global ? path2.join(os2.homedir(), ".claude", "skills", "review") : path2.join(baseDir, ".claude", "skills", "review");
|
|
528
|
+
const filePath = path2.join(skillDir, "SKILL.md");
|
|
529
|
+
if (!fs2.existsSync(filePath)) {
|
|
530
|
+
return { action: "skipped", filePath };
|
|
531
|
+
}
|
|
532
|
+
fs2.unlinkSync(filePath);
|
|
533
|
+
tryRmdir(skillDir);
|
|
534
|
+
tryRmdir(path2.dirname(skillDir));
|
|
535
|
+
return { action: "removed", filePath };
|
|
536
|
+
}
|
|
537
|
+
function teardownGitignore(gitRoot) {
|
|
538
|
+
const filePath = path2.join(gitRoot, ".gitignore");
|
|
539
|
+
if (!fs2.existsSync(filePath)) {
|
|
540
|
+
return { action: "skipped", filePath };
|
|
541
|
+
}
|
|
542
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
543
|
+
const lines = content.split("\n");
|
|
544
|
+
const entrySet = new Set(GITIGNORE_ENTRIES);
|
|
545
|
+
const filtered = lines.filter((l) => !entrySet.has(l.trim()));
|
|
546
|
+
if (filtered.length === lines.length) {
|
|
547
|
+
return { action: "skipped", filePath };
|
|
548
|
+
}
|
|
549
|
+
const newContent = filtered.join("\n");
|
|
550
|
+
if (newContent.trim() === "") {
|
|
551
|
+
fs2.unlinkSync(filePath);
|
|
552
|
+
} else {
|
|
553
|
+
fs2.writeFileSync(filePath, newContent);
|
|
554
|
+
}
|
|
555
|
+
return { action: "removed", filePath };
|
|
556
|
+
}
|
|
557
|
+
function teardownDiffprismDir(gitRoot) {
|
|
558
|
+
const dirPath = path2.join(gitRoot, ".diffprism");
|
|
559
|
+
if (!fs2.existsSync(dirPath)) {
|
|
560
|
+
return { action: "skipped", filePath: dirPath };
|
|
561
|
+
}
|
|
562
|
+
fs2.rmSync(dirPath, { recursive: true });
|
|
563
|
+
return { action: "removed", filePath: dirPath };
|
|
564
|
+
}
|
|
565
|
+
async function teardown(flags) {
|
|
566
|
+
const global = flags.global ?? false;
|
|
567
|
+
const quiet = flags.quiet ?? false;
|
|
568
|
+
const home = os2.homedir();
|
|
569
|
+
const result = { removed: [], skipped: [] };
|
|
570
|
+
if (global) {
|
|
571
|
+
if (!quiet) {
|
|
572
|
+
console.log("Tearing down DiffPrism global configuration...\n");
|
|
573
|
+
}
|
|
574
|
+
const skill2 = teardownSkill("", true);
|
|
575
|
+
result[skill2.action].push(skill2.filePath);
|
|
576
|
+
const perms2 = teardownClaudePermissions(home);
|
|
577
|
+
result[perms2.action].push(perms2.filePath);
|
|
578
|
+
cleanupSettingsFile(home);
|
|
579
|
+
if (!quiet) {
|
|
580
|
+
printTeardownSummary(result, home);
|
|
581
|
+
console.log("\n\u2713 DiffPrism global configuration removed.");
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
const gitRoot = findGitRoot(process.cwd());
|
|
586
|
+
if (!gitRoot) {
|
|
587
|
+
console.error(
|
|
588
|
+
"Error: Not in a git repository. Run this command from inside a git project."
|
|
589
|
+
);
|
|
590
|
+
console.error(
|
|
591
|
+
"Tip: Use `diffprism teardown --global` to remove global DiffPrism configuration."
|
|
592
|
+
);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
return { removed: [], skipped: [] };
|
|
595
|
+
}
|
|
596
|
+
if (!quiet) {
|
|
597
|
+
console.log("Tearing down DiffPrism configuration...\n");
|
|
598
|
+
}
|
|
599
|
+
const mcp = teardownMcpJson(gitRoot);
|
|
600
|
+
result[mcp.action].push(mcp.filePath);
|
|
601
|
+
const perms = teardownClaudePermissions(gitRoot);
|
|
602
|
+
result[perms.action].push(perms.filePath);
|
|
603
|
+
const hooks = teardownHooks(gitRoot);
|
|
604
|
+
result[hooks.action].push(hooks.filePath);
|
|
605
|
+
cleanupSettingsFile(gitRoot);
|
|
606
|
+
const skill = teardownSkill(gitRoot, false);
|
|
607
|
+
result[skill.action].push(skill.filePath);
|
|
608
|
+
const gitignore = teardownGitignore(gitRoot);
|
|
609
|
+
result[gitignore.action].push(gitignore.filePath);
|
|
610
|
+
const diffprismDir = teardownDiffprismDir(gitRoot);
|
|
611
|
+
result[diffprismDir.action].push(diffprismDir.filePath);
|
|
612
|
+
if (!quiet) {
|
|
613
|
+
printTeardownSummary(result, gitRoot);
|
|
614
|
+
console.log("\n\u2713 DiffPrism configuration removed.");
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
function printTeardownSummary(result, baseDir) {
|
|
619
|
+
if (result.removed.length > 0) {
|
|
620
|
+
console.log("Removed:");
|
|
621
|
+
for (const f of result.removed) {
|
|
622
|
+
console.log(` - ${path2.relative(baseDir, f) || f}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (result.skipped.length > 0) {
|
|
626
|
+
console.log("Skipped (not found):");
|
|
627
|
+
for (const f of result.skipped) {
|
|
628
|
+
console.log(` . ${path2.relative(baseDir, f) || f}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
430
633
|
// cli/src/commands/start.ts
|
|
431
634
|
async function start(ref, flags) {
|
|
432
635
|
const outcome = await setup({
|
|
@@ -629,7 +832,7 @@ async function serverStop() {
|
|
|
629
832
|
|
|
630
833
|
// cli/src/index.ts
|
|
631
834
|
var program = new Command();
|
|
632
|
-
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.
|
|
835
|
+
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.20.1" : "0.0.0-dev");
|
|
633
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);
|
|
634
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);
|
|
635
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);
|
|
@@ -638,6 +841,9 @@ program.command("serve").description("Start the MCP server for Claude Code integ
|
|
|
638
841
|
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) => {
|
|
639
842
|
setup(flags);
|
|
640
843
|
});
|
|
844
|
+
program.command("teardown").description("Remove DiffPrism configuration from the current project").option("--global", "Remove global configuration (skill + permissions at ~/.claude/)").option("-q, --quiet", "Suppress output").action((flags) => {
|
|
845
|
+
teardown(flags);
|
|
846
|
+
});
|
|
641
847
|
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);
|
|
642
848
|
serverCmd.command("status").description("Check if the global server is running and list active sessions").action(serverStatus);
|
|
643
849
|
serverCmd.command("stop").description("Stop the running global server").action(serverStop);
|
|
@@ -1132,7 +1132,6 @@ async function isServerAlive() {
|
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
1134
|
// packages/core/src/watch.ts
|
|
1135
|
-
import { createHash } from "crypto";
|
|
1136
1135
|
import getPort2 from "get-port";
|
|
1137
1136
|
import open2 from "open";
|
|
1138
1137
|
|
|
@@ -1265,7 +1264,8 @@ function createWatchBridge(port, callbacks) {
|
|
|
1265
1264
|
});
|
|
1266
1265
|
}
|
|
1267
1266
|
|
|
1268
|
-
// packages/core/src/
|
|
1267
|
+
// packages/core/src/diff-utils.ts
|
|
1268
|
+
import { createHash } from "crypto";
|
|
1269
1269
|
function hashDiff(rawDiff) {
|
|
1270
1270
|
return createHash("sha256").update(rawDiff).digest("hex");
|
|
1271
1271
|
}
|
|
@@ -1296,6 +1296,8 @@ function detectChangedFiles(oldDiffSet, newDiffSet) {
|
|
|
1296
1296
|
}
|
|
1297
1297
|
return changed;
|
|
1298
1298
|
}
|
|
1299
|
+
|
|
1300
|
+
// packages/core/src/watch.ts
|
|
1299
1301
|
async function startWatch(options) {
|
|
1300
1302
|
const {
|
|
1301
1303
|
diffRef,
|
|
@@ -1447,6 +1449,8 @@ import open3 from "open";
|
|
|
1447
1449
|
import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
|
|
1448
1450
|
var sessions2 = /* @__PURE__ */ new Map();
|
|
1449
1451
|
var clientSessions = /* @__PURE__ */ new Map();
|
|
1452
|
+
var sessionWatchers = /* @__PURE__ */ new Map();
|
|
1453
|
+
var serverPollInterval = 2e3;
|
|
1450
1454
|
var reopenBrowserIfNeeded = null;
|
|
1451
1455
|
function toSummary(session) {
|
|
1452
1456
|
const { payload } = session;
|
|
@@ -1466,7 +1470,8 @@ function toSummary(session) {
|
|
|
1466
1470
|
additions,
|
|
1467
1471
|
deletions,
|
|
1468
1472
|
status: session.status,
|
|
1469
|
-
createdAt: session.createdAt
|
|
1473
|
+
createdAt: session.createdAt,
|
|
1474
|
+
hasNewChanges: session.hasNewChanges
|
|
1470
1475
|
};
|
|
1471
1476
|
}
|
|
1472
1477
|
function readBody(req) {
|
|
@@ -1517,6 +1522,93 @@ function sendToSessionClients(sessionId, msg) {
|
|
|
1517
1522
|
}
|
|
1518
1523
|
}
|
|
1519
1524
|
}
|
|
1525
|
+
function hasViewersForSession(sessionId) {
|
|
1526
|
+
for (const [client, sid] of clientSessions.entries()) {
|
|
1527
|
+
if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
|
|
1528
|
+
return true;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
function startSessionWatcher(sessionId) {
|
|
1534
|
+
if (sessionWatchers.has(sessionId)) return;
|
|
1535
|
+
const session = sessions2.get(sessionId);
|
|
1536
|
+
if (!session?.diffRef) return;
|
|
1537
|
+
const interval = setInterval(() => {
|
|
1538
|
+
const s = sessions2.get(sessionId);
|
|
1539
|
+
if (!s?.diffRef) {
|
|
1540
|
+
stopSessionWatcher(sessionId);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(s.diffRef, {
|
|
1545
|
+
cwd: s.projectPath
|
|
1546
|
+
});
|
|
1547
|
+
const newHash = hashDiff(newRawDiff);
|
|
1548
|
+
if (newHash !== s.lastDiffHash) {
|
|
1549
|
+
const newBriefing = analyze(newDiffSet);
|
|
1550
|
+
const changedFiles = detectChangedFiles(s.lastDiffSet ?? null, newDiffSet);
|
|
1551
|
+
s.payload = {
|
|
1552
|
+
...s.payload,
|
|
1553
|
+
diffSet: newDiffSet,
|
|
1554
|
+
rawDiff: newRawDiff,
|
|
1555
|
+
briefing: newBriefing
|
|
1556
|
+
};
|
|
1557
|
+
s.lastDiffHash = newHash;
|
|
1558
|
+
s.lastDiffSet = newDiffSet;
|
|
1559
|
+
if (hasViewersForSession(sessionId)) {
|
|
1560
|
+
sendToSessionClients(sessionId, {
|
|
1561
|
+
type: "diff:update",
|
|
1562
|
+
payload: {
|
|
1563
|
+
diffSet: newDiffSet,
|
|
1564
|
+
rawDiff: newRawDiff,
|
|
1565
|
+
briefing: newBriefing,
|
|
1566
|
+
changedFiles,
|
|
1567
|
+
timestamp: Date.now()
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
s.hasNewChanges = false;
|
|
1571
|
+
} else {
|
|
1572
|
+
s.hasNewChanges = true;
|
|
1573
|
+
broadcastSessionList();
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
}, serverPollInterval);
|
|
1579
|
+
sessionWatchers.set(sessionId, interval);
|
|
1580
|
+
}
|
|
1581
|
+
function stopSessionWatcher(sessionId) {
|
|
1582
|
+
const interval = sessionWatchers.get(sessionId);
|
|
1583
|
+
if (interval) {
|
|
1584
|
+
clearInterval(interval);
|
|
1585
|
+
sessionWatchers.delete(sessionId);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function startAllWatchers() {
|
|
1589
|
+
for (const [id, session] of sessions2.entries()) {
|
|
1590
|
+
if (session.diffRef && !sessionWatchers.has(id)) {
|
|
1591
|
+
startSessionWatcher(id);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function stopAllWatchers() {
|
|
1596
|
+
for (const [id, interval] of sessionWatchers.entries()) {
|
|
1597
|
+
clearInterval(interval);
|
|
1598
|
+
}
|
|
1599
|
+
sessionWatchers.clear();
|
|
1600
|
+
}
|
|
1601
|
+
function hasConnectedClients() {
|
|
1602
|
+
if (!wss) return false;
|
|
1603
|
+
for (const client of wss.clients) {
|
|
1604
|
+
if (client.readyState === WebSocket3.OPEN) return true;
|
|
1605
|
+
}
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
function broadcastSessionList() {
|
|
1609
|
+
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
1610
|
+
broadcastToAll({ type: "session:list", payload: summaries });
|
|
1611
|
+
}
|
|
1520
1612
|
async function handleApiRequest(req, res) {
|
|
1521
1613
|
const method = req.method ?? "GET";
|
|
1522
1614
|
const url = (req.url ?? "/").split("?")[0];
|
|
@@ -1543,18 +1635,28 @@ async function handleApiRequest(req, res) {
|
|
|
1543
1635
|
if (method === "POST" && url === "/api/reviews") {
|
|
1544
1636
|
try {
|
|
1545
1637
|
const body = await readBody(req);
|
|
1546
|
-
const { payload, projectPath } = JSON.parse(body);
|
|
1638
|
+
const { payload, projectPath, diffRef } = JSON.parse(body);
|
|
1547
1639
|
const sessionId = `session-${randomUUID().slice(0, 8)}`;
|
|
1548
1640
|
payload.reviewId = sessionId;
|
|
1641
|
+
if (diffRef) {
|
|
1642
|
+
payload.watchMode = true;
|
|
1643
|
+
}
|
|
1549
1644
|
const session = {
|
|
1550
1645
|
id: sessionId,
|
|
1551
1646
|
payload,
|
|
1552
1647
|
projectPath,
|
|
1553
1648
|
status: "pending",
|
|
1554
1649
|
createdAt: Date.now(),
|
|
1555
|
-
result: null
|
|
1650
|
+
result: null,
|
|
1651
|
+
diffRef,
|
|
1652
|
+
lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
|
|
1653
|
+
lastDiffSet: diffRef ? payload.diffSet : void 0,
|
|
1654
|
+
hasNewChanges: false
|
|
1556
1655
|
};
|
|
1557
1656
|
sessions2.set(sessionId, session);
|
|
1657
|
+
if (diffRef && hasConnectedClients()) {
|
|
1658
|
+
startSessionWatcher(sessionId);
|
|
1659
|
+
}
|
|
1558
1660
|
broadcastToAll({
|
|
1559
1661
|
type: "session:added",
|
|
1560
1662
|
payload: toSummary(session)
|
|
@@ -1644,6 +1746,7 @@ async function handleApiRequest(req, res) {
|
|
|
1644
1746
|
}
|
|
1645
1747
|
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
1646
1748
|
if (deleteParams) {
|
|
1749
|
+
stopSessionWatcher(deleteParams.id);
|
|
1647
1750
|
if (sessions2.delete(deleteParams.id)) {
|
|
1648
1751
|
jsonResponse(res, 200, { ok: true });
|
|
1649
1752
|
} else {
|
|
@@ -1659,8 +1762,10 @@ async function startGlobalServer(options = {}) {
|
|
|
1659
1762
|
httpPort: preferredHttpPort = 24680,
|
|
1660
1763
|
wsPort: preferredWsPort = 24681,
|
|
1661
1764
|
silent = false,
|
|
1662
|
-
dev = false
|
|
1765
|
+
dev = false,
|
|
1766
|
+
pollInterval = 2e3
|
|
1663
1767
|
} = options;
|
|
1768
|
+
serverPollInterval = pollInterval;
|
|
1664
1769
|
const [httpPort, wsPort] = await Promise.all([
|
|
1665
1770
|
getPort3({ port: preferredHttpPort }),
|
|
1666
1771
|
getPort3({ port: preferredWsPort })
|
|
@@ -1686,6 +1791,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1686
1791
|
});
|
|
1687
1792
|
wss = new WebSocketServer3({ port: wsPort });
|
|
1688
1793
|
wss.on("connection", (ws, req) => {
|
|
1794
|
+
startAllWatchers();
|
|
1689
1795
|
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
1690
1796
|
const sessionId = url.searchParams.get("sessionId");
|
|
1691
1797
|
if (sessionId) {
|
|
@@ -1693,6 +1799,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1693
1799
|
const session = sessions2.get(sessionId);
|
|
1694
1800
|
if (session) {
|
|
1695
1801
|
session.status = "in_review";
|
|
1802
|
+
session.hasNewChanges = false;
|
|
1696
1803
|
const msg = {
|
|
1697
1804
|
type: "review:init",
|
|
1698
1805
|
payload: session.payload
|
|
@@ -1711,6 +1818,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1711
1818
|
if (session) {
|
|
1712
1819
|
clientSessions.set(ws, session.id);
|
|
1713
1820
|
session.status = "in_review";
|
|
1821
|
+
session.hasNewChanges = false;
|
|
1714
1822
|
ws.send(JSON.stringify({
|
|
1715
1823
|
type: "review:init",
|
|
1716
1824
|
payload: session.payload
|
|
@@ -1735,17 +1843,32 @@ async function startGlobalServer(options = {}) {
|
|
|
1735
1843
|
if (session) {
|
|
1736
1844
|
clientSessions.set(ws, session.id);
|
|
1737
1845
|
session.status = "in_review";
|
|
1846
|
+
session.hasNewChanges = false;
|
|
1847
|
+
startSessionWatcher(session.id);
|
|
1738
1848
|
ws.send(JSON.stringify({
|
|
1739
1849
|
type: "review:init",
|
|
1740
1850
|
payload: session.payload
|
|
1741
1851
|
}));
|
|
1742
1852
|
}
|
|
1853
|
+
} else if (msg.type === "session:close") {
|
|
1854
|
+
const closedId = msg.payload.sessionId;
|
|
1855
|
+
stopSessionWatcher(closedId);
|
|
1856
|
+
sessions2.delete(closedId);
|
|
1857
|
+
for (const [client, sid] of clientSessions.entries()) {
|
|
1858
|
+
if (sid === closedId) {
|
|
1859
|
+
clientSessions.delete(client);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
broadcastSessionList();
|
|
1743
1863
|
}
|
|
1744
1864
|
} catch {
|
|
1745
1865
|
}
|
|
1746
1866
|
});
|
|
1747
1867
|
ws.on("close", () => {
|
|
1748
1868
|
clientSessions.delete(ws);
|
|
1869
|
+
if (!hasConnectedClients()) {
|
|
1870
|
+
stopAllWatchers();
|
|
1871
|
+
}
|
|
1749
1872
|
});
|
|
1750
1873
|
});
|
|
1751
1874
|
await new Promise((resolve, reject) => {
|
|
@@ -1772,19 +1895,13 @@ Waiting for reviews...
|
|
|
1772
1895
|
}
|
|
1773
1896
|
const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&serverMode=true`;
|
|
1774
1897
|
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
1898
|
reopenBrowserIfNeeded = () => {
|
|
1783
1899
|
if (!hasConnectedClients()) {
|
|
1784
1900
|
open3(uiUrl);
|
|
1785
1901
|
}
|
|
1786
1902
|
};
|
|
1787
1903
|
async function stop() {
|
|
1904
|
+
stopAllWatchers();
|
|
1788
1905
|
if (wss) {
|
|
1789
1906
|
for (const client of wss.clients) {
|
|
1790
1907
|
client.close();
|
package/dist/mcp-server.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
readReviewResult,
|
|
8
8
|
readWatchFile,
|
|
9
9
|
startReview
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-L2D5SDUV.js";
|
|
11
11
|
|
|
12
12
|
// packages/mcp-server/src/index.ts
|
|
13
13
|
import fs from "fs";
|
|
@@ -47,7 +47,7 @@ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
|
|
|
47
47
|
{
|
|
48
48
|
method: "POST",
|
|
49
49
|
headers: { "Content-Type": "application/json" },
|
|
50
|
-
body: JSON.stringify({ payload, projectPath: cwd })
|
|
50
|
+
body: JSON.stringify({ payload, projectPath: cwd, diffRef })
|
|
51
51
|
}
|
|
52
52
|
);
|
|
53
53
|
if (!createResponse.ok) {
|
|
@@ -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.
|
|
79
|
+
version: true ? "0.20.1" : "0.0.0-dev"
|
|
80
80
|
});
|
|
81
81
|
server.tool(
|
|
82
82
|
"open_review",
|