diffing 0.10.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -32
- package/dist/{cli-agent-BBHx1lLJ.mjs → cli-agent-DkwGxuy2.mjs} +43 -12
- package/dist/{cli-gh-B8HybRWZ.mjs → cli-gh-D0NfKpOp.mjs} +4 -4
- package/dist/cli.mjs +165 -35
- package/dist/client/assets/{angular-html-Bn7xcK-X.js → angular-html-Dl53kkiy.js} +1 -1
- package/dist/client/assets/{angular-ts-qK80FnOc.js → angular-ts-wNJFFkey.js} +1 -1
- package/dist/client/assets/{apl-BhBHq9NH.js → apl-BxtQST-h.js} +1 -1
- package/dist/client/assets/{arc-BbRWqKdi.js → arc-CBmZuzPB.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-DkAZjTe2.js → architectureDiagram-3BPJPVTR-CEci_K9f.js} +1 -1
- package/dist/client/assets/{astro--A8NjYkS.js → astro-BpADD5rh.js} +1 -1
- package/dist/client/assets/{blade-DUJ_WZzW.js → blade-B4I5SQmP.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-BJnX6alY.js → blockDiagram-GPEHLZMM-DcKhDtnE.js} +1 -1
- package/dist/client/assets/{c-Ddzn0JTQ.js → c-C3VtOgg8.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-B_JFq-Tk.js → c4Diagram-AAUBKEIU-CaHozoxF.js} +1 -1
- package/dist/client/assets/channel-Ct4-oSQN.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CCnCUy9H.js → chunk-2J33WTMH-tvuzKhdK.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-CJvhnVzd.js → chunk-3OPIFGDE-D_nBoD9w.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-7gGFn4DW.js → chunk-4BX2VUAB-yHPdy1G8.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-CRirC3B0.js → chunk-55IACEB6-yXpL8wjV.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-DSagYOhc.js → chunk-5ZQYHXKU-DZ5cvCDV.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-2fqFvPdL.js → chunk-727SXJPM-CJjgOWmY.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-8l-kDb0Q.js → chunk-AQP2D5EJ-CQYQKXSE.js} +1 -1
- package/dist/client/assets/{chunk-BSJP7CBP-BjTEoy7R.js → chunk-BSJP7CBP-CP_f908b.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-CeHxe0Uf.js → chunk-CSCIHK7Q-tcgA8jR0.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-X-OwPNTB.js → chunk-FMBD7UC4-DlpmhBYg.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-rYccR2Ju.js → chunk-KSCS5N6A-BkfieXpV.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-8JMoad-S.js → chunk-L5ZTLDWV-BjVXSZnt.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DOvCLema.js → chunk-LZXEDZCA-CaAWmy4Q.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-CQQJmqC8.js → chunk-ND2GUHAM-kWh41zUn.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-DPwHQE2m.js → chunk-NZK2D7GU-CDrIi44_.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-Bvzv6QAN.js → chunk-O5CBEL6O-CbNGK3Te.js} +1 -1
- package/dist/client/assets/chunk-QZHKN3VN-BXhKFVX7.js +1 -0
- package/dist/client/assets/chunk-WU5MYG2G-_EwuC51_.js +1 -0
- package/dist/client/assets/{chunk-XPW4576I-DY1teoyy.js → chunk-XPW4576I-Cca0fTtG.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C8bdx9R4.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B26wrQuH.js +1 -0
- package/dist/client/assets/{cobol-EaVCzYAO.js → cobol-j-6eG2UQ.js} +1 -1
- package/dist/client/assets/{coffee-CbmPxJhz.js → coffee-CZLH0dtt.js} +1 -1
- package/dist/client/assets/{cose-bilkent-S5V4N54A-zMLfkB6c.js → cose-bilkent-S5V4N54A-DlxESVex.js} +1 -1
- package/dist/client/assets/{cpp-1v8ov4EG.js → cpp-j6Bp24RI.js} +1 -1
- package/dist/client/assets/{crystal-B88NqWAb.js → crystal-DW7wcFUs.js} +1 -1
- package/dist/client/assets/{css-QFeW3BCZ.js → css-C43BDU2i.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-DxGKZBBq.js → dagre-BM42HDAG-BaemEJFh.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-CiUL7Dov.js → diagram-2AECGRRQ-BaM-bkJ3.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-DCs97wXn.js → diagram-5GNKFQAL-BOH04DJO.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BsSJWXqr.js → diagram-KO2AKTUF-BP5MyqDZ.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-LDuP4WAy.js → diagram-LMA3HP47-Dt4gKMYF.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-CKjIVoHx.js → diagram-OG6HWLK6-DoT1VshN.js} +1 -1
- package/dist/client/assets/{dist-BvtPW002.js → dist-DEAxvC4F.js} +1 -1
- package/dist/client/assets/{edge-5UREsVXQ.js → edge-D9ySFlnD.js} +1 -1
- package/dist/client/assets/{elixir-B_Yali6w.js → elixir-CoJz_LQ5.js} +1 -1
- package/dist/client/assets/{elm-STYz8ayI.js → elm-CvedWh_e.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D7iBLlYp.js → erDiagram-TEJ5UH35-CMP78FsQ.js} +1 -1
- package/dist/client/assets/{erb-CHRx3Beu.js → erb-lD_yYZsm.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Dr2J9T5z.js → flowDiagram-I6XJVG4X-Dp0s5BFj.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-Bb1v2M1H.js → ganttDiagram-6RSMTGT7-GmSG31U-.js} +1 -1
- package/dist/client/assets/{git-rebase-CFmRpmLK.js → git-rebase-DwZABzof.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DP9xmnTA.js → gitGraphDiagram-PVQCEYII-C1SXShag.js} +1 -1
- package/dist/client/assets/{glimmer-js-CdrEdtzi.js → glimmer-js-CWcs3w1Z.js} +1 -1
- package/dist/client/assets/{glimmer-ts-CzrwZqCl.js → glimmer-ts-CrzGN4lJ.js} +1 -1
- package/dist/client/assets/{glsl-Cb4XetAc.js → glsl-DRCtwX7h.js} +1 -1
- package/dist/client/assets/{graphql-mqi9K2_l.js → graphql-rpmrismS.js} +1 -1
- package/dist/client/assets/{hack-DaFJ7htO.js → hack-Ba02RnuQ.js} +1 -1
- package/dist/client/assets/{haml-Bw7q4nKg.js → haml-BHBACStm.js} +1 -1
- package/dist/client/assets/{handlebars-mj81MoXX.js → handlebars-BtlQ1SCa.js} +1 -1
- package/dist/client/assets/{html-LHrj0-2t.js → html-BkVU3Dge.js} +1 -1
- package/dist/client/assets/{html-derivative-L3_OYrFu.js → html-derivative-DoK92vwi.js} +1 -1
- package/dist/client/assets/{http-DE0OFe2c.js → http-CJAwKSPH.js} +1 -1
- package/dist/client/assets/{hurl-CNbk7fqf.js → hurl-kaX5jwuN.js} +1 -1
- package/dist/client/assets/{index-DHt5Hu--.js → index-BHeNiC07.js} +261 -199
- package/dist/client/assets/index-D8hOoJCg.css +1 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-wxqSmRV0.js → infoDiagram-5YYISTIA-B0i3BlNR.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CJa0qhXu.js → ishikawaDiagram-YF4QCWOH-BN8sEei4.js} +1 -1
- package/dist/client/assets/{java-DAwMoXxA.js → java-BvrigIoq.js} +1 -1
- package/dist/client/assets/{javascript-_a2prAz7.js → javascript-DZkPTYAi.js} +1 -1
- package/dist/client/assets/{jinja-JKFA3bu8.js → jinja-Ceg7ui1S.js} +1 -1
- package/dist/client/assets/{jison-UbTjhW7k.js → jison-iE1GaOyN.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Bh5L3OBN.js → journeyDiagram-JHISSGLW-CE6O3NhU.js} +1 -1
- package/dist/client/assets/{json-D74F0BDU.js → json-C0j8rEcg.js} +1 -1
- package/dist/client/assets/{jsx-BsH2-gDN.js → jsx-B7eXljgz.js} +1 -1
- package/dist/client/assets/{julia-DyIO_Gy0.js → julia-DMDw0aMP.js} +1 -1
- package/dist/client/assets/{just-VQTFAZJr.js → just-BryMKRJ_.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BB2voi9I.js → kanban-definition-UN3LZRKU-j-aU6Yzp.js} +1 -1
- package/dist/client/assets/{latex-D0ONGfIo.js → latex-BNC9-sjr.js} +1 -1
- package/dist/client/assets/{line-BkbHeSM4.js → line-CyEqpEDp.js} +1 -1
- package/dist/client/assets/{linear-BYUYqOy1.js → linear-CXeaA9km.js} +1 -1
- package/dist/client/assets/{liquid-BnYpmfMl.js → liquid-D-fQw-oJ.js} +1 -1
- package/dist/client/assets/{lua-WlA-m2lC.js → lua-CD2aJ_rS.js} +1 -1
- package/dist/client/assets/{marko-Bd44681W.js → marko-BQ37hu6z.js} +1 -1
- package/dist/client/assets/{mdc-DhosSoGv.js → mdc-B5NUi3zZ.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-Dq0EOOjQ.js → mermaid-parser.core-BxHF_XbX.js} +1 -1
- package/dist/client/assets/{mermaid.core-qcl7ght-.js → mermaid.core-BUtaf4P1.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-DrZ9di3F.js → mindmap-definition-RKZ34NQL-D13tPu2o.js} +1 -1
- package/dist/client/assets/{nginx-sYwNRk-F.js → nginx-1W1E03Rg.js} +1 -1
- package/dist/client/assets/{nim-DrXU2tn9.js → nim-C5A88bFP.js} +1 -1
- package/dist/client/assets/{perl-TEELuyd-.js → perl-BYoP0wNt.js} +1 -1
- package/dist/client/assets/{php-Dy_Y1r8j.js → php-CFxna09e.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-Aev8QdJC.js → pieDiagram-4H26LBE5-CHL7Ml5w.js} +1 -1
- package/dist/client/assets/{pug-vancrY0a.js → pug-BR4JgWAk.js} +1 -1
- package/dist/client/assets/{qml-BA5z-zbf.js → qml-D1U1EFEe.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DhEDvxx7.js → quadrantDiagram-W4KKPZXB-BnYtv-HY.js} +1 -1
- package/dist/client/assets/{r-BR0O4N-s.js → r-ChE6X060.js} +1 -1
- package/dist/client/assets/{razor-DXiNCb-5.js → razor-B5iVfQMe.js} +1 -1
- package/dist/client/assets/{regexp-BhjJa9MI.js → regexp-Dz2drUIf.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DPS5siTy.js → requirementDiagram-4Y6WPE33-D8QEGLKB.js} +1 -1
- package/dist/client/assets/{review-export-DABY3y7y.js → review-export-CvFFbCAB.js} +1 -1
- package/dist/client/assets/{rst-Bi3zId1d.js → rst-DZM3RGRs.js} +1 -1
- package/dist/client/assets/{ruby-BURiJKuD.js → ruby-DkXnm-J8.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CjZF2AQd.js → sankeyDiagram-5OEKKPKP-wVaks6lB.js} +1 -1
- package/dist/client/assets/{sas-BO_W1LuL.js → sas-DMbpc71c.js} +1 -1
- package/dist/client/assets/{scss-BPUTinXb.js → scss-IwVMMp22.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-0rsTmBLB.js → sequenceDiagram-3UESZ5HK-Cg31TB-R.js} +1 -1
- package/dist/client/assets/{shellscript-B4rfo4qg.js → shellscript-CIStAKx3.js} +1 -1
- package/dist/client/assets/{shellsession-DLX-O8m7.js → shellsession-uLtzHhlo.js} +1 -1
- package/dist/client/assets/{soy-BEUiwSVD.js → soy-BkxBYigm.js} +1 -1
- package/dist/client/assets/{sql-CwSA8v_H.js → sql-Ba9y9EvJ.js} +1 -1
- package/dist/client/assets/{src-Cn22-GjE.js → src-9MKexpFu.js} +1 -1
- package/dist/client/assets/{stata-XfdBiAnW.js → stata-CrPCbiIZ.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-BVvCsgjA.js → stateDiagram-AJRCARHV-BbvNjVF6.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-B5BbZkU5.js +1 -0
- package/dist/client/assets/{surrealql-COaehKkS.js → surrealql-Dvip5u_o.js} +1 -1
- package/dist/client/assets/{svelte-B0kTIq0J.js → svelte-xBLD60Zd.js} +1 -1
- package/dist/client/assets/{templ-DGxWfDf-.js → templ-CWv68xbP.js} +1 -1
- package/dist/client/assets/{tex-C9X3gjZ2.js → tex-CHqz-fYs.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-B535P7_e.js → timeline-definition-PNZ67QCA-CWKkJ3QZ.js} +1 -1
- package/dist/client/assets/{ts-tags-By9ETsTC.js → ts-tags-DS82EDuE.js} +1 -1
- package/dist/client/assets/{tsx-CvVX506n.js → tsx-B2GBjp2F.js} +1 -1
- package/dist/client/assets/{twig-CjLWv7eu.js → twig-CC8x9pG0.js} +1 -1
- package/dist/client/assets/{typescript-BEq3u3Bc.js → typescript-g7i13lnC.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CTV9H1N2.js → vennDiagram-CIIHVFJN-OLItKWeg.js} +1 -1
- package/dist/client/assets/{vue-html-92z_Aqn0.js → vue-html-D9GQtYub.js} +1 -1
- package/dist/client/assets/{vue-DYazforU.js → vue-rFbe7luB.js} +1 -1
- package/dist/client/assets/{vue-vine-VY7raYSI.js → vue-vine-mrNtQtY5.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BzJaykc6.js → wardleyDiagram-YWT4CUSO-Dy1yfeb4.js} +1 -1
- package/dist/client/assets/{xml-hd7OQ798.js → xml-CHfMjyAM.js} +1 -1
- package/dist/client/assets/{xsl-MkuT76g1.js → xsl-BFkNSogJ.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-D36D8smx.js → xychartDiagram-2RQKCTM6-DfSnfjVL.js} +1 -1
- package/dist/client/assets/{yaml-D6zRzOR2.js → yaml-BfSgHc04.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/{completions-Csvfqctx.mjs → completions-BO2DUM-8.mjs} +23 -1
- package/dist/{doctor-B3Nrydc0.mjs → doctor-D0Y3kOY7.mjs} +5 -4
- package/dist/{find-tui-binary-CRpbYXLC.mjs → find-tui-binary-B71xiwWu.mjs} +33 -15
- package/dist/github-CErBub2y.mjs +2 -0
- package/dist/{github-Cf5blBty.mjs → github-DBsZ0h_l.mjs} +47 -5
- package/dist/github-attachments-plDeB0iE.mjs +351 -0
- package/dist/handoff-LAFtmnHf.mjs +11 -0
- package/dist/{mcp-DwnYjFS0.mjs → mcp-DtVrMR2S.mjs} +41 -22
- package/dist/native/tui-darwin-arm64/diffing-tui +0 -0
- package/dist/native/tui-darwin-x64/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-musl/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-musl/diffing-tui +0 -0
- package/dist/native/tui-win32-x64-msvc/diffing-tui.exe +0 -0
- package/dist/{review-export-DjxKBeIZ.mjs → review-export-B63eRAVa.mjs} +1 -1
- package/dist/{settings-bwe7yQ8U.mjs → search-BMPY_ZW8.mjs} +117 -97
- package/dist/search-BO9Im7eu.mjs +2 -0
- package/dist/{server-CZeMCHEa.mjs → server-j0PnTVX1.mjs} +174 -75
- package/dist/server-lock-CY0_1mcS.mjs +275 -0
- package/dist/session-conflict-C6V9ZKo7.mjs +96 -0
- package/dist/session-manager-CYNchhVA.mjs +159 -0
- package/dist/settings-yK0GYpbn.mjs +60 -0
- package/dist/tui-search-bridge-Bfb2ktdA.mjs +133 -0
- package/package.json +6 -4
- 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/index-B42c0TyV.css +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Bo6SeNmU.js +0 -1
- package/dist/github-DWgPIcKC.mjs +0 -2
- package/dist/server-lock-uqSdDYeR.mjs +0 -153
- /package/dist/{apply-suggestion-BAuyCYk6.mjs → apply-suggestion-CHYnw68I.mjs} +0 -0
- /package/dist/{comment-format-B5AcGcR-.mjs → comment-format-B85NHZnG.mjs} +0 -0
- /package/dist/{diff-fingerprint-CWa-soYD.mjs → diff-fingerprint-BgR0tu1o.mjs} +0 -0
- /package/dist/{plan-format-BQyx--mp.mjs → plan-format-BwJh5JSE.mjs} +0 -0
- /package/dist/{update-check-fe0tR8tx.mjs → update-check-Copcz9eb.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-DBsZ0h_l.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,
|
|
2
|
-
import { t as formatComments } from "./comment-format-
|
|
3
|
-
import { n as formatPlanReview } from "./plan-format-
|
|
4
|
-
import {
|
|
1
|
+
import { i as buildGitDiffArgs, s as parseDiffOptions, t as startServer } from "./server-j0PnTVX1.mjs";
|
|
2
|
+
import { t as formatComments } from "./comment-format-B85NHZnG.mjs";
|
|
3
|
+
import { n as formatPlanReview } from "./plan-format-BwJh5JSE.mjs";
|
|
4
|
+
import { c as removeServerLockIfOwned, i as isLockAlive, p as writeServerLock, r as diffScopeKey, s as removeServerLock, t as acquireServerStartupLease, u as resolveActiveServerLock } from "./server-lock-CY0_1mcS.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-LAFtmnHf.mjs";
|
|
5
6
|
import { execFileSync } from "node:child_process";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -337,7 +338,7 @@ function createMcpServer(options) {
|
|
|
337
338
|
const repoRoot = resolveMcpRepository(options.repoRoot, true);
|
|
338
339
|
const startServerFn = options.startServerFn ?? startServer;
|
|
339
340
|
const now = options.now ?? Date.now;
|
|
340
|
-
const readLock = options.readLock ??
|
|
341
|
+
const readLock = options.readLock ?? resolveActiveServerLock;
|
|
341
342
|
const writeLock = options.writeLock ?? writeServerLock;
|
|
342
343
|
const removeLock = options.removeLock ?? removeServerLock;
|
|
343
344
|
const lockIsAlive = options.lockIsAlive ?? isLockAlive;
|
|
@@ -345,14 +346,17 @@ 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
|
|
352
353
|
}, { instructions });
|
|
353
354
|
let reviewCursor = null;
|
|
354
355
|
let planCursor = null;
|
|
356
|
+
let selectedLock = null;
|
|
355
357
|
function liveLock() {
|
|
358
|
+
if (selectedLock && lockIsAlive(selectedLock, repoRoot)) return selectedLock;
|
|
359
|
+
selectedLock = null;
|
|
356
360
|
const lock = readLock(repoRoot);
|
|
357
361
|
return lock && lockIsAlive(lock, repoRoot) ? lock : null;
|
|
358
362
|
}
|
|
@@ -508,6 +512,7 @@ function createMcpServer(options) {
|
|
|
508
512
|
if (!sameScope(existing, requestedScope, diffArgs)) throw new Error(`A live diffing web session already shows a different diff scope. Requested arguments: ${JSON.stringify(diffArgs)}. Use the existing session or end it manually; diffing will not replace it.`);
|
|
509
513
|
const url = lockUrl(existing);
|
|
510
514
|
if (!url) throw new Error("The active diffing web session has no safe loopback URL.");
|
|
515
|
+
selectedLock = existing;
|
|
511
516
|
return {
|
|
512
517
|
status: "reused",
|
|
513
518
|
repository: repoRoot,
|
|
@@ -541,7 +546,8 @@ function createMcpServer(options) {
|
|
|
541
546
|
scope: requestedScope,
|
|
542
547
|
diffArgs: [...diffArgs],
|
|
543
548
|
owner: "mcp",
|
|
544
|
-
ownerId
|
|
549
|
+
ownerId,
|
|
550
|
+
sessionId: ownerId
|
|
545
551
|
};
|
|
546
552
|
try {
|
|
547
553
|
writeLock(lock);
|
|
@@ -551,6 +557,7 @@ function createMcpServer(options) {
|
|
|
551
557
|
const detail = error instanceof Error ? error.message : String(error);
|
|
552
558
|
throw new Error(`The review server bound locally but its discovery lock could not be written: ${detail}. No MCP session was claimed; retry after fixing lock storage. The unadvertised loopback listener will close when this MCP process exits.`);
|
|
553
559
|
}
|
|
560
|
+
selectedLock = lock;
|
|
554
561
|
return {
|
|
555
562
|
status: "started",
|
|
556
563
|
repository: repoRoot,
|
|
@@ -915,10 +922,11 @@ function createMcpServer(options) {
|
|
|
915
922
|
});
|
|
916
923
|
server.registerTool("await_review", {
|
|
917
924
|
title: "Wait for code review handoff",
|
|
918
|
-
description: "
|
|
919
|
-
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(
|
|
925
|
+
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,
|
|
926
|
+
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
|
|
920
927
|
outputSchema: {
|
|
921
928
|
status: z.enum(["released", "timeout"]),
|
|
929
|
+
disposition: z.enum(["park"]).optional(),
|
|
922
930
|
mode: z.enum(["standard", "comment-only"]),
|
|
923
931
|
round: z.number(),
|
|
924
932
|
openCount: z.number().optional(),
|
|
@@ -978,9 +986,10 @@ function createMcpServer(options) {
|
|
|
978
986
|
}
|
|
979
987
|
const structured = {
|
|
980
988
|
status: "timeout",
|
|
989
|
+
disposition: "park",
|
|
981
990
|
mode: "standard",
|
|
982
991
|
round: sinceRound,
|
|
983
|
-
nextAction:
|
|
992
|
+
nextAction: AWAIT_REVIEW_TIMEOUT_NEXT_ACTION
|
|
984
993
|
};
|
|
985
994
|
return textResult(structured.nextAction, structured);
|
|
986
995
|
});
|
|
@@ -1226,7 +1235,7 @@ function createMcpServer(options) {
|
|
|
1226
1235
|
});
|
|
1227
1236
|
server.registerTool("submit_plan", {
|
|
1228
1237
|
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.",
|
|
1238
|
+
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
1239
|
inputSchema: {
|
|
1231
1240
|
title: z.string().optional().describe("Human-readable plan title."),
|
|
1232
1241
|
body: z.string().min(1).describe("Complete Markdown plan body."),
|
|
@@ -1238,7 +1247,8 @@ function createMcpServer(options) {
|
|
|
1238
1247
|
status: z.literal("submitted"),
|
|
1239
1248
|
planId: z.string(),
|
|
1240
1249
|
version: z.number(),
|
|
1241
|
-
url: z.string()
|
|
1250
|
+
url: z.string(),
|
|
1251
|
+
nextAction: z.string()
|
|
1242
1252
|
},
|
|
1243
1253
|
annotations: MUTATING
|
|
1244
1254
|
}, async ({ title, body, source, model, planId }) => {
|
|
@@ -1257,19 +1267,21 @@ function createMcpServer(options) {
|
|
|
1257
1267
|
})
|
|
1258
1268
|
});
|
|
1259
1269
|
const url = `${base}/plan/${plan.id}`;
|
|
1260
|
-
return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}.
|
|
1270
|
+
return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. ${PLAN_SUBMIT_NEXT_ACTION}`, {
|
|
1261
1271
|
status: "submitted",
|
|
1262
1272
|
planId: plan.id,
|
|
1263
1273
|
version: plan.version,
|
|
1264
|
-
url
|
|
1274
|
+
url,
|
|
1275
|
+
nextAction: PLAN_SUBMIT_NEXT_ACTION
|
|
1265
1276
|
});
|
|
1266
1277
|
});
|
|
1267
1278
|
server.registerTool("await_plan_review", {
|
|
1268
1279
|
title: "Wait for plan review verdict",
|
|
1269
|
-
description: "
|
|
1270
|
-
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(
|
|
1280
|
+
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,
|
|
1281
|
+
inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
|
|
1271
1282
|
outputSchema: {
|
|
1272
1283
|
status: z.enum(["released", "timeout"]),
|
|
1284
|
+
disposition: z.enum(["park"]).optional(),
|
|
1273
1285
|
mode: z.enum(["standard", "comment-only"]),
|
|
1274
1286
|
round: z.number(),
|
|
1275
1287
|
planId: z.string().optional(),
|
|
@@ -1335,9 +1347,10 @@ function createMcpServer(options) {
|
|
|
1335
1347
|
}
|
|
1336
1348
|
const structured = {
|
|
1337
1349
|
status: "timeout",
|
|
1350
|
+
disposition: "park",
|
|
1338
1351
|
mode: "standard",
|
|
1339
1352
|
round: sinceRound,
|
|
1340
|
-
nextAction:
|
|
1353
|
+
nextAction: AWAIT_PLAN_TIMEOUT_NEXT_ACTION
|
|
1341
1354
|
};
|
|
1342
1355
|
return textResult(structured.nextAction, structured);
|
|
1343
1356
|
});
|
|
@@ -1471,18 +1484,24 @@ function createMcpServer(options) {
|
|
|
1471
1484
|
|
|
1472
1485
|
This MCP connection is immutably bound to \`${repoRoot}\`.
|
|
1473
1486
|
|
|
1487
|
+
## Handoff (sync vs async)
|
|
1488
|
+
- **Async (default):** share the UI/plan URL and end the turn. Resume when the human says the review or verdict is ready.
|
|
1489
|
+
- **Sync:** call \`await_review\` / \`await_plan_review\` only when the human is reviewing now or asked you to wait.
|
|
1490
|
+
- \`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.
|
|
1491
|
+
|
|
1474
1492
|
## Code review
|
|
1475
1493
|
1. Call \`review_session_status\`; call \`start_review_session\` if needed.
|
|
1476
1494
|
2. Prefer \`diff_summary\` → paged \`diff_files\` → \`diff_hunks\` / bounded \`diff_slice\`; use \`get_diff\` only as an escape hatch.
|
|
1477
1495
|
3. Use \`create_comment\` for actionable inline findings.
|
|
1478
|
-
4.
|
|
1496
|
+
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
1497
|
5. In \`comment-only\` mode, reply without editing files.
|
|
1480
1498
|
|
|
1481
1499
|
## Plan review
|
|
1482
1500
|
1. Start or reuse a review session.
|
|
1483
|
-
2. Call \`submit_plan\`,
|
|
1484
|
-
3. On
|
|
1485
|
-
4. On \`
|
|
1501
|
+
2. Call \`submit_plan\`, share the URL, and park unless asked to wait.
|
|
1502
|
+
3. On resume or sync wait, call \`await_plan_review\` (or \`get_plan\` / \`list_plans\`).
|
|
1503
|
+
4. On \`changes-requested\`, revise and resubmit with the same \`planId\`.
|
|
1504
|
+
5. On \`approved\`, proceed; on \`rejected\`, stop.
|
|
1486
1505
|
|
|
1487
1506
|
## GitHub PR
|
|
1488
1507
|
1. In \`gh-pr\` mode call \`gh_overview\`, then the bounded diff tools.
|
|
@@ -1510,7 +1529,7 @@ MCP connects only to the loopback diffing server and never terminates a user-own
|
|
|
1510
1529
|
role: "user",
|
|
1511
1530
|
content: {
|
|
1512
1531
|
type: "text",
|
|
1513
|
-
text: `Use diffing for plan review in ${repoRoot}. Start or reuse a review session, submit this plan, and
|
|
1532
|
+
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
1533
|
|
|
1515
1534
|
` + plan
|
|
1516
1535
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as reviewDecisionSummary } from "./comment-format-
|
|
1
|
+
import { n as reviewDecisionSummary } from "./comment-format-B85NHZnG.mjs";
|
|
2
2
|
//#region src/lib/review-export.ts
|
|
3
3
|
/**
|
|
4
4
|
* Export a review as human-readable Markdown (clipboard / download / CLI).
|