@krelborn/markdown-review 0.2.0 → 0.4.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 +92 -44
- package/dist/cli.js +775 -117
- package/dist/web/assets/{abnfDiagram-YUKMZFIV-BZIJX5IM.js → abnfDiagram-YUKMZFIV-5p3lK0ml.js} +1 -1
- package/dist/web/assets/{angular-html-CW7LdEF9.js → angular-html-DtTOKQVF.js} +1 -1
- package/dist/web/assets/{angular-ts-DMi4USZg.js → angular-ts-DSZF2tmF.js} +1 -1
- package/dist/web/assets/{apl-Df6WGnpy.js → apl-BI5mQaXa.js} +1 -1
- package/dist/web/assets/{arc-ggKlY47U.js → arc-BrfkN3mA.js} +1 -1
- package/dist/web/assets/{architecture-WOLXFQ4H-BsQd6MbZ.js → architecture-WOLXFQ4H-CB2TejI2.js} +1 -1
- package/dist/web/assets/{architectureDiagram-47P4ROYG-BuE40WJv.js → architectureDiagram-47P4ROYG-YLfruAdT.js} +1 -1
- package/dist/web/assets/{astro-D7eXRakI.js → astro-Bx0B2FVT.js} +1 -1
- package/dist/web/assets/{blade-3AkRFe2C.js → blade-BWTzueMP.js} +1 -1
- package/dist/web/assets/{blockDiagram-E7TT5TSB-B5l0LLhP.js → blockDiagram-E7TT5TSB-DzKCvOwD.js} +1 -1
- package/dist/web/assets/{c-Bkyx37Tk.js → c-DdobnT6K.js} +1 -1
- package/dist/web/assets/{c4Diagram-CRA5TL53-D9Zc33S8.js → c4Diagram-CRA5TL53-p1ftijoG.js} +1 -1
- package/dist/web/assets/channel-B7Y79uhb.js +1 -0
- package/dist/web/assets/{chapel-DggUU-O6.js → chapel-D6rLpKo0.js} +1 -1
- package/dist/web/assets/{chunk-2BW5OAIV-ClRzBhwN.js → chunk-2BW5OAIV-DT1zLa9q.js} +1 -1
- package/dist/web/assets/{chunk-3YJQHVM4-CNLVyfxX.js → chunk-3YJQHVM4-nLx0WWI4.js} +1 -1
- package/dist/web/assets/{chunk-4EA7E6EY-aeSu9hGp.js → chunk-4EA7E6EY-C_YyuzUS.js} +1 -1
- package/dist/web/assets/{chunk-7TKQ45FW-CwZK0ys_.js → chunk-7TKQ45FW-BZ15X6vU.js} +1 -1
- package/dist/web/assets/{chunk-AW2ZBBNX-Do6yJgQr.js → chunk-AW2ZBBNX-Ck9cAmev.js} +1 -1
- package/dist/web/assets/{chunk-DBDB3WZW-BGoybXAD.js → chunk-DBDB3WZW-9vMgzi8H.js} +1 -1
- package/dist/web/assets/{chunk-E2ZNV5FY-vMPrQ96W.js → chunk-E2ZNV5FY-CyMQFbua.js} +1 -1
- package/dist/web/assets/{chunk-EU5HNXII-WT20ypGh.js → chunk-EU5HNXII-CWPCfQmx.js} +1 -1
- package/dist/web/assets/{chunk-FVRAUYC3-B-JlGXnk.js → chunk-FVRAUYC3-jyBnY8AQ.js} +1 -1
- package/dist/web/assets/{chunk-HJ2JQQFS-BpQT9yx5.js → chunk-HJ2JQQFS-Cj_rp1Qw.js} +1 -1
- package/dist/web/assets/{chunk-HTAEGDNF-uqZ4Sk_E.js → chunk-HTAEGDNF-pClFnSXR.js} +1 -1
- package/dist/web/assets/{chunk-KQW6MTUR-CGDbD7J1.js → chunk-KQW6MTUR-C2m9qy9v.js} +1 -1
- package/dist/web/assets/{chunk-NTY3LDVX-BX1ZUg2I.js → chunk-NTY3LDVX-CnUW73mn.js} +1 -1
- package/dist/web/assets/{chunk-VPRB5NB3-CKJwbb6W.js → chunk-VPRB5NB3-BuB50_Ze.js} +1 -1
- package/dist/web/assets/{chunk-XC4XBNZT-Cg0edyiW.js → chunk-XC4XBNZT-BsgxISHC.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-B-il0kWV.js → chunk-XXDRQBXY-jqsHjX2T.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-K4WV4PDN-Bm1AjKhB.js → classDiagram-v2-K4WV4PDN-B0EG_ZZt.js} +1 -1
- package/dist/web/assets/{cobol-eIQA5bMR.js → cobol-B_XRwpIg.js} +1 -1
- package/dist/web/assets/{coffee-Bcy46bJt.js → coffee-C4eMlpSe.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-BMuJXNcP.js → cose-bilkent-JH36ORCC-CrM0VDy4.js} +1 -1
- package/dist/web/assets/{cpp-BTCZX5Lq.js → cpp-BE8dVz2Y.js} +1 -1
- package/dist/web/assets/{crystal-DSpaOPwo.js → crystal-EaFwVcbb.js} +1 -1
- package/dist/web/assets/{css-DZY8fHzP.js → css-oYmBq-By.js} +1 -1
- package/dist/web/assets/{cynefin-EF2NZ3EQ-DwY3GCI9.js → cynefin-EF2NZ3EQ-34vs3gIf.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-3GCD6N5R-BYma0U6p.js → cynefinDiagram-3GCD6N5R-BknEauUi.js} +1 -1
- package/dist/web/assets/{dagre-W4DXFKR2-DpaAT1xc.js → dagre-W4DXFKR2-DZ5C0vE9.js} +1 -1
- package/dist/web/assets/{diagram-2UJZ2QOL-eWEg0n4J.js → diagram-2UJZ2QOL-7gEGibdm.js} +1 -1
- package/dist/web/assets/{diagram-OVF4WLC6-BlhoyOe_.js → diagram-OVF4WLC6-CUXH1cbU.js} +1 -1
- package/dist/web/assets/{diagram-PFPMY2P6-Cx_sb8jV.js → diagram-PFPMY2P6-BOuoN7y0.js} +1 -1
- package/dist/web/assets/{diagram-UMYRVEAY-Dk00eK0U.js → diagram-UMYRVEAY-C3xUjk3B.js} +1 -1
- package/dist/web/assets/{diagram-YEKJPTXX-wVxrnmMO.js → diagram-YEKJPTXX-CmV-UftY.js} +1 -1
- package/dist/web/assets/{diagram-ZIFT7M5P-DkD6Ez5h.js → diagram-ZIFT7M5P-DgGdZ1Kl.js} +1 -1
- package/dist/web/assets/{dist-DmWZBGGc.js → dist-CN4oxUZG.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-VR2GEFS7-DXjQVddY.js → ebnfDiagram-VR2GEFS7-CgqYKMMK.js} +1 -1
- package/dist/web/assets/{edge-A6sjNBe6.js → edge-D6C5szlZ.js} +1 -1
- package/dist/web/assets/{elixir-5kbR6ZAs.js → elixir-D6PdZkcK.js} +1 -1
- package/dist/web/assets/{elk-IJKZMXRS-Df8Ybidc.js → elk-IJKZMXRS-CaNtLBYi.js} +1 -1
- package/dist/web/assets/{elm-naNHE-Rt.js → elm-B8heN2jE.js} +1 -1
- package/dist/web/assets/{erDiagram-O2IAPWRE-CWiXv2eJ.js → erDiagram-O2IAPWRE-B_Ztqgv4.js} +1 -1
- package/dist/web/assets/{erb-VliMfn12.js → erb-CvxZym2t.js} +1 -1
- package/dist/web/assets/{eventmodeling-K75KTNOO-HdXJzsd-.js → eventmodeling-K75KTNOO-rIyIwA17.js} +1 -1
- package/dist/web/assets/flowDiagram-OXPTDLAJ-BuRr7k3B.js +1 -0
- package/dist/web/assets/{ganttDiagram-R7TSEDQI-AkbsS9Oo.js → ganttDiagram-R7TSEDQI-CZHQ2b2p.js} +1 -1
- package/dist/web/assets/{git-rebase-BEAI18h0.js → git-rebase-BQpmx8Pr.js} +1 -1
- package/dist/web/assets/{gitGraph-VSP46ZUC-BHanG7Zi.js → gitGraph-VSP46ZUC-BaI5oVFE.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-XJZIOB7I-DiFINkpn.js → gitGraphDiagram-XJZIOB7I-D019lpA0.js} +1 -1
- package/dist/web/assets/{glimmer-js-CCFczyKx.js → glimmer-js-Q0lvksee.js} +1 -1
- package/dist/web/assets/{glimmer-ts-2gqimpYj.js → glimmer-ts-BlwILAHu.js} +1 -1
- package/dist/web/assets/{glsl-Cg9k-3G7.js → glsl-C2XhxW29.js} +1 -1
- package/dist/web/assets/{graphql-BCWNUo4d.js → graphql-CQpawWlW.js} +1 -1
- package/dist/web/assets/{hack-P7UtntxJ.js → hack-D8CL9Px1.js} +1 -1
- package/dist/web/assets/{haml-CluhvLjx.js → haml-DZZoeC01.js} +1 -1
- package/dist/web/assets/{handlebars-oqVPUk2V.js → handlebars-CnQuZzNH.js} +1 -1
- package/dist/web/assets/{html-CcFED7nW.js → html-CvIoDHKR.js} +1 -1
- package/dist/web/assets/{html-derivative-8MKwl2kE.js → html-derivative-CGk6QFDb.js} +1 -1
- package/dist/web/assets/{http-B-j5imi7.js → http-Dziu_Xj5.js} +1 -1
- package/dist/web/assets/{hurl-EbdE4I5m.js → hurl-DythJo9n.js} +1 -1
- package/dist/web/assets/index-CiOT786B.css +1 -0
- package/dist/web/assets/index-Cm7ENbyj.js +256 -0
- package/dist/web/assets/{info-OHQRW6UA-D8EeGtAZ.js → info-OHQRW6UA-CbxEII--.js} +1 -1
- package/dist/web/assets/{infoDiagram-5W2HQ5XZ-BXakU7Bk.js → infoDiagram-5W2HQ5XZ-uncvednK.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-K3B6WC7H-BjrJYJdZ.js → ishikawaDiagram-K3B6WC7H-DGe07IXX.js} +1 -1
- package/dist/web/assets/{java-DdlG7Knc.js → java-CZZLsDD_.js} +1 -1
- package/dist/web/assets/{javascript-Dqf2fIHF.js → javascript-BcaihfgA.js} +1 -1
- package/dist/web/assets/{jinja-o2b7TNPM.js → jinja-v6gp7mmJ.js} +1 -1
- package/dist/web/assets/{jison-BFaMalob.js → jison-D2SC7D3U.js} +1 -1
- package/dist/web/assets/{journeyDiagram-COXZFDF6-A2kOjbib.js → journeyDiagram-COXZFDF6-DAIUzadY.js} +1 -1
- package/dist/web/assets/{json-rOV04JA5.js → json-BTX5Muj-.js} +1 -1
- package/dist/web/assets/{jsx-f8aDzNJg.js → jsx-sOd5Zb96.js} +1 -1
- package/dist/web/assets/{julia-BEQyLg2u.js → julia-BFKK12Cy.js} +1 -1
- package/dist/web/assets/{just-BQgCkdhV.js → just-5EOII5bf.js} +1 -1
- package/dist/web/assets/{kanban-definition-P3RFRI5V-CoXFy4Ut.js → kanban-definition-P3RFRI5V-CRJ_qKDi.js} +1 -1
- package/dist/web/assets/{latex-Ca5Jf2Jd.js → latex-DlpyBAwB.js} +1 -1
- package/dist/web/assets/{line-C76_QaGr.js → line-DDtZbOq6.js} +1 -1
- package/dist/web/assets/{linear-BuH_Zb4z.js → linear-DiXfgJNJ.js} +1 -1
- package/dist/web/assets/{liquid-CSCRCgF3.js → liquid-i0LOb-_p.js} +1 -1
- package/dist/web/assets/{lua-C4IZzkwc.js → lua-egAglmJR.js} +1 -1
- package/dist/web/assets/{marko-BgEbfzVq.js → marko-pexQ6dyw.js} +1 -1
- package/dist/web/assets/{mdc-DQkkC6Bm.js → mdc-2zx6wVZ1.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-C6bK5onm.js → mermaid-parser.core-ChhS34Wq.js} +3 -3
- package/dist/web/assets/{mermaid.core-BlumzZwI.js → mermaid.core-BLtM-FUH.js} +4 -4
- package/dist/web/assets/{mindmap-definition-LZFPQGKD-DUZ9lkHi.js → mindmap-definition-LZFPQGKD-qHBfvVvR.js} +1 -1
- package/dist/web/assets/{nginx-C-nA8WcZ.js → nginx-BJBqsQfY.js} +1 -1
- package/dist/web/assets/{nim-CdJOam9_.js → nim-BaA50oZ-.js} +1 -1
- package/dist/web/assets/{org-gMZBqtyI.js → org-B-ByT2Q_.js} +1 -1
- package/dist/web/assets/{packet-JDAUHWVQ-zILCc75J.js → packet-JDAUHWVQ-uLD5sbY4.js} +1 -1
- package/dist/web/assets/{pegDiagram-BGZESJAR-MVi1ULP2.js → pegDiagram-BGZESJAR-BRNG5qXG.js} +1 -1
- package/dist/web/assets/{perl-C2-G_qAE.js → perl-DxoQS1Oz.js} +1 -1
- package/dist/web/assets/{php-DTPJPNpf.js → php-DAdurunP.js} +1 -1
- package/dist/web/assets/{pie-XZMESJXO-fQA5AQHi.js → pie-XZMESJXO-rle9Hab0.js} +1 -1
- package/dist/web/assets/{pieDiagram-CAPJLFHJ-BK9l7w7g.js → pieDiagram-CAPJLFHJ-CsLaTTZ4.js} +1 -1
- package/dist/web/assets/{pug-DLUtfK-s.js → pug-CGqTuVm8.js} +1 -1
- package/dist/web/assets/{qml-CRHnLnQC.js → qml-qo_XuOpa.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-GDMTTRAM-Duujn2Nw.js → quadrantDiagram-GDMTTRAM-CcM5DEH6.js} +1 -1
- package/dist/web/assets/{r-BoPR6WG2.js → r-CKiLSGzc.js} +1 -1
- package/dist/web/assets/{radar-ABXABTNO-CKsYvVqz.js → radar-ABXABTNO-CC4F4_aS.js} +1 -1
- package/dist/web/assets/{railroad-I3PHUGI6-B65B3uNN.js → railroad-I3PHUGI6-EiUjcaSq.js} +1 -1
- package/dist/web/assets/{railroad-abnf-LNEFI6M7-3QHUBp4u.js → railroad-abnf-LNEFI6M7-BZzYZu7G.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-FVhXeBSj.js → railroad-ebnf-DWYD2UWJ-DGihz-hz.js} +1 -1
- package/dist/web/assets/{railroad-peg-JR7BVNR7-Dm-KqLjB.js → railroad-peg-JR7BVNR7-D9Youaz5.js} +1 -1
- package/dist/web/assets/{railroadDiagram-SM67HX2B-DF4ZGcIG.js → railroadDiagram-SM67HX2B-ljs2FnSo.js} +1 -1
- package/dist/web/assets/{razor-BuCV9tNu.js → razor-DjN9S-r_.js} +1 -1
- package/dist/web/assets/{regexp-qKTqrnck.js → regexp-yRPnlhcx.js} +1 -1
- package/dist/web/assets/{requirementDiagram-X7JNWC4A-DjRke5ii.js → requirementDiagram-X7JNWC4A-BJ877Yny.js} +1 -1
- package/dist/web/assets/{rst-CRl5lm-Q.js → rst-NbJF2fMO.js} +1 -1
- package/dist/web/assets/{ruby-B9hlbxxT.js → ruby-ZoVZdf-0.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-UM26HJRW-CamOgrS8.js → sankeyDiagram-UM26HJRW-BPwhixv8.js} +1 -1
- package/dist/web/assets/{sas-C8_XUAnK.js → sas-CnTL8nFG.js} +1 -1
- package/dist/web/assets/{scss-CXsBS0VG.js → scss-BAnH3m1c.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-ZO4K6R2Y-B_TGH3Kb.js → sequenceDiagram-ZO4K6R2Y-xhx_vaZj.js} +1 -1
- package/dist/web/assets/{shellscript-DWMstwWF.js → shellscript-ChY3GVRw.js} +1 -1
- package/dist/web/assets/{shellsession-Btxe1tNF.js → shellsession-G4IamFTN.js} +1 -1
- package/dist/web/assets/{soy-mLggCgmX.js → soy-DHZYlzcJ.js} +1 -1
- package/dist/web/assets/{sql-CQWaF3T9.js → sql-BLAJpvko.js} +1 -1
- package/dist/web/assets/{src-C7grwUMY.js → src-CneWU0V7.js} +1 -1
- package/dist/web/assets/{stata-CuWbLp-h.js → stata-BF2DeqJI.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-TBUQTH76-BZ8S3qm5.js → stateDiagram-v2-TBUQTH76-DYEIdXmD.js} +1 -1
- package/dist/web/assets/{surrealql-C-vm0daZ.js → surrealql-CjuctJtE.js} +1 -1
- package/dist/web/assets/{svelte-CtDkvgD0.js → svelte-CAAipNMl.js} +1 -1
- package/dist/web/assets/{swimlanes-N4OXWK64-BfATBQI8.js → swimlanes-N4OXWK64-DaNmUIcz.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-K3J5GTZL-Cc_r8_vm.js +8 -0
- package/dist/web/assets/{templ-H8UIFth7.js → templ-B-6t84-k.js} +1 -1
- package/dist/web/assets/{tex-CkbKsWZ8.js → tex-DsSWxSFa.js} +1 -1
- package/dist/web/assets/{timeline-definition-YOQAKGHF-u2i5XlTn.js → timeline-definition-YOQAKGHF-CmA5uuSJ.js} +1 -1
- package/dist/web/assets/{treeView-D4JQ5SDB-CqQ4lze0.js → treeView-D4JQ5SDB-L8cwVgaN.js} +1 -1
- package/dist/web/assets/{treemap-SAJKECNS-C8LB5E0F.js → treemap-SAJKECNS-Cb56YgJH.js} +1 -1
- package/dist/web/assets/{ts-tags-Cu0aDIzl.js → ts-tags-2bkGTXWr.js} +1 -1
- package/dist/web/assets/{tsx-CAExLcKf.js → tsx-BD6V4u7j.js} +1 -1
- package/dist/web/assets/{twig-CsER03l2.js → twig-BRdwPHsd.js} +1 -1
- package/dist/web/assets/{typescript-5sYbFyp_.js → typescript-CE43_TcG.js} +1 -1
- package/dist/web/assets/{typst-xTMLS2uO.js → typst-Oj4lknbm.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-VIAY4XPW-DkNC7sDP.js → usecaseDiagram-VIAY4XPW-RzU3uMX7.js} +1 -1
- package/dist/web/assets/{vennDiagram-BLWOH2XV-jyKf3LAy.js → vennDiagram-BLWOH2XV-RBYC_TER.js} +1 -1
- package/dist/web/assets/{vue-COaEYbeF.js → vue-DohRCbuk.js} +1 -1
- package/dist/web/assets/{vue-html-C0-x_Zbs.js → vue-html-CUm-vXDJ.js} +1 -1
- package/dist/web/assets/{vue-vine-iVnGh9vX.js → vue-vine-CSJNVT6g.js} +1 -1
- package/dist/web/assets/{wardley-7MLQ67FV-BNNco9TK.js → wardley-7MLQ67FV-BSCtc6D6.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YMQ3BMBF-B7qnVbxX.js → wardleyDiagram-YMQ3BMBF-DiJSX_eB.js} +1 -1
- package/dist/web/assets/{xml-DCmPPgCS.js → xml-CRGrymgs.js} +1 -1
- package/dist/web/assets/{xsl-C95o3Y65.js → xsl-5ujj8eCf.js} +1 -1
- package/dist/web/assets/{xychartDiagram-TAQBALBS-ByDg_iLk.js → xychartDiagram-TAQBALBS-BmlAXIhs.js} +1 -1
- package/dist/web/assets/{yaml-6uUvTVqV.js → yaml-DYq2KMMJ.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/licences.md +1 -1
- package/package.json +4 -4
- package/dist/web/assets/channel-D96j5RGp.js +0 -1
- package/dist/web/assets/flowDiagram-OXPTDLAJ-BBtXQXD5.js +0 -1
- package/dist/web/assets/index-D90DSXXX.css +0 -1
- package/dist/web/assets/index-DBPwT0qJ.js +0 -256
- package/dist/web/assets/swimlanesDiagram-K3J5GTZL-DqMMLgX3.js +0 -8
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import os from "node:os";
|
|
10
|
+
import { parseDocument } from "yaml";
|
|
10
11
|
import openInBrowser from "open";
|
|
11
12
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
12
13
|
import { createServer } from "node:http";
|
|
@@ -43,7 +44,7 @@ function createProcessTerminal() {
|
|
|
43
44
|
//#region src/server/runtime/packageVersion.ts
|
|
44
45
|
var packageVersion = {
|
|
45
46
|
name: "@krelborn/markdown-review",
|
|
46
|
-
version: "0.
|
|
47
|
+
version: "0.4.0",
|
|
47
48
|
description: "Review agent-written markdown in the browser and hand the comments back to the agent",
|
|
48
49
|
homepage: "https://github.com/Krelborn/markdown-review#readme",
|
|
49
50
|
bugs: { "url": "https://github.com/Krelborn/markdown-review/issues" },
|
|
@@ -82,11 +83,12 @@ var packageVersion = {
|
|
|
82
83
|
"hono": "^4.13.13",
|
|
83
84
|
"markdown-it": "^15.0.2",
|
|
84
85
|
"open": "^11.0.4",
|
|
86
|
+
"yaml": "^2.9.1",
|
|
85
87
|
"zod": "^4.6.5"
|
|
86
88
|
},
|
|
87
89
|
devDependencies: {
|
|
88
90
|
"@babel/core": "^8.0.7",
|
|
89
|
-
"@krelborn/stylesui": "^0.
|
|
91
|
+
"@krelborn/stylesui": "^0.7.0",
|
|
90
92
|
"@playwright/test": "^1.64.0",
|
|
91
93
|
"@rolldown/plugin-babel": "^0.2.4",
|
|
92
94
|
"@testing-library/dom": "^10.4.2",
|
|
@@ -113,8 +115,7 @@ var packageVersion = {
|
|
|
113
115
|
"shiki": "^4.5.0",
|
|
114
116
|
"typescript": "^7.0.2",
|
|
115
117
|
"vite": "^8.3.3",
|
|
116
|
-
"vitest": "^5.0.3"
|
|
117
|
-
"yaml": "^2.9.1"
|
|
118
|
+
"vitest": "^5.0.3"
|
|
118
119
|
},
|
|
119
120
|
engines: { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
|
|
120
121
|
packageManager: "pnpm@10.18.3"
|
|
@@ -292,6 +293,24 @@ var anchorSchema = z.discriminatedUnion("kind", [
|
|
|
292
293
|
passageAnchorSchema
|
|
293
294
|
]);
|
|
294
295
|
//#endregion
|
|
296
|
+
//#region src/shared/review/reviewRoundSchema.ts
|
|
297
|
+
var verdictSchema = z.enum([
|
|
298
|
+
"revise",
|
|
299
|
+
"go-ahead",
|
|
300
|
+
"hand-back"
|
|
301
|
+
]);
|
|
302
|
+
var nextTaskSchema = z.string().trim().min(1).max(200).refine((text) => !/[\n\r\u2028\u2029]/.test(text), "must be one line");
|
|
303
|
+
var reviewRoundSchema = z.strictObject({
|
|
304
|
+
documents: z.array(documentPathSchema),
|
|
305
|
+
lastOpenedAt: z.iso.datetime(),
|
|
306
|
+
nextTask: nextTaskSchema.nullable(),
|
|
307
|
+
startedAt: z.iso.datetime(),
|
|
308
|
+
verdict: z.strictObject({
|
|
309
|
+
at: z.iso.datetime(),
|
|
310
|
+
kind: verdictSchema
|
|
311
|
+
}).nullable()
|
|
312
|
+
});
|
|
313
|
+
//#endregion
|
|
295
314
|
//#region src/shared/review/threadSchema.ts
|
|
296
315
|
var messageBodySchema = z.string().refine((body) => body.trim() !== "", "must not be blank");
|
|
297
316
|
var messageSchema = z.strictObject({
|
|
@@ -318,11 +337,6 @@ var threadSchema = z.strictObject({
|
|
|
318
337
|
}).refine((thread) => thread.status === "draft" === (thread.messages.length === 0), { message: "must have messages unless it is a draft" }).refine((thread) => thread.status !== "draft" || thread.draft !== void 0, { message: "must hold its comment as a draft while it is a draft" });
|
|
319
338
|
//#endregion
|
|
320
339
|
//#region src/shared/api/apiResponseSchemas.ts
|
|
321
|
-
var reviewStateSchema = z.strictObject({
|
|
322
|
-
approved: z.boolean(),
|
|
323
|
-
approvedAt: z.iso.datetime().nullable(),
|
|
324
|
-
requestedAt: z.iso.datetime().nullable()
|
|
325
|
-
});
|
|
326
340
|
/**
|
|
327
341
|
* Not strict, so a newer server that reports more fields is still recognised by its protocol rather than mistaken for
|
|
328
342
|
* no server
|
|
@@ -336,13 +350,13 @@ var healthSchema = z.object({
|
|
|
336
350
|
});
|
|
337
351
|
var threadsSnapshotSchema = z.strictObject({
|
|
338
352
|
problems: z.array(z.string()),
|
|
339
|
-
|
|
353
|
+
round: reviewRoundSchema.nullable(),
|
|
340
354
|
threads: z.array(threadSchema)
|
|
341
355
|
});
|
|
342
356
|
var pollResponseSchema = threadsSnapshotSchema.extend({ timedOut: z.boolean() });
|
|
343
357
|
var agentOpenResponseSchema = z.strictObject({
|
|
344
358
|
navigated: z.boolean(),
|
|
345
|
-
|
|
359
|
+
round: reviewRoundSchema.nullable(),
|
|
346
360
|
url: z.string()
|
|
347
361
|
});
|
|
348
362
|
var agentThreadResponseSchema = z.strictObject({
|
|
@@ -364,6 +378,10 @@ z.strictObject({
|
|
|
364
378
|
path: documentPathSchema,
|
|
365
379
|
source: z.string()
|
|
366
380
|
});
|
|
381
|
+
z.strictObject({
|
|
382
|
+
availableVersion: z.string().nullable(),
|
|
383
|
+
command: z.string().nullable()
|
|
384
|
+
});
|
|
367
385
|
var apiErrorSchema = z.strictObject({ error: z.strictObject({
|
|
368
386
|
message: z.string(),
|
|
369
387
|
reason: z.string()
|
|
@@ -402,14 +420,22 @@ var ServerClient = class {
|
|
|
402
420
|
health() {
|
|
403
421
|
return this.request("GET", "/api/health", healthSchema, { signal: AbortSignal.timeout(healthTimeoutMilliseconds) });
|
|
404
422
|
}
|
|
405
|
-
|
|
406
|
-
|
|
423
|
+
/**
|
|
424
|
+
* @param document the doc to show, or null for the docs list
|
|
425
|
+
* @param nextTask what the agent will do once the user says go ahead; null keeps a joined round's task and leaves a
|
|
426
|
+
* new round without one
|
|
427
|
+
*/
|
|
428
|
+
open(document, nextTask) {
|
|
429
|
+
return this.request("POST", "/api/agent/open", agentOpenResponseSchema, { body: {
|
|
430
|
+
...document === null ? {} : { path: document },
|
|
431
|
+
...nextTask === null ? {} : { nextTask }
|
|
432
|
+
} });
|
|
407
433
|
}
|
|
408
434
|
inbox(document) {
|
|
409
435
|
return this.request("GET", `/api/inbox${documentQuery(document)}`, threadsSnapshotSchema);
|
|
410
436
|
}
|
|
411
437
|
/**
|
|
412
|
-
* Waits for the
|
|
438
|
+
* Waits for the user to end the review round, a thread to need the agent, or the timeout
|
|
413
439
|
*/
|
|
414
440
|
poll(document, timeoutSeconds, signal) {
|
|
415
441
|
const query = new URLSearchParams({ timeout: String(timeoutSeconds) });
|
|
@@ -480,11 +506,11 @@ async function findServer(root) {
|
|
|
480
506
|
return { kind: "absent" };
|
|
481
507
|
}
|
|
482
508
|
if (health.root !== root) return { kind: "absent" };
|
|
483
|
-
if (health.protocol <
|
|
509
|
+
if (health.protocol < 2) return {
|
|
484
510
|
client,
|
|
485
511
|
kind: "older"
|
|
486
512
|
};
|
|
487
|
-
return health.protocol >
|
|
513
|
+
return health.protocol > 2 ? {
|
|
488
514
|
health,
|
|
489
515
|
kind: "newer"
|
|
490
516
|
} : {
|
|
@@ -661,6 +687,18 @@ async function findRoot(directory, outsideGitDirectory = directory) {
|
|
|
661
687
|
}
|
|
662
688
|
}
|
|
663
689
|
//#endregion
|
|
690
|
+
//#region src/shared/review/roundOutcome.ts
|
|
691
|
+
/**
|
|
692
|
+
* Tells how the user ended a review round
|
|
693
|
+
*
|
|
694
|
+
* @param round the round, or null before any review was opened
|
|
695
|
+
* @returns the verdict that ended the round, or null while it goes on
|
|
696
|
+
*/
|
|
697
|
+
function roundOutcome(round) {
|
|
698
|
+
const kind = round?.verdict?.kind;
|
|
699
|
+
return kind === "go-ahead" || kind === "hand-back" ? kind : null;
|
|
700
|
+
}
|
|
701
|
+
//#endregion
|
|
664
702
|
//#region src/cli/formatIdList.ts
|
|
665
703
|
/**
|
|
666
704
|
* Lists thread IDs the way the CLI's sentences name them
|
|
@@ -684,7 +722,9 @@ var waitForCommentsStep = `Run \`markdown-review poll\` to wait for the user's c
|
|
|
684
722
|
function waitForCommentsInRootStep(root) {
|
|
685
723
|
return `This doc belongs to the repository at ${root}, so run markdown-review commands from that directory. ` + waitForCommentsStep;
|
|
686
724
|
}
|
|
687
|
-
var
|
|
725
|
+
var wentAheadStep = "The user said go ahead. Carry on with your task; do not run `markdown-review poll` again for this review.";
|
|
726
|
+
var handedBackStep = "Stop here: do not edit the docs, run `markdown-review poll` or carry on with your task. Tell the user in the chat that you have stopped, and wait for their instructions.";
|
|
727
|
+
var handedBackFinishedStep = "The user handed the review back. Tell them what you changed and wait for their instructions. Only if they ask you to continue the review, run `markdown-review open <path> --next-task \"<what you will do once the user says go ahead>\"`, then `markdown-review poll`.";
|
|
688
728
|
/**
|
|
689
729
|
* @param ids the threads that need the agent, at least one
|
|
690
730
|
*/
|
|
@@ -692,11 +732,23 @@ function addressThreadsStep(ids) {
|
|
|
692
732
|
return `Edit the docs, then run \`markdown-review resolve <id> "<what changed>"\` for ${formatIdList(ids)}, or \`markdown-review reply <id> "<question>"\` if you need input. Then run \`markdown-review poll\` to wait for the next round.`;
|
|
693
733
|
}
|
|
694
734
|
/**
|
|
695
|
-
* @param ids the threads the user commented on when
|
|
735
|
+
* @param ids the threads the user commented on when saying go ahead, at least one
|
|
696
736
|
*/
|
|
697
|
-
function
|
|
737
|
+
function wentAheadWithCommentsStep(ids) {
|
|
698
738
|
const resolveCommand = ids.length === 1 ? `\`markdown-review resolve ${ids[0]} "<what changed>"\`` : "`markdown-review resolve <id> \"<what changed>\"` for each";
|
|
699
|
-
return `The user
|
|
739
|
+
return `The user said go ahead, with comments. Address ${formatIdList(ids)} and run ${resolveCommand}, then carry on with your task; do not run \`markdown-review poll\` again for this review.`;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* @param ids the threads still needing the agent after the user handed the review back, at least one
|
|
743
|
+
*/
|
|
744
|
+
function handedBackThreadsLeftStep(ids) {
|
|
745
|
+
return `The user handed the review back, so act on ${formatIdList(ids)} only as they ask in the chat.`;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* @param ids the threads the user commented on when handing the review back, at least one
|
|
749
|
+
*/
|
|
750
|
+
function handedBackWithCommentsStep(ids) {
|
|
751
|
+
return `Stop here: do not edit the docs, resolve or reply to ${formatIdList(ids)}, run \`markdown-review poll\` or carry on with your task. Tell the user in the chat that you have stopped and what their comments say, and wait for their instructions.`;
|
|
700
752
|
}
|
|
701
753
|
//#endregion
|
|
702
754
|
//#region src/cli/formatAgentAction.ts
|
|
@@ -709,10 +761,21 @@ function approvedWithCommentsStep(ids) {
|
|
|
709
761
|
*/
|
|
710
762
|
function formatAgentAction(action, { inbox, thread }) {
|
|
711
763
|
const done = action === "resolved" ? `Resolved #${thread.id}.` : `Replied to #${thread.id}; it waits for the user.`;
|
|
764
|
+
const outcome = roundOutcome(inbox.round);
|
|
712
765
|
const remaining = inbox.threads.map((remainingThread) => remainingThread.id);
|
|
713
|
-
if (remaining.length > 0)
|
|
714
|
-
`;
|
|
715
|
-
|
|
766
|
+
if (remaining.length > 0) {
|
|
767
|
+
const count = remaining.length === 1 ? "1 thread still needs you" : `${remaining.length} threads still need you`;
|
|
768
|
+
const step = outcome === "hand-back" ? handedBackThreadsLeftStep(remaining) : `Address ${formatIdList(remaining)}, then run \`markdown-review resolve <id> "<what changed>"\`, or \`markdown-review reply <id> "<question>"\` if you need input.`;
|
|
769
|
+
return `${done}\n${count}: ${formatIdList(remaining)}.\n\nnext_step: ${step}\n`;
|
|
770
|
+
}
|
|
771
|
+
return `${done} No threads need you now.\n\nnext_step: ${finishedStep(outcome)}\n`;
|
|
772
|
+
}
|
|
773
|
+
function finishedStep(outcome) {
|
|
774
|
+
switch (outcome) {
|
|
775
|
+
case "go-ahead": return wentAheadStep;
|
|
776
|
+
case "hand-back": return handedBackFinishedStep;
|
|
777
|
+
case null: return "Run `markdown-review poll` to wait for the user's next round of comments.";
|
|
778
|
+
}
|
|
716
779
|
}
|
|
717
780
|
//#endregion
|
|
718
781
|
//#region src/cli/readMessageBody.ts
|
|
@@ -792,16 +855,20 @@ var quoteStartLength = 120;
|
|
|
792
855
|
/**
|
|
793
856
|
* Describes the agent's inbox as compact text for the agent to read
|
|
794
857
|
*
|
|
795
|
-
* @param snapshot the review
|
|
796
|
-
* @
|
|
858
|
+
* @param snapshot the review round and the threads that need the agent
|
|
859
|
+
* @param notices lines to put just before `next_step`, each ending in a newline
|
|
860
|
+
* @returns the summary, which says how the user ended the round if they have, each thread grouped by doc with its
|
|
861
|
+
* messages, any store problems, the notices, and a final `next_step`
|
|
797
862
|
*/
|
|
798
|
-
function formatInbox({ problems,
|
|
863
|
+
function formatInbox({ problems, round, threads }, notices = []) {
|
|
864
|
+
const outcome = roundOutcome(round);
|
|
799
865
|
const ordered = orderByDocument(threads);
|
|
800
866
|
const ids = ordered.map((thread) => thread.id);
|
|
801
|
-
const sections = [summaryLine(
|
|
867
|
+
const sections = [summaryLine(outcome, ordered)];
|
|
802
868
|
if (ordered.length > 0) sections.push(ordered.map(formatThread).join("\n\n"));
|
|
803
869
|
if (problems.length > 0) sections.push(problems.map((problem) => `warning: ${indentContinuation(problem, " ")}`).join("\n"));
|
|
804
|
-
sections.push(
|
|
870
|
+
if (notices.length > 0) sections.push(notices.join("").trimEnd());
|
|
871
|
+
sections.push(`next_step: ${nextStep(outcome, ids)}`);
|
|
805
872
|
return `${sections.join("\n\n")}\n`;
|
|
806
873
|
}
|
|
807
874
|
function formatThread(thread) {
|
|
@@ -828,20 +895,40 @@ function describeAnchor({ anchor, id }) {
|
|
|
828
895
|
}
|
|
829
896
|
}
|
|
830
897
|
}
|
|
831
|
-
function summaryLine(
|
|
832
|
-
if (threads.length === 0) return
|
|
898
|
+
function summaryLine(outcome, threads) {
|
|
899
|
+
if (threads.length === 0) return noThreadsSummary(outcome);
|
|
900
|
+
const breakdown = countByGroup(threads);
|
|
901
|
+
const needs = threads.length === 1 ? "1 thread needs you" : `${threads.length} threads need you`;
|
|
902
|
+
switch (outcome) {
|
|
903
|
+
case "go-ahead": return `The user said go ahead, ${needs} (${breakdown})`;
|
|
904
|
+
case "hand-back": return `The user asked to hand back to you, with comments on ${countThreads(threads.length)} (${breakdown})`;
|
|
905
|
+
case null: return `${needs} (${breakdown})`;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function countByGroup(threads) {
|
|
833
909
|
const counts = /* @__PURE__ */ new Map();
|
|
834
910
|
for (const thread of threads) {
|
|
835
911
|
const group = thread.anchor.kind === "review" ? "review" : thread.anchor.document;
|
|
836
912
|
counts.set(group, (counts.get(group) ?? 0) + 1);
|
|
837
913
|
}
|
|
838
|
-
|
|
839
|
-
const needs = threads.length === 1 ? "1 thread needs you" : `${threads.length} threads need you`;
|
|
840
|
-
return approved ? `Review approved, ${needs} (${breakdown})` : `${needs} (${breakdown})`;
|
|
914
|
+
return [...counts].map(([group, count]) => `${group}: ${count}`).join(", ");
|
|
841
915
|
}
|
|
842
|
-
function
|
|
843
|
-
|
|
844
|
-
|
|
916
|
+
function countThreads(count) {
|
|
917
|
+
return count === 1 ? "1 thread" : `${count} threads`;
|
|
918
|
+
}
|
|
919
|
+
function noThreadsSummary(outcome) {
|
|
920
|
+
switch (outcome) {
|
|
921
|
+
case "go-ahead": return "The user said go ahead. No threads need you.";
|
|
922
|
+
case "hand-back": return "The user asked to hand back to you. No threads need you.";
|
|
923
|
+
case null: return "No threads need you.";
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function nextStep(outcome, ids) {
|
|
927
|
+
switch (outcome) {
|
|
928
|
+
case "go-ahead": return ids.length === 0 ? wentAheadStep : wentAheadWithCommentsStep(ids);
|
|
929
|
+
case "hand-back": return ids.length === 0 ? handedBackStep : handedBackWithCommentsStep(ids);
|
|
930
|
+
case null: return ids.length === 0 ? waitForCommentsStep : addressThreadsStep(ids);
|
|
931
|
+
}
|
|
845
932
|
}
|
|
846
933
|
function orderByDocument(threads) {
|
|
847
934
|
const groupOf = (thread) => thread.anchor.kind === "review" ? "" : thread.anchor.document;
|
|
@@ -882,21 +969,319 @@ async function readDocumentOption(root, workingDirectory, value) {
|
|
|
882
969
|
return value === void 0 ? null : toRepositoryPath(root, path.resolve(await realpath(workingDirectory), value));
|
|
883
970
|
}
|
|
884
971
|
//#endregion
|
|
972
|
+
//#region src/server/update/semanticVersions.ts
|
|
973
|
+
var versionPattern = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
974
|
+
/**
|
|
975
|
+
* Orders two semantic versions by semver precedence
|
|
976
|
+
*
|
|
977
|
+
* @param left a version such as "0.4.0" or "0.4.0-beta.1"
|
|
978
|
+
* @param right another version
|
|
979
|
+
* @returns a negative number when `left` comes first, a positive number when `right` does, 0 when they are equal,
|
|
980
|
+
* and null when either is not a semantic version
|
|
981
|
+
*/
|
|
982
|
+
function compareVersions(left, right) {
|
|
983
|
+
const parsedLeft = parseVersion(left);
|
|
984
|
+
const parsedRight = parseVersion(right);
|
|
985
|
+
if (parsedLeft === null || parsedRight === null) return null;
|
|
986
|
+
const [leftMajor, leftMinor, leftPatch] = parsedLeft.release;
|
|
987
|
+
const [rightMajor, rightMinor, rightPatch] = parsedRight.release;
|
|
988
|
+
return leftMajor - rightMajor || leftMinor - rightMinor || leftPatch - rightPatch || comparePrereleases(parsedLeft.prerelease, parsedRight.prerelease);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* @param version a version such as "0.4.0-beta.1"
|
|
992
|
+
* @returns whether it is a semantic version with a pre-release part
|
|
993
|
+
*/
|
|
994
|
+
function isPrerelease(version) {
|
|
995
|
+
return (parseVersion(version)?.prerelease.length ?? 0) > 0;
|
|
996
|
+
}
|
|
997
|
+
function parseVersion(version) {
|
|
998
|
+
const groups = versionPattern.exec(version)?.groups;
|
|
999
|
+
if (groups === void 0) return null;
|
|
1000
|
+
return {
|
|
1001
|
+
prerelease: groups.prerelease?.split(".") ?? [],
|
|
1002
|
+
release: [
|
|
1003
|
+
Number(groups.major),
|
|
1004
|
+
Number(groups.minor),
|
|
1005
|
+
Number(groups.patch)
|
|
1006
|
+
]
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function comparePrereleases(left, right) {
|
|
1010
|
+
if (left.length === 0 || right.length === 0) return right.length - left.length;
|
|
1011
|
+
for (const [i, leftIdentifier] of left.entries()) {
|
|
1012
|
+
const rightIdentifier = right[i];
|
|
1013
|
+
if (rightIdentifier === void 0) break;
|
|
1014
|
+
const difference = compareIdentifiers(leftIdentifier, rightIdentifier);
|
|
1015
|
+
if (difference !== 0) return difference;
|
|
1016
|
+
}
|
|
1017
|
+
return left.length - right.length;
|
|
1018
|
+
}
|
|
1019
|
+
function compareIdentifiers(left, right) {
|
|
1020
|
+
const leftIsNumeric = /^\d+$/u.test(left);
|
|
1021
|
+
const rightIsNumeric = /^\d+$/u.test(right);
|
|
1022
|
+
if (leftIsNumeric && rightIsNumeric) return Number(left) - Number(right);
|
|
1023
|
+
if (leftIsNumeric !== rightIsNumeric) return leftIsNumeric ? -1 : 1;
|
|
1024
|
+
if (left === right) return 0;
|
|
1025
|
+
return left < right ? -1 : 1;
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/server/update/findAvailableUpdate.ts
|
|
1029
|
+
/**
|
|
1030
|
+
* Picks the published version to offer in place of the running one
|
|
1031
|
+
*
|
|
1032
|
+
* @param current the running version
|
|
1033
|
+
* @param distTags the registry's `latest` and `next` tags
|
|
1034
|
+
* @returns the version to offer and its dist-tag, or null when none is newer or a version cannot be read
|
|
1035
|
+
*/
|
|
1036
|
+
function findAvailableUpdate(current, distTags) {
|
|
1037
|
+
const distTagNames = isPrerelease(current) ? ["latest", "next"] : ["latest"];
|
|
1038
|
+
let best = null;
|
|
1039
|
+
for (const distTag of distTagNames) {
|
|
1040
|
+
const version = distTags[distTag];
|
|
1041
|
+
if (version !== void 0 && (compareVersions(version, best?.version ?? current) ?? 0) > 0) best = {
|
|
1042
|
+
distTag,
|
|
1043
|
+
version
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
return best;
|
|
1047
|
+
}
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src/server/update/installCommands.ts
|
|
1050
|
+
/**
|
|
1051
|
+
* @param cliPath the real path of the running `dist/cli.js`
|
|
1052
|
+
* @returns how that copy of markdown-review was installed
|
|
1053
|
+
*/
|
|
1054
|
+
function findInstallKind(cliPath) {
|
|
1055
|
+
return cliPath.split(path.sep).includes("_npx") ? "npx" : "global";
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* @param versionOrTag the version or dist-tag to run, e.g. "latest" or "0.4.0"
|
|
1059
|
+
* @returns the npx command that runs that markdown-review
|
|
1060
|
+
*/
|
|
1061
|
+
function npxCommand(versionOrTag) {
|
|
1062
|
+
return `npx @krelborn/markdown-review@${versionOrTag}`;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* @param kind how markdown-review is installed
|
|
1066
|
+
* @param args the command and its arguments, e.g. "install-skill --global"
|
|
1067
|
+
* @param versionOrTag for npx, the version or dist-tag to run; a global install runs whichever is installed
|
|
1068
|
+
* @returns the shell command that runs markdown-review with those arguments
|
|
1069
|
+
*/
|
|
1070
|
+
function cliCommand(kind, args, versionOrTag) {
|
|
1071
|
+
return `${kind === "npx" ? npxCommand(versionOrTag) : "markdown-review"} ${args}`;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* @param distTag the dist-tag to install, e.g. "latest"
|
|
1075
|
+
* @returns the shell command that updates a global install to that dist-tag's version
|
|
1076
|
+
*/
|
|
1077
|
+
function globalInstallCommand(distTag) {
|
|
1078
|
+
return `npm install --global @krelborn/markdown-review@${distTag}`;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* @param distTag the dist-tag to install, e.g. "latest"
|
|
1082
|
+
* @param root the repo whose review server is running the old version
|
|
1083
|
+
* @returns a POSIX shell command that updates a global install, then stops that repo's server from wherever it is run
|
|
1084
|
+
*/
|
|
1085
|
+
function globalUpdateCommand(distTag, root) {
|
|
1086
|
+
return `${globalInstallCommand(distTag)} && (cd ${quoteForShell(root)} && markdown-review stop)`;
|
|
1087
|
+
}
|
|
1088
|
+
function quoteForShell(text) {
|
|
1089
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
1090
|
+
}
|
|
1091
|
+
//#endregion
|
|
1092
|
+
//#region src/server/update/updateCheckFileSchema.ts
|
|
1093
|
+
/**
|
|
1094
|
+
* The registry's dist-tags that matter here; any others are dropped
|
|
1095
|
+
*/
|
|
1096
|
+
var distTagsSchema = z.object({
|
|
1097
|
+
latest: z.string().optional(),
|
|
1098
|
+
next: z.string().optional()
|
|
1099
|
+
});
|
|
1100
|
+
/**
|
|
1101
|
+
* The user's update check cache: when the registry was last asked, and the dist-tags it last gave
|
|
1102
|
+
*/
|
|
1103
|
+
var updateCheckFileSchema = z.strictObject({
|
|
1104
|
+
checkedAt: z.iso.datetime(),
|
|
1105
|
+
distTags: distTagsSchema
|
|
1106
|
+
});
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region src/server/update/readUpdateCheckFile.ts
|
|
1109
|
+
/**
|
|
1110
|
+
* Reads the user's update check cache
|
|
1111
|
+
*
|
|
1112
|
+
* @param filePath the cache file
|
|
1113
|
+
* @returns the cache, or null when it is missing, unreadable or invalid
|
|
1114
|
+
*/
|
|
1115
|
+
async function readUpdateCheckFile(filePath) {
|
|
1116
|
+
try {
|
|
1117
|
+
const result = await readStoreFile(filePath, updateCheckFileSchema);
|
|
1118
|
+
return result.kind === "valid" ? result.value : null;
|
|
1119
|
+
} catch {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
//#endregion
|
|
1124
|
+
//#region src/server/update/updateCheckSettings.ts
|
|
1125
|
+
var defaultRegistryUrl = "https://registry.npmjs.org";
|
|
1126
|
+
/**
|
|
1127
|
+
* @param env the environment, for `XDG_CACHE_HOME` and `HOME`
|
|
1128
|
+
* @returns the user's update check cache file, shared by every repo
|
|
1129
|
+
*/
|
|
1130
|
+
function updateCheckFilePath(env) {
|
|
1131
|
+
const cacheHome = env.XDG_CACHE_HOME;
|
|
1132
|
+
const cacheDirectory = cacheHome !== void 0 && path.isAbsolute(cacheHome) ? cacheHome : path.join(env.HOME ?? os.homedir(), ".cache");
|
|
1133
|
+
return path.join(cacheDirectory, "markdown-review", "update-check.json");
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* @param env the environment, for `MARKDOWN_REVIEW_REGISTRY_URL`
|
|
1137
|
+
* @returns the npm registry to ask for dist-tags, without a trailing slash
|
|
1138
|
+
*/
|
|
1139
|
+
function registryUrl(env) {
|
|
1140
|
+
return (env.MARKDOWN_REVIEW_REGISTRY_URL ?? defaultRegistryUrl).replace(/\/+$/u, "");
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* @param env the environment
|
|
1144
|
+
* @returns whether the user or CI has opted out of update checks, through `MARKDOWN_REVIEW_NO_UPDATE_CHECK=1`,
|
|
1145
|
+
* `NO_UPDATE_NOTIFIER` or `CI`
|
|
1146
|
+
*/
|
|
1147
|
+
function updateChecksDisabled(env) {
|
|
1148
|
+
return env.MARKDOWN_REVIEW_NO_UPDATE_CHECK === "1" || (env.NO_UPDATE_NOTIFIER ?? "") !== "" || (env.CI ?? "") !== "";
|
|
1149
|
+
}
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/cli/skillInstallPath.ts
|
|
1152
|
+
/**
|
|
1153
|
+
* @param base the repo root, or the home directory for a global install
|
|
1154
|
+
* @returns where `install-skill` writes the skill under that directory
|
|
1155
|
+
*/
|
|
1156
|
+
function skillInstallPath(base) {
|
|
1157
|
+
return path.join(base, ".claude", "skills", "markdown-review", "SKILL.md");
|
|
1158
|
+
}
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/cli/skillStamps.ts
|
|
1161
|
+
var frontmatterPattern = /^---\r?\n(?<frontmatter>[\s\S]*?\r?\n)---\r?\n/u;
|
|
1162
|
+
/**
|
|
1163
|
+
* @param skill the bundled skill
|
|
1164
|
+
* @param version the running markdown-review's version
|
|
1165
|
+
* @returns the skill as `install-skill` writes it, with the version that wrote it in its frontmatter's
|
|
1166
|
+
* `metadata.version`
|
|
1167
|
+
* @throws when the skill has no frontmatter
|
|
1168
|
+
*/
|
|
1169
|
+
function stampSkill(skill, version) {
|
|
1170
|
+
const match = frontmatterPattern.exec(skill);
|
|
1171
|
+
const frontmatter = match?.groups?.frontmatter;
|
|
1172
|
+
if (match === null || frontmatter === void 0) throw new TypeError("The skill has no frontmatter to stamp");
|
|
1173
|
+
const document = parseDocument(frontmatter);
|
|
1174
|
+
document.setIn(["metadata", "version"], version);
|
|
1175
|
+
return `---\n${document.toString({ lineWidth: 0 })}---\n${skill.slice(match[0].length)}`;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* @param installed an installed copy of the skill
|
|
1179
|
+
* @returns the version that installed it, or null when the copy carries none
|
|
1180
|
+
*/
|
|
1181
|
+
function readSkillStamp(installed) {
|
|
1182
|
+
const frontmatter = frontmatterPattern.exec(installed)?.groups?.frontmatter;
|
|
1183
|
+
if (frontmatter === void 0) return null;
|
|
1184
|
+
const version = parseDocument(frontmatter).getIn(["metadata", "version"]);
|
|
1185
|
+
return typeof version === "string" ? version : null;
|
|
1186
|
+
}
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region src/cli/findInstalledSkills.ts
|
|
1189
|
+
/**
|
|
1190
|
+
* Finds the copies of the skill installed in the repo and in the home directory
|
|
1191
|
+
*
|
|
1192
|
+
* @param root the repo root
|
|
1193
|
+
* @param home the home directory
|
|
1194
|
+
* @returns each installed copy, the repo's first; a copy that cannot be read counts as not installed
|
|
1195
|
+
*/
|
|
1196
|
+
async function findInstalledSkills(root, home) {
|
|
1197
|
+
const locations = [{
|
|
1198
|
+
isGlobal: false,
|
|
1199
|
+
path: skillInstallPath(root)
|
|
1200
|
+
}, {
|
|
1201
|
+
isGlobal: true,
|
|
1202
|
+
path: skillInstallPath(home)
|
|
1203
|
+
}];
|
|
1204
|
+
const installed = [];
|
|
1205
|
+
for (const location of locations) {
|
|
1206
|
+
const contents = await readTextFileOrNull(location.path).catch(() => null);
|
|
1207
|
+
if (contents !== null) installed.push({
|
|
1208
|
+
...location,
|
|
1209
|
+
version: readSkillStamp(contents)
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
return installed;
|
|
1213
|
+
}
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region src/cli/formatUpdateNotices.ts
|
|
1216
|
+
/**
|
|
1217
|
+
* Describes a newer markdown-review and outdated copies of the skill for the agent
|
|
1218
|
+
*
|
|
1219
|
+
* @returns the `update_available` and `skill_outdated` lines, each ending in a newline; empty when there is nothing to say
|
|
1220
|
+
*/
|
|
1221
|
+
function formatUpdateNotices({ availableUpdate, currentVersion, installKind, skills, wentAhead }) {
|
|
1222
|
+
if (availableUpdate !== null) {
|
|
1223
|
+
const news = `update_available: markdown-review ${availableUpdate.version} is out; this is ${currentVersion}.`;
|
|
1224
|
+
if (!wentAhead) return [`${news} Tell the user, and offer to update once they say go ahead.\n`];
|
|
1225
|
+
return [`${news} Ask the user whether to update. If they agree, ${describeUpdate(installKind, availableUpdate, skills)}\n`];
|
|
1226
|
+
}
|
|
1227
|
+
const when = wentAhead ? "Run" : "Once the user says go ahead, run";
|
|
1228
|
+
return skills.filter((skill) => isOlderThan(skill, currentVersion)).map((skill) => `skill_outdated: The markdown-review skill at ${skill.path} is older than this version (${currentVersion}). ${when} \`${installSkillCommand$1(installKind, currentVersion, skill)}\`. The new skill loads in the next agent session.\n`);
|
|
1229
|
+
}
|
|
1230
|
+
function describeUpdate(installKind, { distTag, version }, skills) {
|
|
1231
|
+
const steps = `run ${[
|
|
1232
|
+
...installKind === "global" ? [globalInstallCommand(distTag)] : [],
|
|
1233
|
+
cliCommand(installKind, "stop", distTag),
|
|
1234
|
+
...skills.filter((skill) => isOlderThan(skill, version)).map((skill) => installSkillCommand$1(installKind, distTag, skill))
|
|
1235
|
+
].map((command) => `\`${command}\``).join(", then ")}`;
|
|
1236
|
+
return installKind === "npx" ? `${steps}, and from now on run markdown-review as \`${npxCommand(distTag)}\`.` : `${steps}.`;
|
|
1237
|
+
}
|
|
1238
|
+
function installSkillCommand$1(installKind, versionOrTag, skill) {
|
|
1239
|
+
return cliCommand(installKind, skill.isGlobal ? "install-skill --global" : "install-skill", versionOrTag);
|
|
1240
|
+
}
|
|
1241
|
+
function isOlderThan(skill, version) {
|
|
1242
|
+
return skill.version === null || (compareVersions(skill.version, version) ?? -1) < 0;
|
|
1243
|
+
}
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/cli/readUpdateNotices.ts
|
|
1246
|
+
/**
|
|
1247
|
+
* Works out what to tell the agent about a newer markdown-review and outdated copies of the skill, from the update
|
|
1248
|
+
* check cache and the installed skills, without contacting the registry
|
|
1249
|
+
*
|
|
1250
|
+
* @param context the CLI's path and environment
|
|
1251
|
+
* @param root the repo root, where a repo copy of the skill lives
|
|
1252
|
+
* @param wentAhead whether the user said go ahead
|
|
1253
|
+
* @returns the notice lines, each ending in a newline
|
|
1254
|
+
*/
|
|
1255
|
+
async function readUpdateNotices({ cliPath, terminal }, root, wentAhead) {
|
|
1256
|
+
const { env } = terminal;
|
|
1257
|
+
const cached = updateChecksDisabled(env) ? null : await readUpdateCheckFile(updateCheckFilePath(env));
|
|
1258
|
+
const realCliPath = await realpath(cliPath).catch(() => cliPath);
|
|
1259
|
+
return formatUpdateNotices({
|
|
1260
|
+
availableUpdate: findAvailableUpdate(packageVersion, cached?.distTags ?? {}),
|
|
1261
|
+
currentVersion: packageVersion,
|
|
1262
|
+
installKind: findInstallKind(realCliPath),
|
|
1263
|
+
skills: await findInstalledSkills(root, env.HOME ?? os.homedir()),
|
|
1264
|
+
wentAhead
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
//#endregion
|
|
885
1268
|
//#region src/cli/commands/inboxCommand.ts
|
|
886
|
-
async function inboxCommand(args,
|
|
1269
|
+
async function inboxCommand(args, context) {
|
|
1270
|
+
const { cliPath, terminal } = context;
|
|
887
1271
|
const { values } = parseArgs({
|
|
888
1272
|
args,
|
|
889
1273
|
options: { document: { type: "string" } }
|
|
890
1274
|
});
|
|
891
1275
|
const root = await findRoot(terminal.workingDirectory);
|
|
892
1276
|
const document = await readDocumentOption(root, terminal.workingDirectory, values.document);
|
|
893
|
-
const
|
|
894
|
-
|
|
1277
|
+
const inbox = await (await connectToServer(root, cliPath)).inbox(document);
|
|
1278
|
+
const notices = roundOutcome(inbox.round) === "go-ahead" ? await readUpdateNotices(context, root, true) : [];
|
|
1279
|
+
terminal.stdout(formatInbox(inbox, notices));
|
|
895
1280
|
return 0;
|
|
896
1281
|
}
|
|
897
1282
|
//#endregion
|
|
898
1283
|
//#region skills/markdown-review/SKILL.md?raw
|
|
899
|
-
var SKILL_default = "---\nname: markdown-review\ndescription: Hands markdown docs you wrote, such as plans, specs and design notes, to the user to review in their browser, then reads their comments back so you can edit the docs and answer each one. Use when the user asks you to have a doc reviewed, or when a doc you wrote needs their sign-off before you carry on.\n---\n\n# Markdown Review\n\nThe user reviews your markdown in a browser and comments on passages, blocks, whole docs or the review as a whole. You read the comments with the `markdown-review` command, edit the docs, and answer each comment.\n\nIf the `markdown-review` command is not found, install it with `npm install --global @krelborn/markdown-review`.\n\n## The review loop\n\n1. Run `markdown-review open <doc.md
|
|
1284
|
+
var SKILL_default = "---\nname: markdown-review\ndescription: Hands markdown docs you wrote, such as plans, specs and design notes, to the user to review in their browser, then reads their comments back so you can edit the docs and answer each one. Use when the user asks you to have a doc reviewed, or when a doc you wrote needs their sign-off before you carry on.\n---\n\n# Markdown Review\n\nThe user reviews your markdown in a browser and comments on passages, blocks, whole docs or the review as a whole. You read the comments with the `markdown-review` command, edit the docs, and answer each comment.\n\nIf the `markdown-review` command is not found, install it with `npm install --global @krelborn/markdown-review`.\n\n## The review loop\n\n1. Run `markdown-review open <doc.md> --next-task \"<what you will do once the user says go ahead>\"`, for example `--next-task \"implement the plan in src/server\"`. It starts a review round and shows the doc in the user's browser, with your next task under Go ahead.\n2. Run `markdown-review poll`. It waits until the user submits, then prints each comment: its thread ID, file, line range, the quoted text and the user's words.\n - Give the shell command a timeout of at least 600000 ms. `poll` gives up after 540 seconds by default; run it again if it says there are no comments yet.\n - In Claude Code, run it with `run_in_background` and `--timeout 7080`, and a Bash timeout of 7200000 ms. A submit still ends it at once.\n3. Edit the docs. Then answer every thread:\n - `markdown-review resolve <id> \"<what changed>\"` when you have addressed it.\n - `markdown-review reply <id> \"<question>\"` when you need the user's input.\n - For a long message, put it in a file and pass `--file <path>`, or pass `--file -` and write it to standard input.\n4. Run `markdown-review poll` again for the next round.\n\n`markdown-review inbox` prints what `poll` would, without waiting, for picking comments up later.\n\n## How a review ends\n\nThe user ends a review in one of two ways, and `poll` and `inbox` say which on their first line.\n\n- **Go ahead** (\"The user said go ahead\"): address any comments that came with it, then carry on with your task. Do not run `poll` again for that review.\n- **Hand back** (\"The user asked to hand back to you\"): stop. Do not edit the docs, answer the threads, run `poll` or carry on with your task. Tell the user in the chat that you have stopped and what their comments say, and wait for their instructions. If they ask you to continue the review, run `markdown-review open <doc.md> --next-task \"<what you will do once the user says go ahead>\"`, then `markdown-review poll`.\n\n## Updates\n\n- `open` may print `update_available` when a newer markdown-review is out. Tell the user, and carry on with the review.\n- Once the user says go ahead, `poll` and `inbox` print `update_available` again with the commands to run. Ask the user whether to update, and run them only if they agree. If they install packages another way, such as with pnpm or under `sudo`, adapt the install command and keep the rest.\n- `skill_outdated` means an installed copy of this skill is from an older version. Once the user says go ahead, run the command it gives and tell the user.\n\n## Good to know\n\n- A quote is the text as the browser shows it, not the markdown source. The line range is exact, so use it to find the passage.\n- If `poll` is stopped before the user submits, nothing is lost: run it again.\n- Every command ends with a `next_step` line that says what to do next. `markdown-review --help` lists the commands.\n- Run `markdown-review stop` to stop the review server when you have finished.\n";
|
|
900
1285
|
//#endregion
|
|
901
1286
|
//#region src/cli/commands/installSkillCommand.ts
|
|
902
1287
|
/**
|
|
@@ -907,37 +1292,47 @@ async function installSkillCommand(args, { terminal }) {
|
|
|
907
1292
|
args,
|
|
908
1293
|
options: { global: { type: "boolean" } }
|
|
909
1294
|
});
|
|
910
|
-
const
|
|
911
|
-
const
|
|
1295
|
+
const skillPath = skillInstallPath(values.global === true ? terminal.env.HOME ?? os.homedir() : await findRoot(terminal.workingDirectory));
|
|
1296
|
+
const stamped = stampSkill(SKILL_default, packageVersion);
|
|
912
1297
|
const installed = await readTextFileOrNull(skillPath);
|
|
913
1298
|
await mkdir(path.dirname(skillPath), { recursive: true });
|
|
914
|
-
await writeFile(skillPath,
|
|
915
|
-
const outcome = installed === null ? "Installed" : installed ===
|
|
1299
|
+
await writeFile(skillPath, stamped);
|
|
1300
|
+
const outcome = installed === null ? "Installed" : installed === stamped ? "Reinstalled" : "Updated";
|
|
916
1301
|
terminal.stdout(`${outcome} the markdown-review skill at ${skillPath}\n\nnext_step: The skill loads when an agent session starts. To start a review now, run \`markdown-review open <doc.md>\`.
|
|
917
1302
|
`);
|
|
918
1303
|
return 0;
|
|
919
1304
|
}
|
|
920
1305
|
//#endregion
|
|
921
1306
|
//#region src/cli/commands/openCommand.ts
|
|
922
|
-
async function openCommand(args,
|
|
923
|
-
const {
|
|
1307
|
+
async function openCommand(args, context) {
|
|
1308
|
+
const { cliPath, terminal } = context;
|
|
1309
|
+
const { positionals, values } = parseArgs({
|
|
924
1310
|
allowPositionals: true,
|
|
925
1311
|
args,
|
|
926
|
-
options: {}
|
|
1312
|
+
options: { "next-task": { type: "string" } }
|
|
927
1313
|
});
|
|
928
1314
|
if (positionals.length > 1) throw new CliError("open takes at most one path", "Run `markdown-review open [path]`.", 2);
|
|
1315
|
+
const nextTask = readNextTask(values["next-task"]);
|
|
929
1316
|
const target = positionals[0];
|
|
930
1317
|
const workingRoot = await findRoot(terminal.workingDirectory);
|
|
931
1318
|
const { document, root } = target === void 0 ? {
|
|
932
1319
|
document: null,
|
|
933
1320
|
root: workingRoot
|
|
934
1321
|
} : await locate(terminal.workingDirectory, target);
|
|
935
|
-
const { navigated, url } = await (await connectToServer(root, cliPath)).open(document);
|
|
1322
|
+
const { navigated, url } = await (await connectToServer(root, cliPath)).open(document, nextTask);
|
|
936
1323
|
const shown = navigated ? "Shown in the browser tab that was already open." : await openUrl(url, terminal.env);
|
|
937
1324
|
const nextStep = root === workingRoot ? waitForCommentsStep : waitForCommentsInRootStep(root);
|
|
938
|
-
|
|
1325
|
+
const notices = await readUpdateNotices(context, root, false);
|
|
1326
|
+
const noticeSection = notices.length > 0 ? `${notices.join("")}\n` : "";
|
|
1327
|
+
terminal.stdout(`Opened ${document ?? "the docs list"} for review: ${url}\n${shown}\n\n${noticeSection}next_step: ${nextStep}\n`);
|
|
939
1328
|
return 0;
|
|
940
1329
|
}
|
|
1330
|
+
function readNextTask(value) {
|
|
1331
|
+
if (value === void 0) return null;
|
|
1332
|
+
const result = nextTaskSchema.safeParse(value);
|
|
1333
|
+
if (!result.success) throw new CliError("--next-task must be one line of at most 200 characters", "Run `markdown-review open <doc.md> --next-task \"<what you will do once the user says go ahead>\"`.", 2);
|
|
1334
|
+
return result.data;
|
|
1335
|
+
}
|
|
941
1336
|
async function locate(workingDirectory, target) {
|
|
942
1337
|
const absolutePath = path.resolve(workingDirectory, target);
|
|
943
1338
|
const found = await stat(absolutePath).catch(() => null);
|
|
@@ -964,7 +1359,8 @@ var interruptedExitCodes = {
|
|
|
964
1359
|
SIGINT: 130,
|
|
965
1360
|
SIGTERM: 143
|
|
966
1361
|
};
|
|
967
|
-
async function pollCommand(args,
|
|
1362
|
+
async function pollCommand(args, context) {
|
|
1363
|
+
const { cliPath, terminal } = context;
|
|
968
1364
|
const { values } = parseArgs({
|
|
969
1365
|
args,
|
|
970
1366
|
options: {
|
|
@@ -987,8 +1383,12 @@ async function pollCommand(args, { cliPath, terminal }) {
|
|
|
987
1383
|
try {
|
|
988
1384
|
terminal.stderr(`Waiting up to ${timeoutSeconds}s for review comments. If this is interrupted, run \`markdown-review poll\` again; nothing is lost.\n`);
|
|
989
1385
|
const result = await client.poll(document, timeoutSeconds, abort.signal);
|
|
990
|
-
|
|
991
|
-
|
|
1386
|
+
const outcome = roundOutcome(result.round);
|
|
1387
|
+
if (result.timedOut && result.threads.length === 0 && outcome === null) terminal.stdout(`No comments yet.\n\nnext_step: Run \`markdown-review poll\` again to keep waiting. ${pollTimeoutAdvice}\n`);
|
|
1388
|
+
else {
|
|
1389
|
+
const notices = outcome === "go-ahead" ? await readUpdateNotices(context, root, true) : [];
|
|
1390
|
+
terminal.stdout(formatInbox(result, notices));
|
|
1391
|
+
}
|
|
992
1392
|
return 0;
|
|
993
1393
|
} catch (error) {
|
|
994
1394
|
if (interruptedBy !== void 0) {
|
|
@@ -1108,10 +1508,13 @@ var StoreError = class extends Error {
|
|
|
1108
1508
|
};
|
|
1109
1509
|
//#endregion
|
|
1110
1510
|
//#region src/shared/api/apiRequestSchemas.ts
|
|
1111
|
-
var agentOpenRequestSchema = z.strictObject({
|
|
1511
|
+
var agentOpenRequestSchema = z.strictObject({
|
|
1512
|
+
nextTask: nextTaskSchema.optional(),
|
|
1513
|
+
path: documentPathSchema.optional()
|
|
1514
|
+
});
|
|
1112
1515
|
var messageRequestSchema = z.strictObject({ body: messageBodySchema });
|
|
1113
1516
|
var resolveRequestSchema = z.strictObject({ body: messageBodySchema.nullable() });
|
|
1114
|
-
var submitRequestSchema = z.strictObject({ verdict:
|
|
1517
|
+
var submitRequestSchema = z.strictObject({ verdict: verdictSchema });
|
|
1115
1518
|
//#endregion
|
|
1116
1519
|
//#region src/server/files/resolveRepositoryPath.ts
|
|
1117
1520
|
/**
|
|
@@ -1241,19 +1644,19 @@ function registerAgentRoutes(app, dependencies) {
|
|
|
1241
1644
|
return context.json({ stopping: true });
|
|
1242
1645
|
});
|
|
1243
1646
|
app.post("/api/agent/open", async (context) => {
|
|
1244
|
-
const { path } = await readJsonBody(context, agentOpenRequestSchema);
|
|
1647
|
+
const { nextTask, path } = await readJsonBody(context, agentOpenRequestSchema);
|
|
1245
1648
|
if (path !== void 0) {
|
|
1246
1649
|
await checkDocumentPath(root, path, false);
|
|
1247
1650
|
recentDocuments.add(path);
|
|
1248
1651
|
watchDocument(path);
|
|
1249
1652
|
}
|
|
1250
|
-
const
|
|
1653
|
+
const round = await store.openRound(path ?? null, nextTask ?? null);
|
|
1251
1654
|
events.publish({ type: "threads-changed" });
|
|
1252
1655
|
const pagePath = path === void 0 ? "" : `document/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
1253
1656
|
const url = `http://127.0.0.1:${port()}/${pagePath}`;
|
|
1254
1657
|
const response = {
|
|
1255
1658
|
navigated: tabs.navigateLatest(url),
|
|
1256
|
-
|
|
1659
|
+
round,
|
|
1257
1660
|
url
|
|
1258
1661
|
};
|
|
1259
1662
|
return context.json(response);
|
|
@@ -1466,7 +1869,7 @@ function registerHealthRoutes(app, { root, version }) {
|
|
|
1466
1869
|
const health = {
|
|
1467
1870
|
name: "markdown-review",
|
|
1468
1871
|
pid: process.pid,
|
|
1469
|
-
protocol:
|
|
1872
|
+
protocol: 2,
|
|
1470
1873
|
root,
|
|
1471
1874
|
version
|
|
1472
1875
|
};
|
|
@@ -1476,7 +1879,7 @@ function registerHealthRoutes(app, { root, version }) {
|
|
|
1476
1879
|
//#endregion
|
|
1477
1880
|
//#region src/server/http/waitForInbox.ts
|
|
1478
1881
|
/**
|
|
1479
|
-
* Waits until the review
|
|
1882
|
+
* Waits until the user ends the review round or a thread needs the agent
|
|
1480
1883
|
*
|
|
1481
1884
|
* @returns the inbox as soon as it has something for the agent, or as it stands when the time runs out or the
|
|
1482
1885
|
* request is aborted, with `timedOut` set
|
|
@@ -1486,7 +1889,7 @@ async function waitForInbox({ document, events, signal, store, timeoutMillisecon
|
|
|
1486
1889
|
for (;;) {
|
|
1487
1890
|
const nextSubmit = waitForSubmit(events, deadline - Date.now(), signal);
|
|
1488
1891
|
const inbox = await store.readInbox(document);
|
|
1489
|
-
if (inbox.
|
|
1892
|
+
if (roundOutcome(inbox.round) !== null || inbox.threads.length > 0) {
|
|
1490
1893
|
nextSubmit.cancel();
|
|
1491
1894
|
return {
|
|
1492
1895
|
...inbox,
|
|
@@ -1722,6 +2125,18 @@ function withAppearance(html, { mode, palette }) {
|
|
|
1722
2125
|
return html.replace("<html", `<html data-sui-mode="${mode}" data-sui-palette="${palette}"`);
|
|
1723
2126
|
}
|
|
1724
2127
|
//#endregion
|
|
2128
|
+
//#region src/server/http/updateRoutes.ts
|
|
2129
|
+
function registerUpdateRoutes(app, { installKind, root, updateChecker, version }) {
|
|
2130
|
+
app.get("/api/update", async (context) => {
|
|
2131
|
+
const update = updateChecker === null ? null : findAvailableUpdate(version, await updateChecker.distTags());
|
|
2132
|
+
const response = {
|
|
2133
|
+
availableVersion: update?.version ?? null,
|
|
2134
|
+
command: update !== null && installKind === "global" ? globalUpdateCommand(update.distTag, root) : null
|
|
2135
|
+
};
|
|
2136
|
+
return context.json(response);
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
//#endregion
|
|
1725
2140
|
//#region src/server/http/createApp.ts
|
|
1726
2141
|
var statusForStoreError = {
|
|
1727
2142
|
"invalid-file": 500,
|
|
@@ -1777,6 +2192,7 @@ function createApp(dependencies) {
|
|
|
1777
2192
|
registerBrowserRoutes(app, dependencies);
|
|
1778
2193
|
registerEventRoutes(app, dependencies);
|
|
1779
2194
|
registerFileRoutes(app, dependencies);
|
|
2195
|
+
registerUpdateRoutes(app, dependencies);
|
|
1780
2196
|
registerShellRoutes(app, dependencies);
|
|
1781
2197
|
return app;
|
|
1782
2198
|
}
|
|
@@ -1796,6 +2212,21 @@ var RecentDocuments = class {
|
|
|
1796
2212
|
}
|
|
1797
2213
|
};
|
|
1798
2214
|
//#endregion
|
|
2215
|
+
//#region src/shared/review/isDraftInRound.ts
|
|
2216
|
+
/**
|
|
2217
|
+
* Tells whether a submit sends a thread's draft
|
|
2218
|
+
*
|
|
2219
|
+
* @param thread the thread
|
|
2220
|
+
* @param round the current round, or null before any review was opened
|
|
2221
|
+
* @returns false when the thread has no draft; otherwise true when there is no round, or the draft is on the whole
|
|
2222
|
+
* review, on a doc opened this round, or written since the round began
|
|
2223
|
+
*/
|
|
2224
|
+
function isDraftInRound(thread, round) {
|
|
2225
|
+
if (thread.draft === void 0) return false;
|
|
2226
|
+
if (round === null || thread.anchor.kind === "review") return true;
|
|
2227
|
+
return round.documents.includes(thread.anchor.document) || Date.parse(thread.draft.at) >= Date.parse(round.startedAt);
|
|
2228
|
+
}
|
|
2229
|
+
//#endregion
|
|
1799
2230
|
//#region src/shared/markdown/fenceLanguage.ts
|
|
1800
2231
|
/**
|
|
1801
2232
|
* Reads the language of a fenced code block from its info string
|
|
@@ -2363,12 +2794,53 @@ function reanchorDocumentThreads(threads, source) {
|
|
|
2363
2794
|
//#endregion
|
|
2364
2795
|
//#region src/server/store/emptyReviewFile.ts
|
|
2365
2796
|
var emptyReviewFile = {
|
|
2366
|
-
|
|
2367
|
-
requestedAt: null,
|
|
2797
|
+
round: null,
|
|
2368
2798
|
threads: [],
|
|
2369
|
-
version:
|
|
2799
|
+
version: 2
|
|
2370
2800
|
};
|
|
2371
2801
|
//#endregion
|
|
2802
|
+
//#region src/server/store/joinOrStartRound.ts
|
|
2803
|
+
var roundExpiryMilliseconds = 432e5;
|
|
2804
|
+
/**
|
|
2805
|
+
* Adds a doc to the review round going on, or starts the next round
|
|
2806
|
+
*
|
|
2807
|
+
* @param round the current round, or null before any review was opened
|
|
2808
|
+
* @param path the doc the agent opened, or null for the docs list
|
|
2809
|
+
* @param nextTask what the agent will do once the user says go ahead; null keeps a joined round's task and leaves a
|
|
2810
|
+
* new round without one
|
|
2811
|
+
* @param at the current time, ISO 8601
|
|
2812
|
+
* @returns the round joined when it has not ended and had an open or a submit in the last 12 hours, otherwise a new
|
|
2813
|
+
* round, which starts when the user handed the previous one back if that was in the last 12 hours
|
|
2814
|
+
*/
|
|
2815
|
+
function joinOrStartRound(round, path, nextTask, at) {
|
|
2816
|
+
if (round !== null && roundOutcome(round) === null && isRecent(lastActivity(round), at)) return {
|
|
2817
|
+
...round,
|
|
2818
|
+
documents: path === null || round.documents.includes(path) ? round.documents : [...round.documents, path],
|
|
2819
|
+
lastOpenedAt: at,
|
|
2820
|
+
nextTask: nextTask ?? round.nextTask
|
|
2821
|
+
};
|
|
2822
|
+
return {
|
|
2823
|
+
documents: path === null ? [] : [path],
|
|
2824
|
+
lastOpenedAt: at,
|
|
2825
|
+
nextTask,
|
|
2826
|
+
startedAt: recentHandBack(round, at) ?? at,
|
|
2827
|
+
verdict: null
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
function lastActivity({ lastOpenedAt, verdict }) {
|
|
2831
|
+
return verdict !== null && Date.parse(verdict.at) > Date.parse(lastOpenedAt) ? verdict.at : lastOpenedAt;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* @returns when the user handed the round back, no later than `at`, or null when they did not or it was 12 hours ago
|
|
2835
|
+
*/
|
|
2836
|
+
function recentHandBack(round, at) {
|
|
2837
|
+
if (round?.verdict?.kind !== "hand-back" || !isRecent(round.verdict.at, at)) return null;
|
|
2838
|
+
return Date.parse(round.verdict.at) < Date.parse(at) ? round.verdict.at : at;
|
|
2839
|
+
}
|
|
2840
|
+
function isRecent(earlier, at) {
|
|
2841
|
+
return Date.parse(at) - Date.parse(earlier) < roundExpiryMilliseconds;
|
|
2842
|
+
}
|
|
2843
|
+
//#endregion
|
|
2372
2844
|
//#region src/server/store/needsAgent.ts
|
|
2373
2845
|
/**
|
|
2374
2846
|
* Tells whether a thread is waiting for the agent
|
|
@@ -2451,12 +2923,12 @@ var documentThreadsFileSchema = z.strictObject({
|
|
|
2451
2923
|
}).refine((file) => file.threads.every((thread) => thread.anchor.kind !== "review" && thread.anchor.document === file.document), { message: "must hold only threads anchored to its own doc" });
|
|
2452
2924
|
//#endregion
|
|
2453
2925
|
//#region src/shared/review/reviewFileSchema.ts
|
|
2926
|
+
var reviewThreadsSchema = z.array(threadSchema).refine((threads) => threads.every((thread) => thread.anchor.kind === "review"), { message: "must hold only threads on the whole review" });
|
|
2454
2927
|
var reviewFileSchema = z.strictObject({
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
}).refine((file) => file.threads.every((thread) => thread.anchor.kind === "review"), { message: "must hold only threads on the whole review" });
|
|
2928
|
+
round: reviewRoundSchema.nullable(),
|
|
2929
|
+
threads: reviewThreadsSchema,
|
|
2930
|
+
version: z.literal(2)
|
|
2931
|
+
});
|
|
2460
2932
|
//#endregion
|
|
2461
2933
|
//#region src/server/files/writeTextFileUnlessExists.ts
|
|
2462
2934
|
/**
|
|
@@ -2480,6 +2952,44 @@ async function writeTextFileUnlessExists(filePath, contents) {
|
|
|
2480
2952
|
}
|
|
2481
2953
|
}
|
|
2482
2954
|
//#endregion
|
|
2955
|
+
//#region src/server/store/storedReviewFileSchema.ts
|
|
2956
|
+
var reviewFileVersion1Schema = z.strictObject({
|
|
2957
|
+
approvedAt: z.iso.datetime().nullable(),
|
|
2958
|
+
requestedAt: z.iso.datetime().nullable(),
|
|
2959
|
+
threads: reviewThreadsSchema,
|
|
2960
|
+
version: z.literal(1)
|
|
2961
|
+
});
|
|
2962
|
+
/**
|
|
2963
|
+
* Reads `review.json` as written by any version, giving the current version
|
|
2964
|
+
*/
|
|
2965
|
+
var storedReviewFileSchema = z.discriminatedUnion("version", [reviewFileSchema, reviewFileVersion1Schema]).transform((file) => file.version === 1 ? upgradeVersion1(file) : file);
|
|
2966
|
+
/**
|
|
2967
|
+
* @param file a version 1 file, which recorded when the agent last opened the review and when the user last approved
|
|
2968
|
+
* @returns the same threads, with no round when no review was opened, otherwise a round on no docs that started at the
|
|
2969
|
+
* last open and ended with Go ahead when the approval came after it
|
|
2970
|
+
*/
|
|
2971
|
+
function upgradeVersion1({ approvedAt, requestedAt, threads }) {
|
|
2972
|
+
if (requestedAt === null) return {
|
|
2973
|
+
round: null,
|
|
2974
|
+
threads,
|
|
2975
|
+
version: 2
|
|
2976
|
+
};
|
|
2977
|
+
return {
|
|
2978
|
+
round: {
|
|
2979
|
+
documents: [],
|
|
2980
|
+
lastOpenedAt: requestedAt,
|
|
2981
|
+
nextTask: null,
|
|
2982
|
+
startedAt: requestedAt,
|
|
2983
|
+
verdict: approvedAt !== null && Date.parse(approvedAt) > Date.parse(requestedAt) ? {
|
|
2984
|
+
at: approvedAt,
|
|
2985
|
+
kind: "go-ahead"
|
|
2986
|
+
} : null
|
|
2987
|
+
},
|
|
2988
|
+
threads,
|
|
2989
|
+
version: 2
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
//#endregion
|
|
2483
2993
|
//#region src/server/store/StoreFiles.ts
|
|
2484
2994
|
/**
|
|
2485
2995
|
* Reads and writes the files of one root's `.markdown-review/` directory and the docs they refer to
|
|
@@ -2500,7 +3010,7 @@ var StoreFiles = class {
|
|
|
2500
3010
|
* @returns the review file, or an empty one when it does not exist yet
|
|
2501
3011
|
*/
|
|
2502
3012
|
async readReviewFile() {
|
|
2503
|
-
const result = await readStoreFile(reviewFilePath(this.root),
|
|
3013
|
+
const result = await readStoreFile(reviewFilePath(this.root), storedReviewFileSchema);
|
|
2504
3014
|
return result.kind === "valid" ? {
|
|
2505
3015
|
kind: "valid",
|
|
2506
3016
|
value: result.value ?? emptyReviewFile
|
|
@@ -2683,21 +3193,6 @@ function withoutDraft(thread) {
|
|
|
2683
3193
|
return next;
|
|
2684
3194
|
}
|
|
2685
3195
|
//#endregion
|
|
2686
|
-
//#region src/server/store/toReviewState.ts
|
|
2687
|
-
/**
|
|
2688
|
-
* Works out whether the current review round is approved
|
|
2689
|
-
*
|
|
2690
|
-
* @param reviewFile the review's stored timestamps
|
|
2691
|
-
* @returns the timestamps and whether the approval came after the latest request
|
|
2692
|
-
*/
|
|
2693
|
-
function toReviewState({ approvedAt, requestedAt }) {
|
|
2694
|
-
return {
|
|
2695
|
-
approved: approvedAt !== null && (requestedAt === null || Date.parse(approvedAt) > Date.parse(requestedAt)),
|
|
2696
|
-
approvedAt,
|
|
2697
|
-
requestedAt
|
|
2698
|
-
};
|
|
2699
|
-
}
|
|
2700
|
-
//#endregion
|
|
2701
3196
|
//#region src/server/store/ReviewStore.ts
|
|
2702
3197
|
/**
|
|
2703
3198
|
* Holds one root's review threads and carries out every change to them, one at a time
|
|
@@ -2725,18 +3220,22 @@ var ReviewStore = class {
|
|
|
2725
3220
|
});
|
|
2726
3221
|
}
|
|
2727
3222
|
/**
|
|
2728
|
-
*
|
|
3223
|
+
* Adds a doc to the review round going on, or starts the next round
|
|
2729
3224
|
*
|
|
2730
|
-
* @
|
|
3225
|
+
* @param path the doc the agent opened, or null for the docs list
|
|
3226
|
+
* @param nextTask what the agent will do once the user says go ahead; null keeps a joined round's task and leaves a
|
|
3227
|
+
* new round without one
|
|
3228
|
+
* @returns the round the doc is now in
|
|
2731
3229
|
*/
|
|
2732
|
-
|
|
3230
|
+
openRound(path, nextTask) {
|
|
2733
3231
|
return this.queue.enqueue(async () => {
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
3232
|
+
const review = requireValid(await this.files.readReviewFile());
|
|
3233
|
+
const round = joinOrStartRound(review.round, path, nextTask, (/* @__PURE__ */ new Date()).toISOString());
|
|
3234
|
+
await this.files.writeReviewFile({
|
|
3235
|
+
...review,
|
|
3236
|
+
round
|
|
3237
|
+
});
|
|
3238
|
+
return round;
|
|
2740
3239
|
});
|
|
2741
3240
|
}
|
|
2742
3241
|
/**
|
|
@@ -2794,31 +3293,40 @@ var ReviewStore = class {
|
|
|
2794
3293
|
return this.changeThread(id, "user", resolveAsUser);
|
|
2795
3294
|
}
|
|
2796
3295
|
/**
|
|
2797
|
-
* Submits
|
|
3296
|
+
* Submits the drafts that belong to the review round, leaving the rest as drafts, and records the user's verdict on
|
|
3297
|
+
* the round, starting one when no review was opened
|
|
2798
3298
|
*
|
|
2799
|
-
* @param verdict "
|
|
2800
|
-
* @returns the IDs of the threads that received a submitted message, and the
|
|
2801
|
-
* @throws StoreError "invalid-state" when
|
|
3299
|
+
* @param verdict "go-ahead" and "hand-back" end the round; "revise" needs at least one draft to submit
|
|
3300
|
+
* @returns the IDs of the threads that received a submitted message, and the round after the submit
|
|
3301
|
+
* @throws StoreError "invalid-state" when asking for revisions with no drafts to submit
|
|
2802
3302
|
*/
|
|
2803
3303
|
submit(verdict) {
|
|
2804
3304
|
return this.queue.enqueue(async () => {
|
|
2805
3305
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2806
3306
|
const review = requireValid(await this.files.readReviewFile());
|
|
2807
|
-
const
|
|
2808
|
-
const
|
|
2809
|
-
|
|
3307
|
+
const isSent = (thread) => isDraftInRound(thread, review.round);
|
|
3308
|
+
const send = (thread) => isSent(thread) ? submitDraft(thread, at) : thread;
|
|
3309
|
+
const documentFiles = await this.readDocumentFilesToSubmit(isSent);
|
|
3310
|
+
const submittedThreadIds = [...review.threads, ...documentFiles.flatMap((file) => file.threads)].filter(isSent).map((thread) => thread.id).sort((left, right) => left - right);
|
|
3311
|
+
if (verdict === "revise" && submittedThreadIds.length === 0) throw new StoreError("invalid-state", "There are no drafts to submit");
|
|
2810
3312
|
for (const file of documentFiles) await this.files.writeDocumentFile({
|
|
2811
3313
|
...file,
|
|
2812
|
-
threads: file.threads.map(
|
|
3314
|
+
threads: file.threads.map(send)
|
|
2813
3315
|
});
|
|
2814
|
-
const
|
|
2815
|
-
...review,
|
|
2816
|
-
|
|
2817
|
-
|
|
3316
|
+
const round = {
|
|
3317
|
+
...review.round ?? joinOrStartRound(null, null, null, at),
|
|
3318
|
+
verdict: {
|
|
3319
|
+
at,
|
|
3320
|
+
kind: verdict
|
|
3321
|
+
}
|
|
2818
3322
|
};
|
|
2819
|
-
await this.files.writeReviewFile(
|
|
3323
|
+
await this.files.writeReviewFile({
|
|
3324
|
+
...review,
|
|
3325
|
+
round,
|
|
3326
|
+
threads: review.threads.map(send)
|
|
3327
|
+
});
|
|
2820
3328
|
return {
|
|
2821
|
-
|
|
3329
|
+
round,
|
|
2822
3330
|
submittedThreadIds
|
|
2823
3331
|
};
|
|
2824
3332
|
});
|
|
@@ -2842,7 +3350,7 @@ var ReviewStore = class {
|
|
|
2842
3350
|
* Reads threads for the browser, first re-anchoring any doc that changed since its threads were last anchored
|
|
2843
3351
|
*
|
|
2844
3352
|
* @param document the doc whose threads to read, alongside the review's, or null for every thread
|
|
2845
|
-
* @returns the review
|
|
3353
|
+
* @returns the review round, the threads in ID order, and any store files that could not be read
|
|
2846
3354
|
* @throws StoreError "invalid-input" when the doc path is not a repo-relative POSIX path
|
|
2847
3355
|
*/
|
|
2848
3356
|
readThreads(document) {
|
|
@@ -2852,7 +3360,7 @@ var ReviewStore = class {
|
|
|
2852
3360
|
* Reads what the agent has to act on, first re-anchoring any doc that changed since its threads were last anchored
|
|
2853
3361
|
*
|
|
2854
3362
|
* @param document the doc to limit the threads to, or null for every thread including the review's
|
|
2855
|
-
* @returns the review
|
|
3363
|
+
* @returns the review round and the threads that need the agent
|
|
2856
3364
|
* @throws StoreError "invalid-input" when the doc path is not a repo-relative POSIX path
|
|
2857
3365
|
*/
|
|
2858
3366
|
readInbox(document) {
|
|
@@ -2874,7 +3382,7 @@ var ReviewStore = class {
|
|
|
2874
3382
|
for (const name of documents) threads.push(...await this.readDocumentThreadsOrReport(name, problems));
|
|
2875
3383
|
return {
|
|
2876
3384
|
problems,
|
|
2877
|
-
|
|
3385
|
+
round: review.round,
|
|
2878
3386
|
threads: threads.sort((left, right) => left.id - right.id)
|
|
2879
3387
|
};
|
|
2880
3388
|
}
|
|
@@ -2959,12 +3467,12 @@ var ReviewStore = class {
|
|
|
2959
3467
|
problems
|
|
2960
3468
|
};
|
|
2961
3469
|
}
|
|
2962
|
-
async
|
|
3470
|
+
async readDocumentFilesToSubmit(isSent) {
|
|
2963
3471
|
const files = [];
|
|
2964
3472
|
for (const document of await this.files.listDocuments()) {
|
|
2965
3473
|
const result = await this.files.readDocumentFile(document);
|
|
2966
3474
|
if (result.kind === "invalid") this.logger.warn(`Drafts in ${document} were not submitted. ${result.problem}`);
|
|
2967
|
-
else if (result.value?.threads.some(
|
|
3475
|
+
else if (result.value?.threads.some(isSent)) files.push((await this.readCurrentDocumentFile(document)).file);
|
|
2968
3476
|
}
|
|
2969
3477
|
return files;
|
|
2970
3478
|
}
|
|
@@ -3029,6 +3537,139 @@ function requireValid(result) {
|
|
|
3029
3537
|
return result.value;
|
|
3030
3538
|
}
|
|
3031
3539
|
//#endregion
|
|
3540
|
+
//#region src/server/update/RegistryRequestError.ts
|
|
3541
|
+
/**
|
|
3542
|
+
* A dist-tags request the npm registry answered with an error status
|
|
3543
|
+
*/
|
|
3544
|
+
var RegistryRequestError = class extends Error {
|
|
3545
|
+
status;
|
|
3546
|
+
constructor(url, status) {
|
|
3547
|
+
super(`${url} answered ${status}`);
|
|
3548
|
+
this.name = "RegistryRequestError";
|
|
3549
|
+
this.status = status;
|
|
3550
|
+
}
|
|
3551
|
+
};
|
|
3552
|
+
//#endregion
|
|
3553
|
+
//#region src/server/update/fetchDistTags.ts
|
|
3554
|
+
var packageName = "@krelborn/markdown-review";
|
|
3555
|
+
/**
|
|
3556
|
+
* Asks the registry for the package's dist-tags
|
|
3557
|
+
*
|
|
3558
|
+
* @param registry the registry's URL, without a trailing slash
|
|
3559
|
+
* @param fetchUrl the fetch to send the request with
|
|
3560
|
+
* @param timeoutMilliseconds how long to wait for the whole response
|
|
3561
|
+
* @returns the `latest` and `next` tags, when the registry has them
|
|
3562
|
+
* @throws when the request fails, times out, or the response is not a dist-tags object
|
|
3563
|
+
*/
|
|
3564
|
+
async function fetchDistTags(registry, fetchUrl, timeoutMilliseconds) {
|
|
3565
|
+
const url = `${registry}/-/package/${encodeURIComponent(packageName)}/dist-tags`;
|
|
3566
|
+
const response = await fetchUrl(url, { signal: AbortSignal.timeout(timeoutMilliseconds) });
|
|
3567
|
+
if (!response.ok) throw new RegistryRequestError(url, response.status);
|
|
3568
|
+
return distTagsSchema.parse(await response.json());
|
|
3569
|
+
}
|
|
3570
|
+
//#endregion
|
|
3571
|
+
//#region src/server/update/UpdateChecker.ts
|
|
3572
|
+
var dayMilliseconds = 864e5;
|
|
3573
|
+
var minuteMilliseconds = 6e4;
|
|
3574
|
+
/**
|
|
3575
|
+
* Asks the npm registry for markdown-review's dist-tags about once a day for the user, keeping the answer in a cache
|
|
3576
|
+
* file that every repo's server and the CLI read
|
|
3577
|
+
*/
|
|
3578
|
+
var UpdateChecker = class {
|
|
3579
|
+
cachePath;
|
|
3580
|
+
checking;
|
|
3581
|
+
fetch;
|
|
3582
|
+
isStarted = false;
|
|
3583
|
+
logger;
|
|
3584
|
+
now;
|
|
3585
|
+
recorded = null;
|
|
3586
|
+
registry;
|
|
3587
|
+
timer;
|
|
3588
|
+
timeoutMilliseconds;
|
|
3589
|
+
constructor({ cachePath, fetch: fetchUrl = fetch, logger, now = () => /* @__PURE__ */ new Date(), registry, timeoutMilliseconds = 3e3 }) {
|
|
3590
|
+
this.cachePath = cachePath;
|
|
3591
|
+
this.fetch = fetchUrl;
|
|
3592
|
+
this.logger = logger;
|
|
3593
|
+
this.now = now;
|
|
3594
|
+
this.registry = registry;
|
|
3595
|
+
this.timeoutMilliseconds = timeoutMilliseconds;
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Asks the registry unless the cache is less than a day old; never rejects
|
|
3599
|
+
*
|
|
3600
|
+
* @returns once the cache is up to date, or the check has failed
|
|
3601
|
+
*/
|
|
3602
|
+
check() {
|
|
3603
|
+
this.checking ??= this.refresh().finally(() => {
|
|
3604
|
+
this.checking = void 0;
|
|
3605
|
+
});
|
|
3606
|
+
return this.checking;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* @returns the dist-tags last recorded, after any check in progress ends; empty when none are known
|
|
3610
|
+
*/
|
|
3611
|
+
async distTags() {
|
|
3612
|
+
await this.checking;
|
|
3613
|
+
return (await this.readLatest())?.distTags ?? {};
|
|
3614
|
+
}
|
|
3615
|
+
/**
|
|
3616
|
+
* Checks now, then whenever the cache turns a day old while the process runs
|
|
3617
|
+
*/
|
|
3618
|
+
start() {
|
|
3619
|
+
this.isStarted = true;
|
|
3620
|
+
this.checkAndSchedule();
|
|
3621
|
+
}
|
|
3622
|
+
stop() {
|
|
3623
|
+
this.isStarted = false;
|
|
3624
|
+
clearTimeout(this.timer);
|
|
3625
|
+
}
|
|
3626
|
+
async checkAndSchedule() {
|
|
3627
|
+
await this.check();
|
|
3628
|
+
if (!this.isStarted) return;
|
|
3629
|
+
const cached = await this.readLatest();
|
|
3630
|
+
const untilStale = cached === null ? dayMilliseconds : Date.parse(cached.checkedAt) + dayMilliseconds - this.now().getTime();
|
|
3631
|
+
const delay = Math.min(Math.max(untilStale, minuteMilliseconds), dayMilliseconds);
|
|
3632
|
+
clearTimeout(this.timer);
|
|
3633
|
+
this.timer = setTimeout(() => void this.checkAndSchedule(), delay);
|
|
3634
|
+
this.timer.unref();
|
|
3635
|
+
}
|
|
3636
|
+
async refresh() {
|
|
3637
|
+
const cached = await this.readLatest();
|
|
3638
|
+
const now = this.now();
|
|
3639
|
+
if (isFresh(cached, now)) return;
|
|
3640
|
+
let distTags;
|
|
3641
|
+
try {
|
|
3642
|
+
distTags = await fetchDistTags(this.registry, this.fetch, this.timeoutMilliseconds);
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
this.logger.warn("Could not check the npm registry for a newer markdown-review", error);
|
|
3645
|
+
const latest = await this.readLatest();
|
|
3646
|
+
if (isFresh(latest, now)) return;
|
|
3647
|
+
distTags = latest?.distTags ?? {};
|
|
3648
|
+
}
|
|
3649
|
+
const file = {
|
|
3650
|
+
checkedAt: now.toISOString(),
|
|
3651
|
+
distTags
|
|
3652
|
+
};
|
|
3653
|
+
this.recorded = file;
|
|
3654
|
+
try {
|
|
3655
|
+
await writeJsonAtomically(this.cachePath, file);
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
this.logger.warn(`Could not write ${this.cachePath}`, error);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* The cache file, or this checker's own last check when that is newer, as it is when the file could not be written
|
|
3662
|
+
*/
|
|
3663
|
+
async readLatest() {
|
|
3664
|
+
const cached = await readUpdateCheckFile(this.cachePath);
|
|
3665
|
+
if (cached === null || this.recorded !== null && Date.parse(this.recorded.checkedAt) > Date.parse(cached.checkedAt)) return this.recorded;
|
|
3666
|
+
return cached;
|
|
3667
|
+
}
|
|
3668
|
+
};
|
|
3669
|
+
function isFresh(file, now) {
|
|
3670
|
+
return file !== null && now.getTime() - Date.parse(file.checkedAt) < dayMilliseconds;
|
|
3671
|
+
}
|
|
3672
|
+
//#endregion
|
|
3032
3673
|
//#region src/server/runtime/ActivityMonitor.ts
|
|
3033
3674
|
/**
|
|
3034
3675
|
* Tracks connected tabs and waiting polls, reports whether the agent is waiting, and notices when the server is idle
|
|
@@ -3164,7 +3805,7 @@ function preferredPort(root) {
|
|
|
3164
3805
|
*
|
|
3165
3806
|
* @returns the running server; it removes `server.json` when it stops
|
|
3166
3807
|
*/
|
|
3167
|
-
async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMilliseconds = 15e3, root, webDirectory }) {
|
|
3808
|
+
async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMilliseconds = 15e3, root, updateChecker = createUpdateChecker(logger), webDirectory }) {
|
|
3168
3809
|
const store = new ReviewStore(root, logger);
|
|
3169
3810
|
await store.initialize();
|
|
3170
3811
|
const events = new ServerEvents();
|
|
@@ -3199,6 +3840,7 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
|
|
|
3199
3840
|
const app = createApp({
|
|
3200
3841
|
activity,
|
|
3201
3842
|
events,
|
|
3843
|
+
installKind: findInstallKind(process.argv[1] ?? ""),
|
|
3202
3844
|
logger,
|
|
3203
3845
|
pollKeepAliveMilliseconds,
|
|
3204
3846
|
port: () => port,
|
|
@@ -3208,16 +3850,18 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
|
|
|
3208
3850
|
store,
|
|
3209
3851
|
tabs: new BrowserTabs(),
|
|
3210
3852
|
token,
|
|
3853
|
+
updateChecker,
|
|
3211
3854
|
version: packageVersion,
|
|
3212
3855
|
watchDocument: (document) => watcher.watch(document),
|
|
3213
3856
|
webDirectory
|
|
3214
3857
|
});
|
|
3858
|
+
updateChecker?.start();
|
|
3215
3859
|
const server = createServer(getRequestListener(app.fetch));
|
|
3216
3860
|
port = await listen(server, preferredPort(root));
|
|
3217
3861
|
const serverFile = {
|
|
3218
3862
|
pid: process.pid,
|
|
3219
3863
|
port,
|
|
3220
|
-
protocol:
|
|
3864
|
+
protocol: 2,
|
|
3221
3865
|
root,
|
|
3222
3866
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3223
3867
|
token,
|
|
@@ -3228,6 +3872,7 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
|
|
|
3228
3872
|
logger.info(`Serving ${root} on http://127.0.0.1:${port}`);
|
|
3229
3873
|
async function stop() {
|
|
3230
3874
|
activity.stop();
|
|
3875
|
+
updateChecker?.stop();
|
|
3231
3876
|
await watcher.close();
|
|
3232
3877
|
await closeServer(server);
|
|
3233
3878
|
await removeServerFile(root);
|
|
@@ -3240,6 +3885,14 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
|
|
|
3240
3885
|
port
|
|
3241
3886
|
};
|
|
3242
3887
|
}
|
|
3888
|
+
function createUpdateChecker(logger) {
|
|
3889
|
+
if (updateChecksDisabled(process.env)) return null;
|
|
3890
|
+
return new UpdateChecker({
|
|
3891
|
+
cachePath: updateCheckFilePath(process.env),
|
|
3892
|
+
logger,
|
|
3893
|
+
registry: registryUrl(process.env)
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3243
3896
|
async function listen(server, port) {
|
|
3244
3897
|
try {
|
|
3245
3898
|
return await listenOn(server, port);
|
|
@@ -3333,16 +3986,21 @@ async function stopCommand(args, { terminal }) {
|
|
|
3333
3986
|
//#endregion
|
|
3334
3987
|
//#region src/cli/helpText.ts
|
|
3335
3988
|
var commandHelp = {
|
|
3336
|
-
inbox: ["markdown-review inbox [--document <path>]", " Print
|
|
3989
|
+
inbox: ["markdown-review inbox [--document <path>]", " Print how the user ended the review, if they have, and the threads that need you, then return at once."].join("\n"),
|
|
3337
3990
|
"install-skill": [
|
|
3338
3991
|
"markdown-review install-skill [--global]",
|
|
3339
3992
|
" Install the skill that teaches agents this workflow in this repository's .claude/skills, or with --global in",
|
|
3340
3993
|
" ~/.claude/skills. Claude Code and OpenCode both read skills from there."
|
|
3341
3994
|
].join("\n"),
|
|
3342
|
-
open: [
|
|
3995
|
+
open: [
|
|
3996
|
+
"markdown-review open [path] [--next-task <text>]",
|
|
3997
|
+
" Start a review round, or add the doc to the one in progress, and show the doc (or the docs list) in the",
|
|
3998
|
+
" user's browser. --next-task says what you will do once the user says go ahead; the user sees it.",
|
|
3999
|
+
" Prints the URL."
|
|
4000
|
+
].join("\n"),
|
|
3343
4001
|
poll: [
|
|
3344
4002
|
"markdown-review poll [--document <path>] [--timeout <seconds>]",
|
|
3345
|
-
" Wait until the user submits
|
|
4003
|
+
" Wait until the user submits, then print what needs you. The timeout defaults to 540 seconds.",
|
|
3346
4004
|
" Give the shell command a timeout of at least 600000 ms, or in Claude Code run it with run_in_background",
|
|
3347
4005
|
" and --timeout 7080."
|
|
3348
4006
|
].join("\n"),
|