diffing 0.2.0 → 0.2.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.
Files changed (92) hide show
  1. package/README.md +28 -4
  2. package/dist/cli-agent-BLPX3oOP.mjs +443 -0
  3. package/dist/cli-agent-BNwY5lZE.mjs +446 -0
  4. package/dist/cli-agent-CbzXDSSF.mjs +446 -0
  5. package/dist/cli-agent-DsqbS3n7.mjs +443 -0
  6. package/dist/cli-agent-ZAkixf4c.mjs +446 -0
  7. package/dist/cli.mjs +539 -33
  8. package/dist/client/assets/{angular-html-fdYbuxHF.js → angular-html-DsssUIMZ.js} +1 -1
  9. package/dist/client/assets/{angular-ts-D3iFqR-R.js → angular-ts-dTg-sJ77.js} +1 -1
  10. package/dist/client/assets/{apl-MnBV-iQN.js → apl-C1yUKbqz.js} +1 -1
  11. package/dist/client/assets/{astro-BrpK01E7.js → astro-BI07ha2y.js} +1 -1
  12. package/dist/client/assets/{blade-CGmyBuwD.js → blade-DkzUGffS.js} +1 -1
  13. package/dist/client/assets/{c-xQnwtG9Y.js → c-KkuqqOvW.js} +1 -1
  14. package/dist/client/assets/{cobol-ypVzOIQu.js → cobol-DRh7-jLi.js} +1 -1
  15. package/dist/client/assets/{coffee-CrtE1uj2.js → coffee-wR_ZAh9a.js} +1 -1
  16. package/dist/client/assets/{cpp-DLd4XEKA.js → cpp-OD_qyywn.js} +1 -1
  17. package/dist/client/assets/{crystal-CJYvpLt_.js → crystal-E4ahAddE.js} +1 -1
  18. package/dist/client/assets/{css-DrX2B9os.js → css-2NgE_uxz.js} +1 -1
  19. package/dist/client/assets/{edge-BsQYk9OW.js → edge-8mbscxxD.js} +1 -1
  20. package/dist/client/assets/{elixir-DU_VfGYR.js → elixir-CJv1b3Q6.js} +1 -1
  21. package/dist/client/assets/{elm-W4eOLEDv.js → elm-DUPzqZ9H.js} +1 -1
  22. package/dist/client/assets/{erb-D9FdIJ2e.js → erb-ssvEF26z.js} +1 -1
  23. package/dist/client/assets/{git-rebase-BtPCXjbS.js → git-rebase-DiWIK1Oa.js} +1 -1
  24. package/dist/client/assets/{glimmer-js-BVfUGMUb.js → glimmer-js-DrHg6AuA.js} +1 -1
  25. package/dist/client/assets/{glimmer-ts-BZEaHDZL.js → glimmer-ts-wQSGOa8A.js} +1 -1
  26. package/dist/client/assets/{glsl-CSiNDuwt.js → glsl-OY27QGY0.js} +1 -1
  27. package/dist/client/assets/{graphql-Crf08Rhb.js → graphql-CSWSqe6f.js} +1 -1
  28. package/dist/client/assets/{hack-DUiQuO20.js → hack-Bi1v624Q.js} +1 -1
  29. package/dist/client/assets/{haml-BtvVTDRG.js → haml-CX94zV9m.js} +1 -1
  30. package/dist/client/assets/{handlebars-Bu_cdZA_.js → handlebars-C1j6wnjG.js} +1 -1
  31. package/dist/client/assets/{html-C4cun3zD.js → html-BcAjMlB7.js} +1 -1
  32. package/dist/client/assets/{html-derivative-CQnD80KV.js → html-derivative-DcEpOzdx.js} +1 -1
  33. package/dist/client/assets/{http-DuOJF9Kg.js → http-BBxxSc6S.js} +1 -1
  34. package/dist/client/assets/{hurl-DgrdYsYh.js → hurl-CpKRNTz4.js} +1 -1
  35. package/dist/client/assets/{index-CZZd7iTh.js → index-Bwv7BWUp.js} +156 -84
  36. package/dist/client/assets/index-CoZyJYkw.css +1 -0
  37. package/dist/client/assets/{java-D-vtFiv8.js → java-CFUlLvAA.js} +1 -1
  38. package/dist/client/assets/{javascript-CFYFXvdN.js → javascript-C-HnT3sE.js} +1 -1
  39. package/dist/client/assets/{jinja-fZvIg2_s.js → jinja-CPU8RwNP.js} +1 -1
  40. package/dist/client/assets/{jison-DEzxLq5g.js → jison-CW0TBQOs.js} +1 -1
  41. package/dist/client/assets/{json-QM4VBqMC.js → json-qUjlNuEJ.js} +1 -1
  42. package/dist/client/assets/{jsx-DLzUM_cC.js → jsx-KWeo60kf.js} +1 -1
  43. package/dist/client/assets/{julia-Dwt1SIpn.js → julia-CnJB8y2e.js} +1 -1
  44. package/dist/client/assets/{just-Dwp-hLcL.js → just-Do22d9PP.js} +1 -1
  45. package/dist/client/assets/{latex-CMF1JViz.js → latex-CCPgzvKn.js} +1 -1
  46. package/dist/client/assets/{liquid-czkpOZWY.js → liquid-B9btdZoI.js} +1 -1
  47. package/dist/client/assets/{lua-DE5PD0Vh.js → lua-IFRifurW.js} +1 -1
  48. package/dist/client/assets/{marko-DHeEvZiW.js → marko-CCaVBFq1.js} +1 -1
  49. package/dist/client/assets/{mdc-B941liUQ.js → mdc-BtbnOLUc.js} +1 -1
  50. package/dist/client/assets/{nginx-Pb9t8c-r.js → nginx-BkbFZHVt.js} +1 -1
  51. package/dist/client/assets/{nim-MMUvW_R1.js → nim-2uCd5j65.js} +1 -1
  52. package/dist/client/assets/{perl-DXwXRmuC.js → perl-C4ixqSNT.js} +1 -1
  53. package/dist/client/assets/{php-D42kv59r.js → php-DJ8WOtcI.js} +1 -1
  54. package/dist/client/assets/{pug-v0iesski.js → pug-BOaafhT5.js} +1 -1
  55. package/dist/client/assets/{qml-spxQQ-ei.js → qml-CURE-1y4.js} +1 -1
  56. package/dist/client/assets/{r-CobWTBG9.js → r-CPWmgzo3.js} +1 -1
  57. package/dist/client/assets/{razor-B-_Ww3MB.js → razor-DH83VP_d.js} +1 -1
  58. package/dist/client/assets/{regexp-BJMVs8R0.js → regexp-BXVWi-t_.js} +1 -1
  59. package/dist/client/assets/{rst-BHcaWsY0.js → rst-Bbxxs6E2.js} +1 -1
  60. package/dist/client/assets/{ruby-BCMT6kog.js → ruby-C-TuWES4.js} +1 -1
  61. package/dist/client/assets/{sas-DHfZnd3R.js → sas-njzNYNOq.js} +1 -1
  62. package/dist/client/assets/{scss-DjDsJvDO.js → scss-D2i6rHB8.js} +1 -1
  63. package/dist/client/assets/{shellscript-B6qaIiU-.js → shellscript-D6Bxi40C.js} +1 -1
  64. package/dist/client/assets/{shellsession-ByCeir2b.js → shellsession-Defxq-I6.js} +1 -1
  65. package/dist/client/assets/{soy-C6WpnMSF.js → soy-D0GKXrtT.js} +1 -1
  66. package/dist/client/assets/{sql-BIvSNLV_.js → sql-BRW_ADkU.js} +1 -1
  67. package/dist/client/assets/{stata-PG9iZ--I.js → stata-w7w59F48.js} +1 -1
  68. package/dist/client/assets/{surrealql-CmvCzG09.js → surrealql-O-lLuqz7.js} +1 -1
  69. package/dist/client/assets/{svelte-ChumG88s.js → svelte-DN3DR5-t.js} +1 -1
  70. package/dist/client/assets/{templ-CmzuRY_a.js → templ-CJ2JFoia.js} +1 -1
  71. package/dist/client/assets/{tex-DL8t-_eJ.js → tex-CpWrf5g9.js} +1 -1
  72. package/dist/client/assets/{ts-tags-BFfE0Egm.js → ts-tags-CZ6Af6Pe.js} +1 -1
  73. package/dist/client/assets/{tsx-C1bFyHcE.js → tsx-gIUSpRih.js} +1 -1
  74. package/dist/client/assets/{twig-DkIluZEQ.js → twig-DwZINhmw.js} +1 -1
  75. package/dist/client/assets/{typescript-Gc86Bc3j.js → typescript-Dj2aMECR.js} +1 -1
  76. package/dist/client/assets/{vue-C88JmL53.js → vue-B5K0aWrP.js} +1 -1
  77. package/dist/client/assets/{vue-html-Cry4aksC.js → vue-html-DKGgrypd.js} +1 -1
  78. package/dist/client/assets/{vue-vine-zlDkA3Ui.js → vue-vine-B5iDYTCh.js} +1 -1
  79. package/dist/client/assets/{xml-NvwpIFDy.js → xml-CbTzxfc-.js} +1 -1
  80. package/dist/client/assets/{xsl-CDjXvY9w.js → xsl-Dj-Be6nx.js} +1 -1
  81. package/dist/client/assets/{yaml-Bh-LPjcH.js → yaml-rTK8uEjh.js} +1 -1
  82. package/dist/client/index.html +2 -5
  83. package/dist/mcp-CE77qfjx.mjs +254 -0
  84. package/dist/mcp-CFeE13n-.mjs +254 -0
  85. package/dist/mcp-SGCrfYGo.mjs +254 -0
  86. package/dist/server-lock-B4crz9-_.mjs +708 -0
  87. package/dist/server-lock-DpCrvL-Z.mjs +714 -0
  88. package/dist/update-check-DJuFE9w6.mjs +128 -0
  89. package/dist/update-check-DSC02MWQ.mjs +129 -0
  90. package/dist/update-check-KL-2sGxE.mjs +129 -0
  91. package/package.json +12 -14
  92. package/dist/client/assets/index-CkC-EvWo.css +0 -1
@@ -0,0 +1,714 @@
1
+ import { basename, join, relative, resolve } from "node:path";
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ import { execFile, execFileSync } from "node:child_process";
5
+ import { readFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { createHash } from "node:crypto";
8
+ import { parseSync } from "editorconfig";
9
+ //#region src/lib/path.ts
10
+ function decodeAndNormalize(p) {
11
+ let decoded = p;
12
+ try {
13
+ decoded = decodeURIComponent(p);
14
+ } catch {}
15
+ return decoded.replace(/\\/g, "/");
16
+ }
17
+ function toSafeRelativePath(filePath, baseDir) {
18
+ const normalized = decodeAndNormalize(filePath);
19
+ if (normalized.includes("..") || normalized.includes("\0")) return null;
20
+ const resolved = resolve(baseDir, normalized);
21
+ const resolvedBase = resolve(baseDir);
22
+ if (!(resolved.startsWith(resolvedBase + "/") || resolved === resolvedBase)) return null;
23
+ return relative(resolvedBase, resolved);
24
+ }
25
+ function isSafePath(relativePath, baseDir) {
26
+ return toSafeRelativePath(relativePath, baseDir) !== null;
27
+ }
28
+ //#endregion
29
+ //#region src/lib/git.ts
30
+ const execFileAsync = promisify(execFile);
31
+ function isBinaryFile(absolutePath) {
32
+ try {
33
+ const buffer = readFileSync(absolutePath);
34
+ const bytesToCheck = Math.min(buffer.length, 8192);
35
+ for (let i = 0; i < bytesToCheck; i++) if (buffer[i] === 0) return true;
36
+ return false;
37
+ } catch {
38
+ return true;
39
+ }
40
+ }
41
+ function getFileContent(filePath, version) {
42
+ const root = getRepoRoot();
43
+ const relPath = toSafeRelativePath(filePath, root);
44
+ if (!relPath) return null;
45
+ const resolved = resolve(root, relPath);
46
+ if (version === "new") try {
47
+ return readFileSync(resolved);
48
+ } catch {
49
+ return null;
50
+ }
51
+ try {
52
+ return execFileSync("git", ["show", `HEAD:${relPath}`], {
53
+ stdio: "pipe",
54
+ maxBuffer: 50 * 1024 * 1024
55
+ });
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ let cachedRepoRoot = null;
61
+ const editorConfigCache = /* @__PURE__ */ new Map();
62
+ function isGitRepo() {
63
+ try {
64
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ function getRepoRoot() {
71
+ if (cachedRepoRoot !== null) return cachedRepoRoot;
72
+ cachedRepoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
73
+ return cachedRepoRoot;
74
+ }
75
+ function getRepoName() {
76
+ return basename(getRepoRoot());
77
+ }
78
+ function getBranchName() {
79
+ try {
80
+ return execFileSync("git", [
81
+ "rev-parse",
82
+ "--abbrev-ref",
83
+ "HEAD"
84
+ ], {
85
+ stdio: "pipe",
86
+ encoding: "utf-8"
87
+ }).trim();
88
+ } catch {
89
+ return "";
90
+ }
91
+ }
92
+ const DIFF_FLAGS = ["--no-ext-diff", "--no-color"];
93
+ function getTabSizeForFiles(filePaths) {
94
+ const root = getRepoRoot();
95
+ const result = {};
96
+ for (const filePath of filePaths) try {
97
+ const config = parseSync(join(root, filePath), { cache: editorConfigCache });
98
+ const size = config.tab_width ?? (config.indent_size === "tab" ? void 0 : config.indent_size);
99
+ if (typeof size === "number") result[filePath] = size;
100
+ } catch {}
101
+ return result;
102
+ }
103
+ async function getRepoRootAsync() {
104
+ if (cachedRepoRoot !== null) return cachedRepoRoot;
105
+ try {
106
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" });
107
+ cachedRepoRoot = stdout.trim();
108
+ return cachedRepoRoot;
109
+ } catch {
110
+ return "";
111
+ }
112
+ }
113
+ async function getUntrackedFilePathsAsync() {
114
+ try {
115
+ const { stdout } = await execFileAsync("git", [
116
+ "ls-files",
117
+ "--others",
118
+ "--exclude-standard"
119
+ ], {
120
+ encoding: "utf-8",
121
+ maxBuffer: 50 * 1024 * 1024
122
+ });
123
+ const trimmed = stdout.trim();
124
+ return trimmed ? trimmed.split("\n") : [];
125
+ } catch {
126
+ return [];
127
+ }
128
+ }
129
+ async function getUntrackedFilesDiffAsync() {
130
+ const root = await getRepoRootAsync();
131
+ const filePaths = await getUntrackedFilePathsAsync();
132
+ if (filePaths.length === 0) return "";
133
+ const patches = [];
134
+ const fileDiffPromises = filePaths.map(async (file) => {
135
+ const absolutePath = join(root, file);
136
+ if (isBinaryFile(absolutePath)) return [
137
+ `diff --git a/${file} b/${file}`,
138
+ "new file mode 100644",
139
+ "index 0000000..0000001",
140
+ `Binary files /dev/null and b/${file} differ`
141
+ ].join("\n");
142
+ else try {
143
+ const lines = (await readFile(absolutePath, "utf-8")).split("\n");
144
+ const diffLines = lines.map((l) => `+${l}`);
145
+ return [
146
+ `diff --git a/${file} b/${file}`,
147
+ "new file mode 100644",
148
+ "index 0000000..0000001",
149
+ "--- /dev/null",
150
+ `+++ b/${file}`,
151
+ `@@ -0,0 +1,${lines.length} @@`,
152
+ ...diffLines
153
+ ].join("\n");
154
+ } catch {
155
+ return "";
156
+ }
157
+ });
158
+ const results = await Promise.all(fileDiffPromises);
159
+ for (const r of results) if (r) patches.push(r);
160
+ return patches.length > 0 ? "\n" + patches.join("\n") : "";
161
+ }
162
+ async function getCustomGitDiffAsync(args) {
163
+ try {
164
+ const { stdout } = await execFileAsync("git", [
165
+ "diff",
166
+ ...DIFF_FLAGS,
167
+ ...args
168
+ ], {
169
+ encoding: "utf-8",
170
+ maxBuffer: 50 * 1024 * 1024
171
+ });
172
+ return stdout;
173
+ } catch {
174
+ return "";
175
+ }
176
+ }
177
+ async function getGitDiffAsync(options = {}) {
178
+ const unstagedPromise = execFileAsync("git", ["diff", ...DIFF_FLAGS], {
179
+ encoding: "utf-8",
180
+ maxBuffer: 50 * 1024 * 1024
181
+ }).then(({ stdout }) => stdout).catch(() => "");
182
+ const stagedPromise = options.staged ? execFileAsync("git", [
183
+ "diff",
184
+ ...DIFF_FLAGS,
185
+ "--staged"
186
+ ], {
187
+ encoding: "utf-8",
188
+ maxBuffer: 50 * 1024 * 1024
189
+ }).then(({ stdout }) => stdout).catch(() => "") : Promise.resolve("");
190
+ const untrackedPromise = options.untracked ? getUntrackedFilesDiffAsync() : Promise.resolve("");
191
+ const [unstaged, staged, untrackedPatch] = await Promise.all([
192
+ unstagedPromise,
193
+ stagedPromise,
194
+ untrackedPromise
195
+ ]);
196
+ const parts = [];
197
+ if (unstaged) parts.push(unstaged);
198
+ if (staged) parts.push(staged);
199
+ if (untrackedPatch) parts.push(untrackedPatch);
200
+ return parts.join("\n");
201
+ }
202
+ /**
203
+ * Lists every file the working tree knows about (tracked + untracked,
204
+ * excluding standard-ignored paths). Used by the Phase D file viewer to
205
+ * power a path picker.
206
+ */
207
+ function listRepoFiles() {
208
+ try {
209
+ const tracked = execFileSync("git", ["ls-files"], {
210
+ encoding: "utf-8",
211
+ maxBuffer: 100 * 1024 * 1024
212
+ }).trim();
213
+ const untracked = execFileSync("git", [
214
+ "ls-files",
215
+ "--others",
216
+ "--exclude-standard"
217
+ ], {
218
+ encoding: "utf-8",
219
+ maxBuffer: 100 * 1024 * 1024
220
+ }).trim();
221
+ const set = /* @__PURE__ */ new Set();
222
+ for (const list of [tracked, untracked]) {
223
+ if (!list) continue;
224
+ for (const line of list.split("\n")) if (line) set.add(line);
225
+ }
226
+ return [...set].sort();
227
+ } catch {
228
+ return [];
229
+ }
230
+ }
231
+ /**
232
+ * Reports whether the repo is currently in the middle of a merge and which
233
+ * files have unresolved conflicts. Used by the Phase C merge-conflict UI to
234
+ * decide when to render the @pierre/diffs UnresolvedFile component.
235
+ */
236
+ function getMergeStatus() {
237
+ let root;
238
+ try {
239
+ root = getRepoRoot();
240
+ } catch {
241
+ return {
242
+ inMerge: false,
243
+ conflicts: []
244
+ };
245
+ }
246
+ const mergeHead = join(root, ".git", "MERGE_HEAD");
247
+ let inMerge = false;
248
+ try {
249
+ readFileSync(mergeHead);
250
+ inMerge = true;
251
+ } catch {
252
+ inMerge = false;
253
+ }
254
+ let conflicts = [];
255
+ try {
256
+ const out = execFileSync("git", [
257
+ "diff",
258
+ "--name-only",
259
+ "--diff-filter=U"
260
+ ], {
261
+ encoding: "utf-8",
262
+ maxBuffer: 50 * 1024 * 1024
263
+ }).trim();
264
+ conflicts = out ? out.split("\n") : [];
265
+ } catch {
266
+ conflicts = [];
267
+ }
268
+ return {
269
+ inMerge: inMerge || conflicts.length > 0,
270
+ conflicts
271
+ };
272
+ }
273
+ function gitAddFile(filePath) {
274
+ execFileSync("git", [
275
+ "add",
276
+ "--",
277
+ filePath
278
+ ], { stdio: "pipe" });
279
+ }
280
+ /**
281
+ * Returns a unified diff for a single working-tree file (covers unstaged
282
+ * and untracked changes). Used by the Phase E hunk-revert flow to derive
283
+ * the exact patch text we need to `git apply --reverse`.
284
+ */
285
+ function getFilePatch(filePath) {
286
+ try {
287
+ const unstaged = execFileSync("git", [
288
+ "diff",
289
+ ...DIFF_FLAGS,
290
+ "--",
291
+ filePath
292
+ ], {
293
+ encoding: "utf-8",
294
+ maxBuffer: 50 * 1024 * 1024
295
+ });
296
+ if (unstaged) return unstaged;
297
+ } catch {}
298
+ try {
299
+ const absolutePath = join(getRepoRoot(), filePath);
300
+ if (isBinaryFile(absolutePath)) return "";
301
+ const lines = readFileSync(absolutePath, "utf-8").split("\n");
302
+ return [
303
+ `diff --git a/${filePath} b/${filePath}`,
304
+ "new file mode 100644",
305
+ "index 0000000..0000001",
306
+ "--- /dev/null",
307
+ `+++ b/${filePath}`,
308
+ `@@ -0,0 +1,${lines.length} @@`,
309
+ ...lines.map((l) => `+${l}`)
310
+ ].join("\n");
311
+ } catch {
312
+ return "";
313
+ }
314
+ }
315
+ /**
316
+ * Extracts the Nth hunk text from a single-file patch. Returns the lines
317
+ * from `@@ ... @@` through the line before the next `@@` (or EOF).
318
+ */
319
+ function extractHunk(patch, hunkIndex) {
320
+ const lines = patch.split("\n");
321
+ const hunkStarts = [];
322
+ for (let i = 0; i < lines.length; i++) if (lines[i].startsWith("@@ ")) hunkStarts.push(i);
323
+ if (hunkIndex < 0 || hunkIndex >= hunkStarts.length) return null;
324
+ const start = hunkStarts[hunkIndex];
325
+ const end = hunkIndex + 1 < hunkStarts.length ? hunkStarts[hunkIndex + 1] : lines.length;
326
+ return lines.slice(start, end).join("\n");
327
+ }
328
+ /** Header lines that the per-file patch needs in order for `git apply` to
329
+ * recognize the target. Stops just before the first `@@` hunk header. */
330
+ function extractPatchHeader(patch) {
331
+ const lines = patch.split("\n");
332
+ const out = [];
333
+ for (const line of lines) {
334
+ if (line.startsWith("@@ ")) break;
335
+ out.push(line);
336
+ }
337
+ return out.join("\n");
338
+ }
339
+ /**
340
+ * Reverts a single hunk from the working tree by piping a minimal patch to
341
+ * `git apply --reverse`. Throws if git rejects the apply (typically because
342
+ * the file has shifted since the diff was rendered).
343
+ */
344
+ function revertHunk(filePath, hunkIndex) {
345
+ const patch = getFilePatch(filePath);
346
+ if (!patch) throw new Error("No diff available for this file");
347
+ const header = extractPatchHeader(patch);
348
+ const hunk = extractHunk(patch, hunkIndex);
349
+ if (!hunk) throw new Error(`Hunk ${hunkIndex} not found`);
350
+ execFileSync("git", [
351
+ "apply",
352
+ "--reverse",
353
+ "-"
354
+ ], {
355
+ input: `${header}\n${hunk}\n`,
356
+ stdio: [
357
+ "pipe",
358
+ "pipe",
359
+ "pipe"
360
+ ]
361
+ });
362
+ }
363
+ function getProjectStorageDir(customRepoRoot) {
364
+ const root = customRepoRoot || getRepoRoot();
365
+ const hash = createHash("sha256").update(root).digest("hex").slice(0, 8);
366
+ const repoName = basename(root);
367
+ return join(homedir(), ".diffing", `${repoName}-${hash}`);
368
+ }
369
+ /**
370
+ * Retrieves git blame for deleted lines and recent log history for the file,
371
+ * providing context on what commit introduced the deleted code.
372
+ */
373
+ function getHunkHistory(filePath, deletionStart, deletionCount, revision = "HEAD") {
374
+ if (!isSafePath(filePath, getRepoRoot())) throw new Error("Forbidden file path");
375
+ const blame = [];
376
+ if (deletionCount > 0) try {
377
+ const lines = execFileSync("git", [
378
+ "blame",
379
+ "--date=format:%Y-%m-%d",
380
+ "-L",
381
+ `${deletionStart},+${deletionCount}`,
382
+ revision,
383
+ "--",
384
+ filePath
385
+ ], {
386
+ encoding: "utf-8",
387
+ maxBuffer: 10 * 1024 * 1024
388
+ }).split("\n");
389
+ const commitHashes = /* @__PURE__ */ new Set();
390
+ for (const line of lines) {
391
+ if (!line.trim()) continue;
392
+ const match = line.match(/^(\^?[0-9a-fA-F]+) \((.*?) (\d{4}-\d{2}-\d{2}) (\d+)\)(?: (.*))?$/);
393
+ if (match) {
394
+ const [_, commit, author, date, lineNum, content] = match;
395
+ const cleanCommit = commit.startsWith("^") ? commit.slice(1) : commit;
396
+ blame.push({
397
+ commit: cleanCommit,
398
+ author: author.trim(),
399
+ date,
400
+ line: parseInt(lineNum, 10),
401
+ content: content || "",
402
+ summary: ""
403
+ });
404
+ commitHashes.add(cleanCommit);
405
+ }
406
+ }
407
+ const commitSummaries = {};
408
+ for (const hash of commitHashes) try {
409
+ commitSummaries[hash] = execFileSync("git", [
410
+ "show",
411
+ "-s",
412
+ "--format=%s",
413
+ hash
414
+ ], { encoding: "utf-8" }).trim();
415
+ } catch {
416
+ commitSummaries[hash] = "Unknown commit";
417
+ }
418
+ for (const entry of blame) entry.summary = commitSummaries[entry.commit] || "Unknown commit";
419
+ } catch (err) {
420
+ console.warn("Failed to get git blame for hunk history:", err.message);
421
+ }
422
+ const recentCommits = [];
423
+ try {
424
+ const logOut = execFileSync("git", [
425
+ "log",
426
+ "-n",
427
+ "5",
428
+ "--date=format:%Y-%m-%d",
429
+ "--format=%h|%an|%ad|%s",
430
+ "--",
431
+ filePath
432
+ ], { encoding: "utf-8" });
433
+ for (const line of logOut.split("\n")) {
434
+ if (!line.trim()) continue;
435
+ const [hash, author, date, summary] = line.split("|");
436
+ if (hash && author && date && summary) recentCommits.push({
437
+ hash,
438
+ author: author.trim(),
439
+ date,
440
+ summary: summary.trim()
441
+ });
442
+ }
443
+ } catch (err) {
444
+ console.warn("Failed to get git log for hunk history:", err.message);
445
+ }
446
+ return {
447
+ blame,
448
+ recentCommits
449
+ };
450
+ }
451
+ //#endregion
452
+ //#region src/lib/comment-format.ts
453
+ /** Plain-language guidance the agent should act on, derived from the verdict. */
454
+ function reviewDecisionSummary(decision) {
455
+ switch (decision) {
456
+ case "approved": return "The reviewer APPROVED these changes. Address any open comments below, then proceed — the overall direction is good.";
457
+ case "changes-requested": return "The reviewer REQUESTED EDITS. Address every open comment below and apply the requested changes before considering this review done.";
458
+ case "rejected": return "The reviewer REJECTED these changes. Do NOT keep building on this approach; reconsider it in light of the comments below before continuing.";
459
+ }
460
+ }
461
+ /**
462
+ * Render review comments as the `<code-review-comments>` XML envelope used to
463
+ * hand a review to an AI agent. Shared by the UI clipboard button
464
+ * (`useComments.formatAllComments`), the server's `/api/review/send` handoff,
465
+ * and the `diffing` CLI/MCP so every channel emits byte-identical output.
466
+ *
467
+ * A review may carry a headline `decision` (approve / request edits / reject)
468
+ * and/or an overall comment, so the envelope is emitted even with zero inline
469
+ * comments — letting a reviewer submit a verdict without annotating any line.
470
+ */
471
+ function formatComments(comments, generalComment, decision) {
472
+ const trimmedGeneral = generalComment?.trim();
473
+ if (comments.length === 0 && !decision && !trimmedGeneral) return "";
474
+ const grouped = /* @__PURE__ */ new Map();
475
+ for (const comment of comments) {
476
+ const list = grouped.get(comment.filePath) ?? [];
477
+ list.push(comment);
478
+ grouped.set(comment.filePath, list);
479
+ }
480
+ const lines = [];
481
+ lines.push(decision ? `<code-review-comments decision="${decision}">` : "<code-review-comments>");
482
+ lines.push(" <instructions>");
483
+ lines.push(" You are an AI coding assistant. You are receiving a structured list of code review comments to address in the repository.");
484
+ lines.push(" For each file, review the inline comments and apply the changes requested.");
485
+ if (decision) {
486
+ lines.push(" - The \"decision\" attribute on the root element is the reviewer's headline verdict: \"approved\", \"changes-requested\", or \"rejected\".");
487
+ lines.push(" - <decision-summary> tells you, in plain language, what to do next based on that verdict.");
488
+ }
489
+ lines.push(" - Target lines are specified by the \"line\" attribute (e.g. line=\"10\" or line=\"10-15\").");
490
+ lines.push(" - \"side\" indicates whether the comment is on \"additions\" (added/modified lines) or \"deletions\" (deleted/old lines).");
491
+ lines.push(" - \"status\" indicates whether the comment is \"open\" or \"resolved\". Only address comments with status=\"open\".");
492
+ lines.push(" - The <code> block contains the specific code context at the reviewed lines, prefixed with \"+\" or \"-\".");
493
+ lines.push(" - The <body> tag contains the review feedback or request.");
494
+ lines.push(" - If developers have replied to the comment, their discussion is captured under the <replies> element.");
495
+ lines.push(" - The comment \"id\" attribute can be used to reference or update the comment via API if available.");
496
+ lines.push("");
497
+ lines.push(" HOW TO REPLY OR ASK FOR CLARIFICATION:");
498
+ lines.push(" If you need to ask for clarification, explain what you did, or reply to any comment:");
499
+ lines.push("");
500
+ lines.push(" Option A: Via the diffing CLI or MCP (Preferred — port-agnostic, no copy-paste)");
501
+ lines.push(" diffing reply <comment-id> --body \"Your response\" --model \"<your-model-name>\"");
502
+ lines.push(" diffing resolve <comment-id>");
503
+ lines.push(" (Or the equivalent MCP tools: reply_to_comment, resolve_comment.)");
504
+ lines.push("");
505
+ lines.push(" Option B: Via the local HTTP API (if you know the running port)");
506
+ lines.push(" POST http://localhost:<port>/api/comments/<comment-id>/replies");
507
+ lines.push(" Payload: { \"body\": \"Your response or clarification request here\", \"model\": \"<your-model-name>\" }");
508
+ lines.push(" PUT http://localhost:<port>/api/comments/<comment-id> Payload: { \"status\": \"resolved\" }");
509
+ lines.push("");
510
+ lines.push(" Option C: Via Text Response (Offline / Chat Copy-Paste)");
511
+ lines.push(" If you do not have local API access, output your comments/replies inside a structured XML block at the end of your response:");
512
+ lines.push(" <comment-replies>");
513
+ lines.push(" <reply to=\"<comment-id>\" model=\"<your-model-name>\"><![CDATA[Your reply or clarification request here]]></reply>");
514
+ lines.push(" </comment-replies>");
515
+ lines.push(" </instructions>");
516
+ if (decision) lines.push(` <decision-summary><![CDATA[${reviewDecisionSummary(decision)}]]></decision-summary>`);
517
+ if (trimmedGeneral) {
518
+ lines.push(" <general-comment>");
519
+ lines.push(` <![CDATA[${trimmedGeneral}]]>`);
520
+ lines.push(" </general-comment>");
521
+ }
522
+ for (const [filePath, fileComments] of grouped) {
523
+ lines.push(` <file path="${filePath}">`);
524
+ for (const comment of fileComments) {
525
+ const lineAttr = comment.lineNumber === 0 ? "file" : comment.startLineNumber && comment.startLineNumber !== comment.lineNumber ? `${comment.startLineNumber}-${comment.lineNumber}` : `${comment.lineNumber}`;
526
+ const isoDate = new Date(comment.createdAt).toISOString();
527
+ lines.push(` <comment id="${comment.id}" line="${lineAttr}" side="${comment.side}" status="${comment.status}" created-at="${isoDate}">`);
528
+ if (comment.lineNumber !== 0) {
529
+ const prefix = comment.side === "additions" ? "+" : "-";
530
+ const isMultiLine = comment.lineContent && comment.lineContent.includes("\n");
531
+ let codeVal = "";
532
+ if (isMultiLine) codeVal = `\n${comment.lineContent.split("\n").map((l) => `${prefix} ${l}`).join("\n")}\n`;
533
+ else codeVal = `${prefix} ${comment.lineContent}`;
534
+ lines.push(` <code><![CDATA[${codeVal}]]></code>`);
535
+ }
536
+ lines.push(` <body><![CDATA[${comment.body}]]></body>`);
537
+ if (comment.replies && comment.replies.length > 0) {
538
+ lines.push(" <replies>");
539
+ for (const reply of comment.replies) {
540
+ const replyIsoDate = new Date(reply.createdAt).toISOString();
541
+ const roleAttr = reply.role ? ` role="${reply.role}"` : " role=\"agent\"";
542
+ const modelAttr = reply.model ? ` model="${reply.model}"` : "";
543
+ lines.push(` <reply id="${reply.id}" created-at="${replyIsoDate}"${roleAttr}${modelAttr}>`);
544
+ lines.push(` <![CDATA[${reply.body}]]>`);
545
+ lines.push(" </reply>");
546
+ }
547
+ lines.push(" </replies>");
548
+ }
549
+ lines.push(" </comment>");
550
+ }
551
+ lines.push(" </file>");
552
+ }
553
+ lines.push("</code-review-comments>");
554
+ return lines.join("\n");
555
+ }
556
+ //#endregion
557
+ //#region src/lib/plan-format.ts
558
+ /**
559
+ * Serialize a reviewed plan into the `<plan-review>` XML envelope handed back to
560
+ * a waiting agent. The diff-review counterpart is {@link formatComments}; this
561
+ * keeps the two handoffs structurally consistent so an agent learns one shape.
562
+ *
563
+ * Shared by the server's `/api/plan-review/await` handoff, the `diffing plan`
564
+ * CLI, and the MCP server so every channel emits byte-identical output.
565
+ */
566
+ function escapeAttr(value) {
567
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
568
+ }
569
+ /** Human-readable guidance the agent should act on, derived from the verdict. */
570
+ function decisionSummary(decision) {
571
+ switch (decision) {
572
+ case "approved": return "The reviewer APPROVED this plan. Proceed with implementation exactly as described, taking any inline comments into account.";
573
+ case "rejected": return "The reviewer REJECTED this plan. Do NOT proceed. Reconsider the approach based on the comments; only continue once a new plan is approved.";
574
+ case "changes-requested": return "The reviewer REQUESTED CHANGES. Revise the plan to address every open comment below, then resubmit the updated plan for re-review (do not start implementation yet).";
575
+ default: return "This plan has not been decided yet. Wait for the reviewer to approve, reject, or request changes.";
576
+ }
577
+ }
578
+ /**
579
+ * Resolve the line label for a plan comment: `plan` for whole-plan notes,
580
+ * `12-15` for a multi-line selection, otherwise the single line number.
581
+ */
582
+ function lineLabel(comment) {
583
+ if (comment.lineNumber === 0) return "plan";
584
+ if (comment.startLineNumber && comment.startLineNumber !== comment.lineNumber) return `${comment.startLineNumber}-${comment.lineNumber}`;
585
+ return `${comment.lineNumber}`;
586
+ }
587
+ function formatPlanReview(plan) {
588
+ const lines = [];
589
+ lines.push("<plan-review>");
590
+ lines.push(" <instructions>");
591
+ lines.push(" You are an AI coding assistant receiving a human review of a plan you submitted.");
592
+ lines.push(" - The \"decision\" attribute is the headline verdict: \"approved\", \"rejected\", \"changes-requested\", or \"pending\".");
593
+ lines.push(" - <decision-summary> tells you, in plain language, what to do next based on that verdict.");
594
+ lines.push(" - <decision-comment> is the reviewer's overall note (may be absent).");
595
+ lines.push(" - <plan-body> is the exact markdown of the plan being reviewed. Inline comments target lines within it.");
596
+ lines.push(" - Each <comment> targets a line (\"line=42\"), a range (\"line=12-15\"), or the whole plan (\"line=plan\").");
597
+ lines.push(" \"section\" names the nearest markdown heading for context. Only address comments with status=\"open\".");
598
+ lines.push(" - <context> contains the exact plan text the comment is anchored to.");
599
+ lines.push("");
600
+ lines.push(" HOW TO RESPOND:");
601
+ lines.push(" - If approved: proceed with the work.");
602
+ lines.push(" - If changes-requested: revise the plan and resubmit it for another review round.");
603
+ lines.push(" - If rejected: stop and rethink; do not implement.");
604
+ lines.push(" Prefer the diffing CLI or MCP (port-agnostic, no copy-paste):");
605
+ lines.push(" diffing plan reply <comment-id> --body \"...\" --model \"<your-model-name>\"");
606
+ lines.push(" diffing plan resolve <comment-id>");
607
+ lines.push(" diffing plan submit <revised-plan.md> --id <plan-id> # resubmit a new version for re-review");
608
+ lines.push(" (Or the equivalent MCP tools: reply_to_plan_comment, resolve_plan_comment, submit_plan.)");
609
+ lines.push(" </instructions>");
610
+ const decidedAttr = plan.decidedAt ? ` decided-at="${new Date(plan.decidedAt).toISOString()}"` : "";
611
+ lines.push(` <plan id="${escapeAttr(plan.id)}" title="${escapeAttr(plan.title)}" version="${plan.version}" decision="${plan.decision}"${decidedAttr}>`);
612
+ lines.push(` <decision-summary><![CDATA[${decisionSummary(plan.decision)}]]></decision-summary>`);
613
+ const trimmedDecisionComment = plan.decisionComment?.trim();
614
+ if (trimmedDecisionComment) lines.push(` <decision-comment><![CDATA[${trimmedDecisionComment}]]></decision-comment>`);
615
+ lines.push(` <plan-body><![CDATA[${plan.body}]]></plan-body>`);
616
+ const comments = plan.comments ?? [];
617
+ if (comments.length > 0) {
618
+ lines.push(" <comments>");
619
+ for (const comment of comments) {
620
+ const sectionAttr = comment.sectionTitle ? ` section="${escapeAttr(comment.sectionTitle)}"` : "";
621
+ const isoDate = new Date(comment.createdAt).toISOString();
622
+ lines.push(` <comment id="${escapeAttr(comment.id)}" line="${lineLabel(comment)}"${sectionAttr} status="${comment.status}" created-at="${isoDate}">`);
623
+ if (comment.lineNumber !== 0 && comment.lineContent) lines.push(` <context><![CDATA[${comment.lineContent}]]></context>`);
624
+ lines.push(` <body><![CDATA[${comment.body}]]></body>`);
625
+ if (comment.replies && comment.replies.length > 0) {
626
+ lines.push(" <replies>");
627
+ for (const reply of comment.replies) {
628
+ const replyIsoDate = new Date(reply.createdAt).toISOString();
629
+ const roleAttr = reply.role ? ` role="${reply.role}"` : " role=\"agent\"";
630
+ const modelAttr = reply.model ? ` model="${escapeAttr(reply.model)}"` : "";
631
+ lines.push(` <reply id="${escapeAttr(reply.id)}" created-at="${replyIsoDate}"${roleAttr}${modelAttr}>`);
632
+ lines.push(` <![CDATA[${reply.body}]]>`);
633
+ lines.push(" </reply>");
634
+ }
635
+ lines.push(" </replies>");
636
+ }
637
+ lines.push(" </comment>");
638
+ }
639
+ lines.push(" </comments>");
640
+ }
641
+ lines.push(" </plan>");
642
+ lines.push("</plan-review>");
643
+ return lines.join("\n");
644
+ }
645
+ /**
646
+ * Find the nearest markdown ATX heading at or above a 1-based line, so a comment
647
+ * can record which section of the plan it belongs to. Returns the heading text
648
+ * without the leading `#`s, or undefined when the line precedes any heading.
649
+ */
650
+ function sectionTitleForLine(body, lineNumber) {
651
+ if (lineNumber < 1) return void 0;
652
+ const lines = body.split("\n");
653
+ const end = Math.min(lineNumber, lines.length);
654
+ for (let i = end - 1; i >= 0; i--) {
655
+ const match = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(lines[i]);
656
+ if (match) return match[2].trim();
657
+ }
658
+ }
659
+ /**
660
+ * Snapshot the plan text spanned by a 1-based, inclusive [start, end] line
661
+ * range, used as the `lineContent` anchor when a comment is created without one
662
+ * (e.g. posted by an agent over the API).
663
+ */
664
+ function extractPlanLines(body, startLine, endLine) {
665
+ if (startLine < 1) return "";
666
+ const lines = body.split("\n");
667
+ const from = Math.max(1, startLine);
668
+ const to = Math.min(endLine, lines.length);
669
+ if (to < from) return "";
670
+ return lines.slice(from - 1, to).join("\n");
671
+ }
672
+ //#endregion
673
+ //#region src/lib/server-lock.ts
674
+ function lockPath(repoRoot) {
675
+ return join(getProjectStorageDir(repoRoot), "server.json");
676
+ }
677
+ function writeServerLock(lock) {
678
+ const path = lockPath(lock.repoRoot);
679
+ mkdirSync(join(path, ".."), { recursive: true });
680
+ writeFileSync(path, JSON.stringify(lock, null, 2), "utf-8");
681
+ }
682
+ function readServerLock(repoRoot) {
683
+ try {
684
+ const raw = readFileSync(lockPath(repoRoot), "utf-8");
685
+ const lock = JSON.parse(raw);
686
+ if (typeof lock.port !== "number" || typeof lock.pid !== "number") return null;
687
+ return lock;
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+ /**
693
+ * True if the process named by the lock is still alive and the lock belongs to
694
+ * this repo. `process.kill(pid, 0)` sends no signal — it just probes existence.
695
+ */
696
+ function isLockAlive(lock) {
697
+ try {
698
+ process.kill(lock.pid, 0);
699
+ } catch {
700
+ return false;
701
+ }
702
+ try {
703
+ return lock.repoRoot === getRepoRoot();
704
+ } catch {
705
+ return true;
706
+ }
707
+ }
708
+ function removeServerLock(repoRoot) {
709
+ try {
710
+ rmSync(lockPath(repoRoot), { force: true });
711
+ } catch {}
712
+ }
713
+ //#endregion
714
+ export { revertHunk as C, listRepoFiles as S, toSafeRelativePath as T, getRepoRoot as _, extractPlanLines as a, gitAddFile as b, formatComments as c, getFileContent as d, getGitDiffAsync as f, getRepoName as g, getProjectStorageDir as h, writeServerLock as i, getBranchName as l, getMergeStatus as m, readServerLock as n, formatPlanReview as o, getHunkHistory as p, removeServerLock as r, sectionTitleForLine as s, isLockAlive as t, getCustomGitDiffAsync as u, getTabSizeForFiles as v, isSafePath as w, isGitRepo as x, getUntrackedFilePathsAsync as y };