diffing 0.10.4 → 0.10.5
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/{cli-agent-BBHx1lLJ.mjs → cli-agent-BJiATn11.mjs} +10 -8
- package/dist/{cli-gh-B8HybRWZ.mjs → cli-gh-_bkDCxhh.mjs} +1 -1
- package/dist/cli.mjs +213 -12
- package/dist/client/assets/{angular-html-Bn7xcK-X.js → angular-html-CBcwKYDC.js} +1 -1
- package/dist/client/assets/{angular-ts-qK80FnOc.js → angular-ts-DRBYUaJA.js} +1 -1
- package/dist/client/assets/{apl-BhBHq9NH.js → apl-BM0X8jeL.js} +1 -1
- package/dist/client/assets/{arc-BbRWqKdi.js → arc-DTkmmKl3.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-DkAZjTe2.js → architectureDiagram-3BPJPVTR-Cw75ay7q.js} +1 -1
- package/dist/client/assets/{astro--A8NjYkS.js → astro-DKTNkoRZ.js} +1 -1
- package/dist/client/assets/{blade-DUJ_WZzW.js → blade-DaM7a9Zp.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-BJnX6alY.js → blockDiagram-GPEHLZMM-DAzINMr_.js} +1 -1
- package/dist/client/assets/{c-Ddzn0JTQ.js → c-B3C0Or4o.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-B_JFq-Tk.js → c4Diagram-AAUBKEIU-C7SsS28g.js} +1 -1
- package/dist/client/assets/channel-lsVHTuYR.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CCnCUy9H.js → chunk-2J33WTMH-DHOUGWx8.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-CJvhnVzd.js → chunk-3OPIFGDE-D8d2scLX.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-7gGFn4DW.js → chunk-4BX2VUAB-BSC-E5yg.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-CRirC3B0.js → chunk-55IACEB6-27pC5pKn.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-DSagYOhc.js → chunk-5ZQYHXKU-BM60x82y.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-2fqFvPdL.js → chunk-727SXJPM-CHCF96vE.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-8l-kDb0Q.js → chunk-AQP2D5EJ-BoVsvmTa.js} +1 -1
- package/dist/client/assets/{chunk-BSJP7CBP-BjTEoy7R.js → chunk-BSJP7CBP-Iip4uSio.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-CeHxe0Uf.js → chunk-CSCIHK7Q-DuIWPyay.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-X-OwPNTB.js → chunk-FMBD7UC4-Du6rtd2k.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-rYccR2Ju.js → chunk-KSCS5N6A-BBq105vI.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-8JMoad-S.js → chunk-L5ZTLDWV-0DmB5Qp8.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DOvCLema.js → chunk-LZXEDZCA-CoRsjUfr.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-CQQJmqC8.js → chunk-ND2GUHAM-tX8cKEls.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-DPwHQE2m.js → chunk-NZK2D7GU-BoJGgPic.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-Bvzv6QAN.js → chunk-O5CBEL6O-mlwCWs97.js} +1 -1
- package/dist/client/assets/chunk-QZHKN3VN-AHUiTM0z.js +1 -0
- package/dist/client/assets/chunk-WU5MYG2G-D41j4yxb.js +1 -0
- package/dist/client/assets/{chunk-XPW4576I-DY1teoyy.js → chunk-XPW4576I-B5efww8M.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-D1Eo3kly.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-bAMsbsfB.js +1 -0
- package/dist/client/assets/{cobol-EaVCzYAO.js → cobol-De9QR_Td.js} +1 -1
- package/dist/client/assets/{coffee-CbmPxJhz.js → coffee-Ct_GvuHP.js} +1 -1
- package/dist/client/assets/{cose-bilkent-S5V4N54A-zMLfkB6c.js → cose-bilkent-S5V4N54A-ATaJZGF7.js} +1 -1
- package/dist/client/assets/{cpp-1v8ov4EG.js → cpp-Dw52w9cj.js} +1 -1
- package/dist/client/assets/{crystal-B88NqWAb.js → crystal-Dx8ADdsd.js} +1 -1
- package/dist/client/assets/{css-QFeW3BCZ.js → css-CZ13XvDU.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-DxGKZBBq.js → dagre-BM42HDAG-aDltuYut.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-CiUL7Dov.js → diagram-2AECGRRQ-DGWnHVMJ.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-DCs97wXn.js → diagram-5GNKFQAL-Dd7eZBKe.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BsSJWXqr.js → diagram-KO2AKTUF-DWThXREf.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-LDuP4WAy.js → diagram-LMA3HP47-CXR4Bjtc.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-CKjIVoHx.js → diagram-OG6HWLK6-D4Cbf0nW.js} +1 -1
- package/dist/client/assets/{dist-BvtPW002.js → dist-CUDhrHNw.js} +1 -1
- package/dist/client/assets/{edge-5UREsVXQ.js → edge-CQCjdJKj.js} +1 -1
- package/dist/client/assets/{elixir-B_Yali6w.js → elixir-bkjwPazA.js} +1 -1
- package/dist/client/assets/{elm-STYz8ayI.js → elm-CCweX5Cq.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D7iBLlYp.js → erDiagram-TEJ5UH35-BmAyt7SU.js} +1 -1
- package/dist/client/assets/{erb-CHRx3Beu.js → erb-D-Z-KBLp.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Dr2J9T5z.js → flowDiagram-I6XJVG4X-C6nIFUVc.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-Bb1v2M1H.js → ganttDiagram-6RSMTGT7-DMlxThBf.js} +1 -1
- package/dist/client/assets/{git-rebase-CFmRpmLK.js → git-rebase-BdoFdenB.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DP9xmnTA.js → gitGraphDiagram-PVQCEYII-JaIFkAIT.js} +1 -1
- package/dist/client/assets/{glimmer-js-CdrEdtzi.js → glimmer-js-CZQrBJKq.js} +1 -1
- package/dist/client/assets/{glimmer-ts-CzrwZqCl.js → glimmer-ts-CBYW46D4.js} +1 -1
- package/dist/client/assets/{glsl-Cb4XetAc.js → glsl-wxixKkY_.js} +1 -1
- package/dist/client/assets/{graphql-mqi9K2_l.js → graphql-CqZ0Lo5a.js} +1 -1
- package/dist/client/assets/{hack-DaFJ7htO.js → hack-rqWymQRV.js} +1 -1
- package/dist/client/assets/{haml-Bw7q4nKg.js → haml-DFw_IoGM.js} +1 -1
- package/dist/client/assets/{handlebars-mj81MoXX.js → handlebars-3VmTVMYt.js} +1 -1
- package/dist/client/assets/{html-LHrj0-2t.js → html-C0Li76jB.js} +1 -1
- package/dist/client/assets/{html-derivative-L3_OYrFu.js → html-derivative-CO-RQW8y.js} +1 -1
- package/dist/client/assets/{http-DE0OFe2c.js → http-sPrZOeBY.js} +1 -1
- package/dist/client/assets/{hurl-CNbk7fqf.js → hurl-DUoPe4Lc.js} +1 -1
- package/dist/client/assets/{index-DHt5Hu--.js → index-BN_-di_r.js} +24 -24
- package/dist/client/assets/{infoDiagram-5YYISTIA-wxqSmRV0.js → infoDiagram-5YYISTIA-B6AZ5SCV.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CJa0qhXu.js → ishikawaDiagram-YF4QCWOH-CytMrpo8.js} +1 -1
- package/dist/client/assets/{java-DAwMoXxA.js → java-Cj0G6QRg.js} +1 -1
- package/dist/client/assets/{javascript-_a2prAz7.js → javascript-d5SUuyWD.js} +1 -1
- package/dist/client/assets/{jinja-JKFA3bu8.js → jinja-BTw9k0vO.js} +1 -1
- package/dist/client/assets/{jison-UbTjhW7k.js → jison-9xSVvR1q.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Bh5L3OBN.js → journeyDiagram-JHISSGLW-Bd4jZUYI.js} +1 -1
- package/dist/client/assets/{json-D74F0BDU.js → json-DjAt53ka.js} +1 -1
- package/dist/client/assets/{jsx-BsH2-gDN.js → jsx-BX5KmQ7d.js} +1 -1
- package/dist/client/assets/{julia-DyIO_Gy0.js → julia-CLLx1dHC.js} +1 -1
- package/dist/client/assets/{just-VQTFAZJr.js → just-C0v80bKe.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BB2voi9I.js → kanban-definition-UN3LZRKU-Ds4pXHJa.js} +1 -1
- package/dist/client/assets/{latex-D0ONGfIo.js → latex-DY-oQfwO.js} +1 -1
- package/dist/client/assets/{line-BkbHeSM4.js → line-BbSLfOl6.js} +1 -1
- package/dist/client/assets/{linear-BYUYqOy1.js → linear-BMIZJAAR.js} +1 -1
- package/dist/client/assets/{liquid-BnYpmfMl.js → liquid-DDHDgbZx.js} +1 -1
- package/dist/client/assets/{lua-WlA-m2lC.js → lua-CXymerKS.js} +1 -1
- package/dist/client/assets/{marko-Bd44681W.js → marko-DoPFsptl.js} +1 -1
- package/dist/client/assets/{mdc-DhosSoGv.js → mdc-BxSmTphd.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-Dq0EOOjQ.js → mermaid-parser.core-CcYSvN4q.js} +1 -1
- package/dist/client/assets/{mermaid.core-qcl7ght-.js → mermaid.core-COTNtqgQ.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-DrZ9di3F.js → mindmap-definition-RKZ34NQL-BTxraRzn.js} +1 -1
- package/dist/client/assets/{nginx-sYwNRk-F.js → nginx-Bma0jNOT.js} +1 -1
- package/dist/client/assets/{nim-DrXU2tn9.js → nim-DMBpjygU.js} +1 -1
- package/dist/client/assets/{perl-TEELuyd-.js → perl-CrnrV-AJ.js} +1 -1
- package/dist/client/assets/{php-Dy_Y1r8j.js → php-CDYxzX6t.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-Aev8QdJC.js → pieDiagram-4H26LBE5-DglRmzer.js} +1 -1
- package/dist/client/assets/{pug-vancrY0a.js → pug-DNewy3GU.js} +1 -1
- package/dist/client/assets/{qml-BA5z-zbf.js → qml-COjcnZ-W.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DhEDvxx7.js → quadrantDiagram-W4KKPZXB-BjSmyES7.js} +1 -1
- package/dist/client/assets/{r-BR0O4N-s.js → r-CrCaMxLS.js} +1 -1
- package/dist/client/assets/{razor-DXiNCb-5.js → razor-jeuOZIar.js} +1 -1
- package/dist/client/assets/{regexp-BhjJa9MI.js → regexp-DSHVUX08.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DPS5siTy.js → requirementDiagram-4Y6WPE33-B4f430jS.js} +1 -1
- package/dist/client/assets/{review-export-DABY3y7y.js → review-export-Cen1WEcX.js} +1 -1
- package/dist/client/assets/{rst-Bi3zId1d.js → rst-CIDgytzc.js} +1 -1
- package/dist/client/assets/{ruby-BURiJKuD.js → ruby-DtuseoNq.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CjZF2AQd.js → sankeyDiagram-5OEKKPKP-TQPPlxc6.js} +1 -1
- package/dist/client/assets/{sas-BO_W1LuL.js → sas-C72Theia.js} +1 -1
- package/dist/client/assets/{scss-BPUTinXb.js → scss-ClECtwRk.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-0rsTmBLB.js → sequenceDiagram-3UESZ5HK-D1tx07Wg.js} +1 -1
- package/dist/client/assets/{shellscript-B4rfo4qg.js → shellscript-BCwBsi2k.js} +1 -1
- package/dist/client/assets/{shellsession-DLX-O8m7.js → shellsession-BrOddEl5.js} +1 -1
- package/dist/client/assets/{soy-BEUiwSVD.js → soy-Dcs0GWFW.js} +1 -1
- package/dist/client/assets/{sql-CwSA8v_H.js → sql-BAjA7Gv7.js} +1 -1
- package/dist/client/assets/{src-Cn22-GjE.js → src-CYYhkqTK.js} +1 -1
- package/dist/client/assets/{stata-XfdBiAnW.js → stata-19ngQtzR.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-BVvCsgjA.js → stateDiagram-AJRCARHV-BZRCEzb5.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Da-mGMA2.js +1 -0
- package/dist/client/assets/{surrealql-COaehKkS.js → surrealql-Ce2TBRjE.js} +1 -1
- package/dist/client/assets/{svelte-B0kTIq0J.js → svelte-DQ_1eGZz.js} +1 -1
- package/dist/client/assets/{templ-DGxWfDf-.js → templ-Cw2DDxWF.js} +1 -1
- package/dist/client/assets/{tex-C9X3gjZ2.js → tex-myDSO2eP.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-B535P7_e.js → timeline-definition-PNZ67QCA-CP0OZhBT.js} +1 -1
- package/dist/client/assets/{ts-tags-By9ETsTC.js → ts-tags-rh39ZwVs.js} +1 -1
- package/dist/client/assets/{tsx-CvVX506n.js → tsx-B0-26jrU.js} +1 -1
- package/dist/client/assets/{twig-CjLWv7eu.js → twig-CgasHzPb.js} +1 -1
- package/dist/client/assets/{typescript-BEq3u3Bc.js → typescript-Dq3T0c-m.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CTV9H1N2.js → vennDiagram-CIIHVFJN-DHaKHj0j.js} +1 -1
- package/dist/client/assets/{vue-DYazforU.js → vue-CzFVh92-.js} +1 -1
- package/dist/client/assets/{vue-html-92z_Aqn0.js → vue-html-Bv2uFkTH.js} +1 -1
- package/dist/client/assets/{vue-vine-VY7raYSI.js → vue-vine-DzieBk9Y.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BzJaykc6.js → wardleyDiagram-YWT4CUSO-Dky7jxlZ.js} +1 -1
- package/dist/client/assets/{xml-hd7OQ798.js → xml-wcdHRHN8.js} +1 -1
- package/dist/client/assets/{xsl-MkuT76g1.js → xsl-CXrwsLG9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-D36D8smx.js → xychartDiagram-2RQKCTM6-BQ2hG60h.js} +1 -1
- package/dist/client/assets/{yaml-D6zRzOR2.js → yaml-lO6lK6Xe.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/{completions-Csvfqctx.mjs → completions-3UF8uzhx.mjs} +2 -0
- package/dist/{doctor-B3Nrydc0.mjs → doctor-BGYZNIYP.mjs} +2 -2
- package/dist/{github-Cf5blBty.mjs → github-CEjtGd41.mjs} +47 -5
- package/dist/github-DKT0lJXv.mjs +2 -0
- package/dist/github-attachments-Cf17YWp4.mjs +351 -0
- package/dist/handoff-DPoIyeWn.mjs +11 -0
- package/dist/{mcp-DwnYjFS0.mjs → mcp-Dq7J5QOP.mjs} +31 -18
- package/dist/{server-CZeMCHEa.mjs → server-C9i4V36o.mjs} +38 -10
- package/dist/{server-lock-uqSdDYeR.mjs → server-lock-Dd_ss_Il.mjs} +1 -1
- package/package.json +1 -1
- package/dist/client/assets/channel-BcJKoISM.js +0 -1
- package/dist/client/assets/chunk-QZHKN3VN-90aaHYyK.js +0 -1
- package/dist/client/assets/chunk-WU5MYG2G-CfaWSqjJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CsDELGUF.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-R5SygY4h.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Bo6SeNmU.js +0 -1
- package/dist/github-DWgPIcKC.mjs +0 -2
- /package/dist/{apply-suggestion-BAuyCYk6.mjs → apply-suggestion-DNNsC5gx.mjs} +0 -0
- /package/dist/{diff-fingerprint-CWa-soYD.mjs → diff-fingerprint-C8YO3TTE.mjs} +0 -0
- /package/dist/{review-export-DjxKBeIZ.mjs → review-export-BBDWvftT.mjs} +0 -0
- /package/dist/{update-check-fe0tR8tx.mjs → update-check-DUuHmPeZ.mjs} +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { d as getProjectStorageDir, w as toSafeRelativePath } from "./git-Czo96ymM.mjs";
|
|
2
|
+
import { _ as isGithubDotCom, g as githubApiBase, m as ghHostnameArgs, o as detectGhCli, s as execWithInput, x as readGithubToken } from "./github-CEjtGd41.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { extname, join } from "node:path";
|
|
5
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
|
+
//#region src/lib/github-attachments.ts
|
|
7
|
+
/**
|
|
8
|
+
* Publish-time rewrite of local `/api/attachments/…` markdown into
|
|
9
|
+
* repo-ACL-bound GitHub raw blob URLs (hidden orphan ref via Git Data API).
|
|
10
|
+
*
|
|
11
|
+
* Private repos / GHE: URLs inherit repository visibility. Never uses
|
|
12
|
+
* release assets (always public) or cookie-based user-attachments upload.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
15
|
+
const ALLOWED_EXT = new Set([
|
|
16
|
+
".png",
|
|
17
|
+
".jpg",
|
|
18
|
+
".jpeg",
|
|
19
|
+
".gif",
|
|
20
|
+
".webp",
|
|
21
|
+
".svg"
|
|
22
|
+
]);
|
|
23
|
+
/** Markdown image/link targets that point at local diffing attachments. */
|
|
24
|
+
const LOCAL_ATTACHMENT_RE = /(?:!?\[[^\]]*\]\()(\/api\/attachments\/([^)\s]+))\)|(?:src=["'])(\/api\/attachments\/([^"'\s]+))["']/gi;
|
|
25
|
+
/** Public web origin for markdown image URLs (not the REST API host). */
|
|
26
|
+
function githubWebOrigin(host) {
|
|
27
|
+
if (isGithubDotCom(host)) return "https://github.com";
|
|
28
|
+
return (host.includes("://") ? host : `https://${host}`).replace(/\/$/, "");
|
|
29
|
+
}
|
|
30
|
+
/** Build a private-repo-safe raw content URL for a blob at a commit. */
|
|
31
|
+
function githubRawBlobUrl(resolved, commitSha, treePath) {
|
|
32
|
+
const origin = githubWebOrigin(resolved.host);
|
|
33
|
+
const segments = treePath.split("/").map((s) => encodeURIComponent(s)).join("/");
|
|
34
|
+
return `${origin}/${resolved.owner}/${resolved.repo}/raw/${commitSha}/${segments}`;
|
|
35
|
+
}
|
|
36
|
+
function attachmentRefName(pullNumber) {
|
|
37
|
+
return `refs/diffing/attachments/pr-${pullNumber}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract safe local attachment filenames from markdown. Rejects traversal,
|
|
41
|
+
* absolute paths, and empty names.
|
|
42
|
+
*/
|
|
43
|
+
function extractLocalAttachmentRefs(markdown) {
|
|
44
|
+
const found = /* @__PURE__ */ new Map();
|
|
45
|
+
LOCAL_ATTACHMENT_RE.lastIndex = 0;
|
|
46
|
+
let match;
|
|
47
|
+
while ((match = LOCAL_ATTACHMENT_RE.exec(markdown)) !== null) {
|
|
48
|
+
const url = match[1] ?? match[3];
|
|
49
|
+
const filename = match[2] ?? match[4];
|
|
50
|
+
if (!url || !filename) continue;
|
|
51
|
+
const safe = sanitizeAttachmentFilename(filename);
|
|
52
|
+
if (!safe) continue;
|
|
53
|
+
found.set(url, {
|
|
54
|
+
url,
|
|
55
|
+
filename: safe
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return [...found.values()];
|
|
59
|
+
}
|
|
60
|
+
/** Returns basename if safe; null if traversal / bad extension / weird chars. */
|
|
61
|
+
function sanitizeAttachmentFilename(filename) {
|
|
62
|
+
let decoded = filename;
|
|
63
|
+
try {
|
|
64
|
+
decoded = decodeURIComponent(filename);
|
|
65
|
+
} catch {}
|
|
66
|
+
if (!decoded || decoded.includes("\0") || decoded.includes("/") || decoded.includes("\\")) return null;
|
|
67
|
+
if (decoded.includes("..") || decoded.startsWith(".") || decoded !== decoded.trim()) return null;
|
|
68
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(decoded)) return null;
|
|
69
|
+
const ext = extname(decoded).toLowerCase();
|
|
70
|
+
if (!ALLOWED_EXT.has(ext)) return null;
|
|
71
|
+
return decoded;
|
|
72
|
+
}
|
|
73
|
+
function rewriteAttachmentUrls(markdown, urlMap) {
|
|
74
|
+
if (Object.keys(urlMap).length === 0) return markdown;
|
|
75
|
+
let out = markdown;
|
|
76
|
+
for (const [local, remote] of Object.entries(urlMap)) out = out.split(local).join(remote);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function attachmentsDir(custom) {
|
|
80
|
+
return custom ?? join(getProjectStorageDir(), "attachments");
|
|
81
|
+
}
|
|
82
|
+
async function loadAttachmentFile(dir, filename) {
|
|
83
|
+
const safeRel = toSafeRelativePath(filename, dir);
|
|
84
|
+
if (!safeRel || safeRel.includes("..") || safeRel.includes("/") || safeRel.includes("\\")) return {
|
|
85
|
+
bytes: Buffer.alloc(0),
|
|
86
|
+
error: `Refusing unsafe attachment path: ${filename}`
|
|
87
|
+
};
|
|
88
|
+
const absolute = join(dir, safeRel);
|
|
89
|
+
try {
|
|
90
|
+
const st = await stat(absolute);
|
|
91
|
+
if (!st.isFile()) return {
|
|
92
|
+
bytes: Buffer.alloc(0),
|
|
93
|
+
error: `Attachment is not a file: ${filename}`
|
|
94
|
+
};
|
|
95
|
+
if (st.size > 10485760) return {
|
|
96
|
+
bytes: Buffer.alloc(0),
|
|
97
|
+
error: `Attachment ${filename} exceeds ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MB limit`
|
|
98
|
+
};
|
|
99
|
+
return { bytes: await readFile(absolute) };
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
bytes: Buffer.alloc(0),
|
|
103
|
+
error: `Missing local attachment: ${filename}`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function planAttachmentUploads(markdownBodies, opts) {
|
|
108
|
+
const refs = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const body of markdownBodies) for (const ref of extractLocalAttachmentRefs(body)) refs.set(ref.url, ref);
|
|
110
|
+
if (refs.size === 0) return { plans: [] };
|
|
111
|
+
const dir = attachmentsDir(opts?.attachmentsDir);
|
|
112
|
+
const plans = [];
|
|
113
|
+
const hashToTreePath = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const ref of refs.values()) {
|
|
115
|
+
const loaded = await loadAttachmentFile(dir, ref.filename);
|
|
116
|
+
if (loaded.error) return {
|
|
117
|
+
plans: [],
|
|
118
|
+
error: loaded.error
|
|
119
|
+
};
|
|
120
|
+
const contentHash = createHash("sha256").update(loaded.bytes).digest("hex");
|
|
121
|
+
let treePath = hashToTreePath.get(contentHash);
|
|
122
|
+
if (!treePath) {
|
|
123
|
+
const ext = extname(ref.filename).toLowerCase() || ".bin";
|
|
124
|
+
treePath = `${contentHash.slice(0, 16)}${ext}`;
|
|
125
|
+
hashToTreePath.set(contentHash, treePath);
|
|
126
|
+
}
|
|
127
|
+
plans.push({
|
|
128
|
+
filename: ref.filename,
|
|
129
|
+
localUrl: ref.url,
|
|
130
|
+
bytes: loaded.bytes.length,
|
|
131
|
+
contentHash,
|
|
132
|
+
treePath
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return { plans };
|
|
136
|
+
}
|
|
137
|
+
function contentsWriteHint(message) {
|
|
138
|
+
const lower = message.toLowerCase();
|
|
139
|
+
if (lower.includes("resource not accessible") || lower.includes("not found") || lower.includes("403") || lower.includes("forbidden") || lower.includes("permission") || lower.includes("scopes")) return `${message} — Image upload needs contents:write (re-auth \`gh\` with the repo scope, or use a token with Contents: Write).`;
|
|
140
|
+
return message;
|
|
141
|
+
}
|
|
142
|
+
async function createGhApiCaller(resolved) {
|
|
143
|
+
const gh = await detectGhCli();
|
|
144
|
+
if (gh.available && gh.authenticated) {
|
|
145
|
+
const caller = async (method, path, body) => {
|
|
146
|
+
const args = [
|
|
147
|
+
"api",
|
|
148
|
+
...ghHostnameArgs(resolved),
|
|
149
|
+
"--method",
|
|
150
|
+
method,
|
|
151
|
+
path,
|
|
152
|
+
"-H",
|
|
153
|
+
"Accept: application/vnd.github+json"
|
|
154
|
+
];
|
|
155
|
+
try {
|
|
156
|
+
let stdout;
|
|
157
|
+
if (body !== void 0) {
|
|
158
|
+
args.push("--input", "-");
|
|
159
|
+
({stdout} = await execWithInput("gh", args, JSON.stringify(body)));
|
|
160
|
+
} else ({stdout} = await execWithInput("gh", args, ""));
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
status: 200,
|
|
164
|
+
json: stdout.trim() ? JSON.parse(stdout) : null,
|
|
165
|
+
text: stdout
|
|
166
|
+
};
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const stdout = String(err?.stdout || "");
|
|
169
|
+
const stderr = String(err?.stderr || err?.message || "");
|
|
170
|
+
let json = null;
|
|
171
|
+
try {
|
|
172
|
+
json = stdout ? JSON.parse(stdout) : null;
|
|
173
|
+
} catch {}
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
status: typeof err?.code === "number" ? err.code : 500,
|
|
177
|
+
json,
|
|
178
|
+
text: json?.message || stderr || stdout || "gh api failed"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
caller,
|
|
184
|
+
authSource: "gh"
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const token = readGithubToken();
|
|
188
|
+
if (!token) return { error: "Cannot upload images: no `gh` CLI on $PATH (or not authenticated) and no $GITHUB_TOKEN. Run `gh auth login` or set $GITHUB_TOKEN." };
|
|
189
|
+
const base = githubApiBase(resolved.host);
|
|
190
|
+
const caller = async (method, path, body) => {
|
|
191
|
+
const res = await fetch(`${base}/${path.replace(/^\//, "")}`, {
|
|
192
|
+
method,
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${token}`,
|
|
195
|
+
Accept: "application/vnd.github+json",
|
|
196
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"User-Agent": "diffing-cli"
|
|
199
|
+
},
|
|
200
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
201
|
+
signal: AbortSignal.timeout(45e3)
|
|
202
|
+
});
|
|
203
|
+
const text = await res.text().catch(() => "");
|
|
204
|
+
let json = null;
|
|
205
|
+
try {
|
|
206
|
+
json = text ? JSON.parse(text) : null;
|
|
207
|
+
} catch {}
|
|
208
|
+
return {
|
|
209
|
+
ok: res.ok,
|
|
210
|
+
status: res.status,
|
|
211
|
+
json,
|
|
212
|
+
text: json?.message || text
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
caller,
|
|
217
|
+
authSource: "token"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function readFileBytes(dir, filename) {
|
|
221
|
+
return readFile(join(dir, filename));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Upload unique attachment blobs to `refs/diffing/attachments/pr-<N>` and
|
|
225
|
+
* rewrite every local `/api/attachments/…` URL in the given bodies.
|
|
226
|
+
*
|
|
227
|
+
* On `dryRun: true`, builds placeholder URLs (`…/raw/<pending>/…`) without
|
|
228
|
+
* calling GitHub.
|
|
229
|
+
*/
|
|
230
|
+
async function rewriteLocalAttachmentsInBodies(resolved, bodies, opts) {
|
|
231
|
+
const dryRun = opts?.dryRun === true;
|
|
232
|
+
const planned = await planAttachmentUploads(bodies, { attachmentsDir: opts?.attachmentsDir });
|
|
233
|
+
if (planned.error) return {
|
|
234
|
+
bodies,
|
|
235
|
+
urlMap: {},
|
|
236
|
+
uploaded: 0,
|
|
237
|
+
dryRun,
|
|
238
|
+
error: planned.error
|
|
239
|
+
};
|
|
240
|
+
if (planned.plans.length === 0) return {
|
|
241
|
+
bodies,
|
|
242
|
+
urlMap: {},
|
|
243
|
+
uploaded: 0,
|
|
244
|
+
dryRun
|
|
245
|
+
};
|
|
246
|
+
if (dryRun) {
|
|
247
|
+
const urlMap = {};
|
|
248
|
+
for (const p of planned.plans) urlMap[p.localUrl] = githubRawBlobUrl(resolved, "<pending>", p.treePath);
|
|
249
|
+
return {
|
|
250
|
+
bodies: bodies.map((b) => rewriteAttachmentUrls(b, urlMap)),
|
|
251
|
+
urlMap,
|
|
252
|
+
uploaded: 0,
|
|
253
|
+
dryRun: true
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const api = await createGhApiCaller(resolved);
|
|
257
|
+
if ("error" in api) return {
|
|
258
|
+
bodies,
|
|
259
|
+
urlMap: {},
|
|
260
|
+
uploaded: 0,
|
|
261
|
+
dryRun,
|
|
262
|
+
error: api.error
|
|
263
|
+
};
|
|
264
|
+
const { caller } = api;
|
|
265
|
+
const repoPath = `repos/${resolved.owner}/${resolved.repo}`;
|
|
266
|
+
const dir = attachmentsDir(opts?.attachmentsDir);
|
|
267
|
+
const hashToBlobSha = /* @__PURE__ */ new Map();
|
|
268
|
+
const uniqueByHash = /* @__PURE__ */ new Map();
|
|
269
|
+
for (const p of planned.plans) if (!uniqueByHash.has(p.contentHash)) uniqueByHash.set(p.contentHash, p);
|
|
270
|
+
for (const p of uniqueByHash.values()) {
|
|
271
|
+
const bytes = await readFileBytes(dir, p.filename);
|
|
272
|
+
const res = await caller("POST", `${repoPath}/git/blobs`, {
|
|
273
|
+
content: bytes.toString("base64"),
|
|
274
|
+
encoding: "base64"
|
|
275
|
+
});
|
|
276
|
+
if (!res.ok || !res.json?.sha) return {
|
|
277
|
+
bodies,
|
|
278
|
+
urlMap: {},
|
|
279
|
+
uploaded: 0,
|
|
280
|
+
dryRun,
|
|
281
|
+
error: contentsWriteHint(res.text || `Failed to create blob for ${p.filename}`)
|
|
282
|
+
};
|
|
283
|
+
hashToBlobSha.set(p.contentHash, res.json.sha);
|
|
284
|
+
}
|
|
285
|
+
const tree = [...uniqueByHash.values()].map((p) => ({
|
|
286
|
+
path: p.treePath,
|
|
287
|
+
mode: "100644",
|
|
288
|
+
type: "blob",
|
|
289
|
+
sha: hashToBlobSha.get(p.contentHash)
|
|
290
|
+
}));
|
|
291
|
+
const treeRes = await caller("POST", `${repoPath}/git/trees`, { tree });
|
|
292
|
+
if (!treeRes.ok || !treeRes.json?.sha) return {
|
|
293
|
+
bodies,
|
|
294
|
+
urlMap: {},
|
|
295
|
+
uploaded: 0,
|
|
296
|
+
dryRun,
|
|
297
|
+
error: contentsWriteHint(treeRes.text || "Failed to create git tree for attachments")
|
|
298
|
+
};
|
|
299
|
+
const refName = attachmentRefName(resolved.pullNumber);
|
|
300
|
+
const refSuffix = refName.replace(/^refs\//, "");
|
|
301
|
+
const existing = await caller("GET", `${repoPath}/git/ref/${refSuffix}`);
|
|
302
|
+
const parents = existing.ok && existing.json?.object?.sha ? [existing.json.object.sha] : [];
|
|
303
|
+
const commitRes = await caller("POST", `${repoPath}/git/commits`, {
|
|
304
|
+
message: `diffing: attach review images for PR #${resolved.pullNumber}`,
|
|
305
|
+
tree: treeRes.json.sha,
|
|
306
|
+
parents
|
|
307
|
+
});
|
|
308
|
+
if (!commitRes.ok || !commitRes.json?.sha) return {
|
|
309
|
+
bodies,
|
|
310
|
+
urlMap: {},
|
|
311
|
+
uploaded: 0,
|
|
312
|
+
dryRun,
|
|
313
|
+
error: contentsWriteHint(commitRes.text || "Failed to create attachment commit")
|
|
314
|
+
};
|
|
315
|
+
const commitSha = commitRes.json.sha;
|
|
316
|
+
if (parents.length === 0) {
|
|
317
|
+
const createRef = await caller("POST", `${repoPath}/git/refs`, {
|
|
318
|
+
ref: refName,
|
|
319
|
+
sha: commitSha
|
|
320
|
+
});
|
|
321
|
+
if (!createRef.ok) return {
|
|
322
|
+
bodies,
|
|
323
|
+
urlMap: {},
|
|
324
|
+
uploaded: 0,
|
|
325
|
+
dryRun,
|
|
326
|
+
error: contentsWriteHint(createRef.text || `Failed to create ${refName}`)
|
|
327
|
+
};
|
|
328
|
+
} else {
|
|
329
|
+
const updateRef = await caller("PATCH", `${repoPath}/git/refs/${refSuffix}`, {
|
|
330
|
+
sha: commitSha,
|
|
331
|
+
force: false
|
|
332
|
+
});
|
|
333
|
+
if (!updateRef.ok) return {
|
|
334
|
+
bodies,
|
|
335
|
+
urlMap: {},
|
|
336
|
+
uploaded: 0,
|
|
337
|
+
dryRun,
|
|
338
|
+
error: contentsWriteHint(updateRef.text || `Failed to update ${refName}`)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const urlMap = {};
|
|
342
|
+
for (const p of planned.plans) urlMap[p.localUrl] = githubRawBlobUrl(resolved, commitSha, p.treePath);
|
|
343
|
+
return {
|
|
344
|
+
bodies: bodies.map((b) => rewriteAttachmentUrls(b, urlMap)),
|
|
345
|
+
urlMap,
|
|
346
|
+
uploaded: uniqueByHash.size,
|
|
347
|
+
dryRun: false
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
export { rewriteLocalAttachmentsInBodies };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/lib/handoff.ts
|
|
2
|
+
const PLAN_SUBMIT_NEXT_ACTION = "Share the plan review URL with the human and end your turn (async handoff). Call await_plan_review only when the human is reviewing now or explicitly asked you to wait.";
|
|
3
|
+
const AWAIT_REVIEW_TIMEOUT_NEXT_ACTION = "Wait budget elapsed — expected, not a failure. Park: tell the human the review UI is waiting and end your turn. Call await_review again only if they asked you to keep waiting (at most once more unless they repeat that ask). Do not retry in a silent loop. When they say the review is ready, call await_review once (or list_comments with openOnly) to resume; a prior Send-to-agent handoff is replayed.";
|
|
4
|
+
const AWAIT_PLAN_TIMEOUT_NEXT_ACTION = "Wait budget elapsed — expected, not a failure. Park: tell the human the plan URL is waiting for Submit review and end your turn. Call await_plan_review again only if they asked you to keep waiting (at most once more unless they repeat that ask). Do not retry in a silent loop. When they say a verdict is ready, call await_plan_review once (or get_plan / list_plans) to resume; a prior verdict is replayed.";
|
|
5
|
+
const CLI_AWAIT_REVIEW_TIMEOUT_HINT = "Wait budget elapsed (expected). Prefer parking until the human is ready; re-run `diffing await-review` only if they asked you to keep waiting.";
|
|
6
|
+
const CLI_AWAIT_PLAN_TIMEOUT_HINT = "Wait budget elapsed (expected). Prefer parking until the human is ready; re-run `diffing plan await` only if they asked you to keep waiting.";
|
|
7
|
+
const CLI_PLAN_SUBMIT_PARK_HINT = "Async handoff: share the URL above. Use --wait / `diffing plan await` only when the human is reviewing now or asked you to block.";
|
|
8
|
+
/** Compact MCP / skill preamble for wait tools. */
|
|
9
|
+
const AWAIT_TOOL_DESCRIPTION_SUFFIX = "Timeout means the wait budget elapsed (expected). Park and end the turn unless the human asked you to keep waiting; never silent-loop. Progress notifications are sent between long polls.";
|
|
10
|
+
//#endregion
|
|
11
|
+
export { CLI_AWAIT_REVIEW_TIMEOUT_HINT as a, CLI_AWAIT_PLAN_TIMEOUT_HINT as i, AWAIT_REVIEW_TIMEOUT_NEXT_ACTION as n, CLI_PLAN_SUBMIT_PARK_HINT as o, AWAIT_TOOL_DESCRIPTION_SUFFIX as r, PLAN_SUBMIT_NEXT_ACTION as s, AWAIT_PLAN_TIMEOUT_NEXT_ACTION as t };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { i as buildGitDiffArgs, o as parseDiffOptions, t as startServer } from "./server-
|
|
1
|
+
import { i as buildGitDiffArgs, o as parseDiffOptions, t as startServer } from "./server-C9i4V36o.mjs";
|
|
2
2
|
import { t as formatComments } from "./comment-format-B5AcGcR-.mjs";
|
|
3
3
|
import { n as formatPlanReview } from "./plan-format-BQyx--mp.mjs";
|
|
4
|
-
import { a as removeServerLock, i as readServerLock, n as diffScopeKey, o as removeServerLockIfOwned, r as isLockAlive, s as writeServerLock, t as acquireServerStartupLease } from "./server-lock-
|
|
4
|
+
import { a as removeServerLock, i as readServerLock, n as diffScopeKey, o as removeServerLockIfOwned, r as isLockAlive, s as writeServerLock, t as acquireServerStartupLease } from "./server-lock-Dd_ss_Il.mjs";
|
|
5
|
+
import { n as AWAIT_REVIEW_TIMEOUT_NEXT_ACTION, r as AWAIT_TOOL_DESCRIPTION_SUFFIX, s as PLAN_SUBMIT_NEXT_ACTION, t as AWAIT_PLAN_TIMEOUT_NEXT_ACTION } from "./handoff-DPoIyeWn.mjs";
|
|
5
6
|
import { execFileSync } from "node:child_process";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -345,7 +346,7 @@ function createMcpServer(options) {
|
|
|
345
346
|
const ownerId = options.ownerId ?? randomUUID();
|
|
346
347
|
const defaultClientDir = existsSync(resolve(moduleDirectory, "client")) ? resolve(moduleDirectory, "client") : resolve(repoRoot, "dist/client");
|
|
347
348
|
const clientDir = options.clientDir ?? defaultClientDir;
|
|
348
|
-
const instructions = `diffing is bound to ${repoRoot} for this connection. First call review_session_status. If no web session is running, call start_review_session; it is safe to retry and never replaces a user session. For code review, prefer diff_summary then paged diff_files/diff_hunks/diff_slice over get_diff; create_comment as needed
|
|
349
|
+
const instructions = `diffing is bound to ${repoRoot} for this connection. First call review_session_status. If no web session is running, call start_review_session; it is safe to retry and never replaces a user session. For code review, prefer diff_summary then paged diff_files/diff_hunks/diff_slice over get_diff; create_comment as needed. Handoff: default is async — share the UI URL and end your turn; use await_review only when the human is reviewing now or asked you to wait. For a GitHub PR session, call gh_overview and bounded diff tools; use gh_list_threads/reviews for discussion, and mutate GitHub only with explicit user authorization. For plan review, call submit_plan, share the plan URL, and park unless asked to wait; then await_plan_review when sync waiting. A changes-requested verdict means revise and resubmit the same planId; rejected means stop; approved means proceed. Wait tools return released or timeout with disposition/nextAction; timeout means park (do not silent-loop). MCP connects only to the loopback diffing server; explicitly authorized GitHub tools may make the server call GitHub.`;
|
|
349
350
|
const server = new McpServer({
|
|
350
351
|
name: "diffing",
|
|
351
352
|
version: MCP_VERSION
|
|
@@ -915,10 +916,11 @@ function createMcpServer(options) {
|
|
|
915
916
|
});
|
|
916
917
|
server.registerTool("await_review", {
|
|
917
918
|
title: "Wait for code review handoff",
|
|
918
|
-
description: "
|
|
919
|
-
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(
|
|
919
|
+
description: "Sync wait for the human to click Send to agent. Prefer async handoff (share the UI URL and end the turn) unless they are reviewing now or asked you to wait. Returns status=released or status=timeout; timeout includes disposition=park. " + AWAIT_TOOL_DESCRIPTION_SUFFIX,
|
|
920
|
+
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
|
|
920
921
|
outputSchema: {
|
|
921
922
|
status: z.enum(["released", "timeout"]),
|
|
923
|
+
disposition: z.enum(["park"]).optional(),
|
|
922
924
|
mode: z.enum(["standard", "comment-only"]),
|
|
923
925
|
round: z.number(),
|
|
924
926
|
openCount: z.number().optional(),
|
|
@@ -978,9 +980,10 @@ function createMcpServer(options) {
|
|
|
978
980
|
}
|
|
979
981
|
const structured = {
|
|
980
982
|
status: "timeout",
|
|
983
|
+
disposition: "park",
|
|
981
984
|
mode: "standard",
|
|
982
985
|
round: sinceRound,
|
|
983
|
-
nextAction:
|
|
986
|
+
nextAction: AWAIT_REVIEW_TIMEOUT_NEXT_ACTION
|
|
984
987
|
};
|
|
985
988
|
return textResult(structured.nextAction, structured);
|
|
986
989
|
});
|
|
@@ -1226,7 +1229,7 @@ function createMcpServer(options) {
|
|
|
1226
1229
|
});
|
|
1227
1230
|
server.registerTool("submit_plan", {
|
|
1228
1231
|
title: "Submit or resubmit a plan",
|
|
1229
|
-
description: "Submit Markdown for human plan review. To revise after changes-requested, pass the same planId so a new version is created.",
|
|
1232
|
+
description: "Submit Markdown for human plan review. Default handoff is async: return the URL and park. Call await_plan_review only when the human is reviewing now or asked you to wait. To revise after changes-requested, pass the same planId so a new version is created.",
|
|
1230
1233
|
inputSchema: {
|
|
1231
1234
|
title: z.string().optional().describe("Human-readable plan title."),
|
|
1232
1235
|
body: z.string().min(1).describe("Complete Markdown plan body."),
|
|
@@ -1238,7 +1241,8 @@ function createMcpServer(options) {
|
|
|
1238
1241
|
status: z.literal("submitted"),
|
|
1239
1242
|
planId: z.string(),
|
|
1240
1243
|
version: z.number(),
|
|
1241
|
-
url: z.string()
|
|
1244
|
+
url: z.string(),
|
|
1245
|
+
nextAction: z.string()
|
|
1242
1246
|
},
|
|
1243
1247
|
annotations: MUTATING
|
|
1244
1248
|
}, async ({ title, body, source, model, planId }) => {
|
|
@@ -1257,19 +1261,21 @@ function createMcpServer(options) {
|
|
|
1257
1261
|
})
|
|
1258
1262
|
});
|
|
1259
1263
|
const url = `${base}/plan/${plan.id}`;
|
|
1260
|
-
return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}.
|
|
1264
|
+
return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. ${PLAN_SUBMIT_NEXT_ACTION}`, {
|
|
1261
1265
|
status: "submitted",
|
|
1262
1266
|
planId: plan.id,
|
|
1263
1267
|
version: plan.version,
|
|
1264
|
-
url
|
|
1268
|
+
url,
|
|
1269
|
+
nextAction: PLAN_SUBMIT_NEXT_ACTION
|
|
1265
1270
|
});
|
|
1266
1271
|
});
|
|
1267
1272
|
server.registerTool("await_plan_review", {
|
|
1268
1273
|
title: "Wait for plan review verdict",
|
|
1269
|
-
description: "
|
|
1270
|
-
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(
|
|
1274
|
+
description: "Sync wait for a plan verdict (Submit review). Prefer async handoff after submit_plan unless the human is reviewing now or asked you to wait. Returns status=released or status=timeout; timeout includes disposition=park. " + AWAIT_TOOL_DESCRIPTION_SUFFIX,
|
|
1275
|
+
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
|
|
1271
1276
|
outputSchema: {
|
|
1272
1277
|
status: z.enum(["released", "timeout"]),
|
|
1278
|
+
disposition: z.enum(["park"]).optional(),
|
|
1273
1279
|
mode: z.enum(["standard", "comment-only"]),
|
|
1274
1280
|
round: z.number(),
|
|
1275
1281
|
planId: z.string().optional(),
|
|
@@ -1335,9 +1341,10 @@ function createMcpServer(options) {
|
|
|
1335
1341
|
}
|
|
1336
1342
|
const structured = {
|
|
1337
1343
|
status: "timeout",
|
|
1344
|
+
disposition: "park",
|
|
1338
1345
|
mode: "standard",
|
|
1339
1346
|
round: sinceRound,
|
|
1340
|
-
nextAction:
|
|
1347
|
+
nextAction: AWAIT_PLAN_TIMEOUT_NEXT_ACTION
|
|
1341
1348
|
};
|
|
1342
1349
|
return textResult(structured.nextAction, structured);
|
|
1343
1350
|
});
|
|
@@ -1471,18 +1478,24 @@ function createMcpServer(options) {
|
|
|
1471
1478
|
|
|
1472
1479
|
This MCP connection is immutably bound to \`${repoRoot}\`.
|
|
1473
1480
|
|
|
1481
|
+
## Handoff (sync vs async)
|
|
1482
|
+
- **Async (default):** share the UI/plan URL and end the turn. Resume when the human says the review or verdict is ready.
|
|
1483
|
+
- **Sync:** call \`await_review\` / \`await_plan_review\` only when the human is reviewing now or asked you to wait.
|
|
1484
|
+
- \`status=timeout\` + \`disposition=park\` means the wait budget elapsed — park; do not silent-loop. At most one extra await if they asked you to keep waiting.
|
|
1485
|
+
|
|
1474
1486
|
## Code review
|
|
1475
1487
|
1. Call \`review_session_status\`; call \`start_review_session\` if needed.
|
|
1476
1488
|
2. Prefer \`diff_summary\` → paged \`diff_files\` → \`diff_hunks\` / bounded \`diff_slice\`; use \`get_diff\` only as an escape hatch.
|
|
1477
1489
|
3. Use \`create_comment\` for actionable inline findings.
|
|
1478
|
-
4.
|
|
1490
|
+
4. Prefer async handoff; use \`await_review\` for sync waits. On resume, one \`await_review\` (or \`list_comments\`) replays a prior Send-to-agent.
|
|
1479
1491
|
5. In \`comment-only\` mode, reply without editing files.
|
|
1480
1492
|
|
|
1481
1493
|
## Plan review
|
|
1482
1494
|
1. Start or reuse a review session.
|
|
1483
|
-
2. Call \`submit_plan\`,
|
|
1484
|
-
3. On
|
|
1485
|
-
4. On \`
|
|
1495
|
+
2. Call \`submit_plan\`, share the URL, and park unless asked to wait.
|
|
1496
|
+
3. On resume or sync wait, call \`await_plan_review\` (or \`get_plan\` / \`list_plans\`).
|
|
1497
|
+
4. On \`changes-requested\`, revise and resubmit with the same \`planId\`.
|
|
1498
|
+
5. On \`approved\`, proceed; on \`rejected\`, stop.
|
|
1486
1499
|
|
|
1487
1500
|
## GitHub PR
|
|
1488
1501
|
1. In \`gh-pr\` mode call \`gh_overview\`, then the bounded diff tools.
|
|
@@ -1510,7 +1523,7 @@ MCP connects only to the loopback diffing server and never terminates a user-own
|
|
|
1510
1523
|
role: "user",
|
|
1511
1524
|
content: {
|
|
1512
1525
|
type: "text",
|
|
1513
|
-
text: `Use diffing for plan review in ${repoRoot}. Start or reuse a review session, submit this plan, and
|
|
1526
|
+
text: `Use diffing for plan review in ${repoRoot}. Start or reuse a review session, submit this plan, share the plan URL, and park (async handoff) unless the human asked you to wait. When they are ready or asked you to block, call await_plan_review. On changes-requested revise and resubmit the same planId; on rejected stop; on approved proceed. Timeout means park — do not silent-loop.
|
|
1514
1527
|
|
|
1515
1528
|
` + plan
|
|
1516
1529
|
}
|
|
@@ -2,7 +2,7 @@ import { C as isSafePath, S as revertHunk, _ as getUntrackedFilePathsAsync, a as
|
|
|
2
2
|
import { a as searchContent, c as trackSelection, i as searchAll, n as saveSettings, o as searchFiles, r as getSearchStatus, s as searchSymbols, t as loadSettings } from "./settings-bwe7yQ8U.mjs";
|
|
3
3
|
import { t as formatComments } from "./comment-format-B5AcGcR-.mjs";
|
|
4
4
|
import { n as formatPlanReview, r as sectionTitleForLine, t as extractPlanLines } from "./plan-format-BQyx--mp.mjs";
|
|
5
|
-
import { D as updatePrReviewComment, E as submitReview, S as refreshPrSession, T as setPrReviewThreadResolved, a as detectCwdRepo, b as parsePrRef, f as fetchPrFileContentViaGh, i as deletePrReviewComment, l as fetchExistingCommentsViaGh, t as buildPrSession, u as fetchExistingReviewsViaGh, w as resolvedFromSession } from "./github-
|
|
5
|
+
import { D as updatePrReviewComment, E as submitReview, S as refreshPrSession, T as setPrReviewThreadResolved, a as detectCwdRepo, b as parsePrRef, f as fetchPrFileContentViaGh, i as deletePrReviewComment, l as fetchExistingCommentsViaGh, t as buildPrSession, u as fetchExistingReviewsViaGh, w as resolvedFromSession } from "./github-CEjtGd41.mjs";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -51,6 +51,8 @@ const DEFAULTS = {
|
|
|
51
51
|
outputMode: "auto",
|
|
52
52
|
host: "127.0.0.1",
|
|
53
53
|
noOpen: false,
|
|
54
|
+
reuseSession: false,
|
|
55
|
+
replaceSession: false,
|
|
54
56
|
help: false,
|
|
55
57
|
version: false,
|
|
56
58
|
includeUnstaged: true,
|
|
@@ -179,6 +181,14 @@ const DIFFING_OPTIONS = {
|
|
|
179
181
|
type: "boolean",
|
|
180
182
|
default: false
|
|
181
183
|
},
|
|
184
|
+
"reuse-session": {
|
|
185
|
+
type: "boolean",
|
|
186
|
+
default: false
|
|
187
|
+
},
|
|
188
|
+
"replace-session": {
|
|
189
|
+
type: "boolean",
|
|
190
|
+
default: false
|
|
191
|
+
},
|
|
182
192
|
web: {
|
|
183
193
|
type: "boolean",
|
|
184
194
|
default: false
|
|
@@ -226,6 +236,8 @@ Diffing Server Options:
|
|
|
226
236
|
--host <host> Host address to bind to (default: 127.0.0.1). Pass
|
|
227
237
|
0.0.0.0 to expose the server to the local network.
|
|
228
238
|
--no-open Don't open the browser automatically
|
|
239
|
+
--reuse-session If a review is already running, open it instead of prompting
|
|
240
|
+
--replace-session If a review is already running, stop it and start a new one
|
|
229
241
|
--gh-pr <ref> Open a GitHub PR review session (number, owner/repo#N, or URL)
|
|
230
242
|
Same as: diffing "gh pr <ref>"
|
|
231
243
|
|
|
@@ -488,6 +500,8 @@ function parseDiffOptions(rawArgs) {
|
|
|
488
500
|
if (values.port) opts.port = parseInt(values.port, 10);
|
|
489
501
|
if (values.host) opts.host = values.host;
|
|
490
502
|
if (values["no-open"]) opts.noOpen = true;
|
|
503
|
+
if (values["reuse-session"]) opts.reuseSession = true;
|
|
504
|
+
if (values["replace-session"]) opts.replaceSession = true;
|
|
491
505
|
if (values["gh-pr"]) opts.ghPr = String(values["gh-pr"]);
|
|
492
506
|
if (values.staged || values.cached) opts.staged = true;
|
|
493
507
|
if (values.merge) opts.merge = true;
|
|
@@ -3401,7 +3415,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3401
3415
|
if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
|
|
3402
3416
|
const absolutePath = resolve(root, relPath);
|
|
3403
3417
|
const content = await readFile(absolutePath, "utf-8");
|
|
3404
|
-
const { applySuggestionToContent } = await import("./apply-suggestion-
|
|
3418
|
+
const { applySuggestionToContent } = await import("./apply-suggestion-DNNsC5gx.mjs");
|
|
3405
3419
|
const result = applySuggestionToContent({
|
|
3406
3420
|
content,
|
|
3407
3421
|
lineNumber: comment.lineNumber,
|
|
@@ -3648,7 +3662,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3648
3662
|
const session = await prStore.get();
|
|
3649
3663
|
if (!session) return notInPrMode(c);
|
|
3650
3664
|
try {
|
|
3651
|
-
const { fetchPrChecks } = await import("./github-
|
|
3665
|
+
const { fetchPrChecks } = await import("./github-DKT0lJXv.mjs");
|
|
3652
3666
|
const checks = await fetchPrChecks(resolvedFromSession(session), session.headSha);
|
|
3653
3667
|
const summary = {
|
|
3654
3668
|
total: checks.length,
|
|
@@ -3680,7 +3694,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3680
3694
|
const text = typeof body.body === "string" ? body.body.trim() : "";
|
|
3681
3695
|
if (!text) return c.json({ error: "body is required" }, 400);
|
|
3682
3696
|
try {
|
|
3683
|
-
const { replyToPrComment } = await import("./github-
|
|
3697
|
+
const { replyToPrComment } = await import("./github-DKT0lJXv.mjs");
|
|
3684
3698
|
const result = await replyToPrComment({
|
|
3685
3699
|
resolved: resolvedFromSession(session),
|
|
3686
3700
|
inReplyTo: id,
|
|
@@ -3894,18 +3908,32 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3894
3908
|
if (decision !== "approve" && decision !== "comment" && decision !== "request-changes" && decision !== "draft") return c.json({ error: "decision must be one of: approve, comment, request-changes, draft" }, 400);
|
|
3895
3909
|
const generalBody = typeof body.body === "string" ? body.body : "";
|
|
3896
3910
|
if (body.dryRun === true) {
|
|
3897
|
-
const { buildReviewPayload } = await import("./github-
|
|
3911
|
+
const { buildReviewPayload, resolvedFromSession } = await import("./github-DKT0lJXv.mjs");
|
|
3912
|
+
const { rewriteLocalAttachmentsInBodies } = await import("./github-attachments-Cf17YWp4.mjs");
|
|
3913
|
+
const resolved = resolvedFromSession(session);
|
|
3914
|
+
const comments = session.comments ?? [];
|
|
3915
|
+
const rewritten = await rewriteLocalAttachmentsInBodies(resolved, [generalBody, ...comments.map((c) => c.body ?? "")], { dryRun: true });
|
|
3916
|
+
if (rewritten.error) return c.json({
|
|
3917
|
+
ok: false,
|
|
3918
|
+
dryRun: true,
|
|
3919
|
+
error: rewritten.error
|
|
3920
|
+
}, 400);
|
|
3921
|
+
const [body, ...commentBodies] = rewritten.bodies;
|
|
3898
3922
|
const payload = buildReviewPayload({
|
|
3899
3923
|
decision,
|
|
3900
|
-
body: generalBody,
|
|
3901
|
-
comments:
|
|
3924
|
+
body: body ?? generalBody,
|
|
3925
|
+
comments: comments.map((c, i) => ({
|
|
3926
|
+
...c,
|
|
3927
|
+
body: commentBodies[i] ?? c.body
|
|
3928
|
+
}))
|
|
3902
3929
|
});
|
|
3903
3930
|
return c.json({
|
|
3904
3931
|
ok: true,
|
|
3905
3932
|
dryRun: true,
|
|
3906
3933
|
authSource: session.authSource ?? "none",
|
|
3907
3934
|
failedComments: 0,
|
|
3908
|
-
payload
|
|
3935
|
+
payload,
|
|
3936
|
+
attachmentRewrites: rewritten.urlMap
|
|
3909
3937
|
});
|
|
3910
3938
|
}
|
|
3911
3939
|
const result = await submitReview({
|
|
@@ -3978,7 +4006,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
3978
4006
|
const openCount = all.filter((x) => x.status === "open").length;
|
|
3979
4007
|
let diffFingerprints;
|
|
3980
4008
|
try {
|
|
3981
|
-
const { fingerprintDiffFiles } = await import("./diff-fingerprint-
|
|
4009
|
+
const { fingerprintDiffFiles } = await import("./diff-fingerprint-C8YO3TTE.mjs");
|
|
3982
4010
|
if (prMode) {
|
|
3983
4011
|
const prSession = await prStore.get();
|
|
3984
4012
|
if (prSession?.diff) diffFingerprints = fingerprintDiffFiles(prSession.diff);
|
|
@@ -4046,7 +4074,7 @@ function createApp(clientDir, diffOptsInput = DEFAULTS, commentStore, planStore,
|
|
|
4046
4074
|
} catch (err) {
|
|
4047
4075
|
return c.json({ error: err?.message ?? "Failed to compute current diff" }, 500);
|
|
4048
4076
|
}
|
|
4049
|
-
const { fingerprintDiffFiles, diffSinceLast, filesToReviewSinceLast } = await import("./diff-fingerprint-
|
|
4077
|
+
const { fingerprintDiffFiles, diffSinceLast, filesToReviewSinceLast } = await import("./diff-fingerprint-C8YO3TTE.mjs");
|
|
4050
4078
|
const delta = diffSinceLast(previous, fingerprintDiffFiles(currentPatch));
|
|
4051
4079
|
return c.json({
|
|
4052
4080
|
hasBaseline: true,
|
|
@@ -9,7 +9,7 @@ const STARTUP_LEASE_STALE_MS = 3e4;
|
|
|
9
9
|
* mismatches between a user-started session and an MCP request.
|
|
10
10
|
*/
|
|
11
11
|
function diffScopeKey(options) {
|
|
12
|
-
const { port: _port, host: _host, noOpen: _noOpen, help: _help, version: _version, outputMode: _outputMode, tui: _tui, gpu: _gpu, noExtDiff: _noExtDiff, ...scope } = options;
|
|
12
|
+
const { port: _port, host: _host, noOpen: _noOpen, reuseSession: _reuseSession, replaceSession: _replaceSession, help: _help, version: _version, outputMode: _outputMode, tui: _tui, gpu: _gpu, noExtDiff: _noExtDiff, ...scope } = options;
|
|
13
13
|
return JSON.stringify(scope);
|
|
14
14
|
}
|
|
15
15
|
function lockPath(repoRoot) {
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{at as e,ot as t}from"./chunk-CSCIHK7Q-CeHxe0Uf.js";var n=(n,r)=>t.lang.round(e.parse(n)[r]);export{n as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{h as e}from"./src-Cn22-GjE.js";var t=class{constructor(e){this.init=e,this.records=this.init()}static{e(this,`ImperativeState`)}reset(){this.records=this.init()}};export{t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{h as e,p as t}from"./src-Cn22-GjE.js";import{x as n}from"./chunk-CSCIHK7Q-CeHxe0Uf.js";var r=e(e=>{let{securityLevel:r}=n(),i=t(`body`);return r===`sandbox`&&(i=t((t(`#i${e}`).node()?.contentDocument??document).body)),i.select(`#${e}`)},`selectSvgElement`);export{r as t};
|