@tequila99/githuman 0.11.0 → 0.12.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 +62 -2
- package/dist/cli/commands/mcp.js +112 -0
- package/dist/cli/commands/pi-acp.js +21 -0
- package/dist/cli/config.js +7 -1
- package/dist/cli/index.js +19 -8
- package/dist/cli/server-runtime.js +1 -0
- package/dist/server/adapters/agents/artifact-permission.js +88 -0
- package/dist/server/adapters/pi-acp/agent.js +98 -0
- package/dist/server/adapters/pi-acp/compatibility.js +57 -0
- package/dist/server/adapters/pi-acp/control.js +76 -0
- package/dist/server/adapters/pi-acp/errors.js +17 -0
- package/dist/server/adapters/pi-acp/extension.js +255 -0
- package/dist/server/adapters/pi-acp/launch.js +16 -0
- package/dist/server/adapters/pi-acp/mcp.js +153 -0
- package/dist/server/adapters/pi-acp/process.js +199 -0
- package/dist/server/adapters/pi-acp/session.js +409 -0
- package/dist/server/adapters/pi-acp/vendor/LICENSE +21 -0
- package/dist/server/adapters/pi-acp/vendor/README.md +29 -0
- package/dist/server/adapters/pi-acp/vendor/prompt.js +56 -0
- package/dist/server/app.js +94 -5
- package/dist/server/config/agents.js +4 -6
- package/dist/server/db/artifact-graph-migrations.js +56 -0
- package/dist/server/db/artifact-migrations.js +70 -0
- package/dist/server/db/artifact-review-migrations.js +24 -0
- package/dist/server/db/artifact-search-migrations.js +31 -0
- package/dist/server/db/artifact-validation-migrations.js +70 -0
- package/dist/server/db/index.js +3 -0
- package/dist/server/db/migrations.js +10 -0
- package/dist/server/db/sqlite-error.js +15 -0
- package/dist/server/db/transaction.js +40 -0
- package/dist/server/errors/artifacts.js +28 -0
- package/dist/server/errors/database.js +2 -0
- package/dist/server/hooks/access/require-access.js +8 -0
- package/dist/server/repositories/artifact-catalog.repo.js +283 -0
- package/dist/server/repositories/artifact-files.repo.js +181 -0
- package/dist/server/repositories/artifact-graph.repo.js +50 -0
- package/dist/server/repositories/artifact-search.repo.js +192 -0
- package/dist/server/repositories/artifact-transition.repo.js +85 -0
- package/dist/server/repositories/artifact-validation.repo.js +146 -0
- package/dist/server/repositories/notes-catalog.repo.js +2 -10
- package/dist/server/repositories/task.repo.js +114 -0
- package/dist/server/routes/agent.js +8 -1
- package/dist/server/routes/artifact-actions.js +129 -0
- package/dist/server/routes/artifact-agent.js +183 -0
- package/dist/server/routes/artifact-definitions.js +163 -0
- package/dist/server/routes/artifact-transitions.js +80 -0
- package/dist/server/routes/artifacts.js +111 -0
- package/dist/server/routes/events.js +5 -1
- package/dist/server/routes/project-files.js +90 -0
- package/dist/server/services/agent/agent-session.js +180 -10
- package/dist/server/services/agent/context-builder.js +30 -2
- package/dist/server/services/agent/session-registry.js +106 -2
- package/dist/server/services/agent/turn-task-context.js +1 -0
- package/dist/server/services/artifact-authorization.service.js +174 -0
- package/dist/server/services/artifact-definition.service.js +96 -0
- package/dist/server/services/artifact-export.service.js +44 -0
- package/dist/server/services/artifact-graph-validation.js +497 -0
- package/dist/server/services/artifact-graph.service.js +81 -0
- package/dist/server/services/artifact-lifecycle.service.js +343 -0
- package/dist/server/services/artifact-maintenance.service.js +55 -0
- package/dist/server/services/artifact-search.service.js +331 -0
- package/dist/server/services/artifact-storage.service.js +646 -0
- package/dist/server/services/artifact-transition.service.js +607 -0
- package/dist/server/services/artifact-type.service.js +74 -0
- package/dist/server/services/artifact-validation-jobs.service.js +518 -0
- package/dist/server/services/device-connections.js +9 -1
- package/dist/server/services/project-files.service.js +144 -0
- package/dist/server/services/review.service.js +6 -5
- package/dist/server/services/task.service.js +225 -0
- package/dist/server/utils/artifact-actor.js +13 -0
- package/dist/server/utils/artifact-page.js +42 -0
- package/dist/server/utils/take-within-bytes.js +16 -0
- package/dist/server/utils/text.js +7 -0
- package/dist/shared/agents/schemas.js +27 -0
- package/dist/shared/app/storage-constants.js +2 -0
- package/dist/shared/artifact-tools/descriptions.js +11 -0
- package/dist/shared/artifact-tools/instructions.js +14 -0
- package/dist/shared/artifact-tools/schemas.js +119 -0
- package/dist/shared/artifact-transitions/constants.js +21 -0
- package/dist/shared/artifact-transitions/schemas.js +111 -0
- package/dist/shared/artifact-transitions/transitions.js +61 -0
- package/dist/shared/artifact-transitions/types.js +1 -0
- package/dist/shared/artifacts/constants.js +34 -0
- package/dist/shared/artifacts/mcp-constants.js +24 -0
- package/dist/shared/artifacts/normalize.js +9 -0
- package/dist/shared/artifacts/schemas.js +820 -0
- package/dist/shared/artifacts/search.js +101 -0
- package/dist/shared/artifacts/types.js +1 -0
- package/dist/shared/events/schemas.js +1 -1
- package/dist/shared/events/types.js +1 -0
- package/dist/shared/git/constants.js +2 -0
- package/dist/shared/project-files/constants.js +18 -0
- package/dist/shared/project-files/schemas.js +35 -0
- package/dist/shared/project-files/types.js +1 -0
- package/dist/shared/tasks/schemas.js +261 -0
- package/dist/shared/tasks/types.js +1 -0
- package/dist/shared/utils/artifact-body.js +10 -0
- package/dist/shared/utils/canonical-json.js +22 -0
- package/dist/shared/utils/is-safe-prefixed-filename.js +10 -0
- package/dist/shared/utils/is-uuid.js +4 -2
- package/dist/shared/utils/is-valid-artifact-type-id.js +9 -0
- package/dist/shared/utils/is-valid-filename-prefix.js +13 -0
- package/dist/shared/utils/markdown-path.js +4 -0
- package/dist/shared/utils/pdf-path.js +4 -0
- package/dist/web/assets/AccessPage-Dvj0aCkm.js +1 -0
- package/dist/web/assets/AddButton-CMC9qf51.js +4 -0
- package/dist/web/assets/AddToAgentButton-BitTdCEg.js +1 -0
- package/dist/web/assets/AppPage-Cozv2vqG.js +1 -0
- package/dist/web/assets/ArtifactDefinitionPicker-BQOoU60k.js +3 -0
- package/dist/web/assets/ArtifactDefinitionPicker-D3qdrxlC.css +1 -0
- package/dist/web/assets/ArtifactsPage-BJR-XYYs.js +2 -0
- package/dist/web/assets/ArtifactsPage-Cu1xdYNf.css +1 -0
- package/dist/web/assets/CancelButton-mkxRHwoZ.js +1 -0
- package/dist/web/assets/ClosePopup-BtDeDZsk.js +1 -0
- package/dist/web/assets/CreateButton-zCmb7snC.js +1 -0
- package/dist/web/assets/CreateReviewFabButton-BCg1TvFh.js +1 -0
- package/dist/web/assets/DialogHeader-CPL9OtDV.css +1 -0
- package/dist/web/assets/DialogHeader-CQvAZVx7.js +1 -0
- package/dist/web/assets/{ErrorNotFound-DtnO85PU.js → ErrorNotFound-fXS367lZ.js} +1 -1
- package/dist/web/assets/{IndexPage-D5PvyGrX.css → IndexPage-5V5GLtgi.css} +1 -1
- package/dist/web/assets/IndexPage-DMTZyWQA.js +1 -0
- package/dist/web/assets/MainLayout-DWJ_-EMV.js +7 -0
- package/dist/web/assets/MainLayout-Dy74m_q1.css +1 -0
- package/dist/web/assets/NoteVisualEditor-5rHBQ3j0.js +1054 -0
- package/dist/web/assets/{NoteVisualEditor-BYikrY7Q.css → NoteVisualEditor-DE4ACTRJ.css} +1 -1
- package/dist/web/assets/NotesPage-C_eoCUl7.css +1 -0
- package/dist/web/assets/NotesPage-CjaxjLBW.js +4 -0
- package/dist/web/assets/{PdfPreview-kL_nk9my.js → PdfPreview-2NQ5gkjh.js} +2 -2
- package/dist/web/assets/ProjectFilePage-B-WUUxnq.js +1 -0
- package/dist/web/assets/QBtn-V3VXtJXi.js +1 -0
- package/dist/web/assets/QFab-CxpedhPI.js +1 -0
- package/dist/web/assets/QForm-ClpSneeD.js +1 -0
- package/dist/web/assets/QInput-BnqLJGXl.js +1 -0
- package/dist/web/assets/QScrollArea-js5dkUz1.js +1 -0
- package/dist/web/assets/QSpace-C7iAnGlU.js +1 -0
- package/dist/web/assets/QTabs-DIy5gWMb.js +1 -0
- package/dist/web/assets/QVirtualScroll-DkD0TczV.js +1 -0
- package/dist/web/assets/ReviewDetailPage-BgxcsTqY.css +1 -0
- package/dist/web/assets/ReviewDetailPage-Bmc8DmSu.js +1 -0
- package/dist/web/assets/ReviewsPage-DR1IWCmu.js +1 -0
- package/dist/web/assets/SearchInput-BRyN93Z1.js +1 -0
- package/dist/web/assets/StatusSwitcher-CpkH8BuC.js +1 -0
- package/dist/web/assets/{StatusSwitcher-D6i8_OfR.css → StatusSwitcher-DaE1ufAA.css} +1 -1
- package/dist/web/assets/TagFilterField-Bl3FBkB5.js +1 -0
- package/dist/web/assets/TagFilterField-DjUH0m3_.css +1 -0
- package/dist/web/assets/{TerminalView-617DZuPt.css → TerminalView-BCeke4xd.css} +1 -1
- package/dist/web/assets/{TerminalView-BsO_SZut.js → TerminalView-tEyLM5qZ.js} +12 -12
- package/dist/web/assets/{abnfDiagram-O67JEVCF-qUSrlp_c.js → abnfDiagram-O67JEVCF-DN9xiV9p.js} +1 -1
- package/dist/web/assets/{access-store-Q8nkv4Nb.js → access-store-CwkOl71l.js} +1 -1
- package/dist/web/assets/{agent-store-D2jgeBgi.js → agent-store-Grbx4jkz.js} +5 -5
- package/dist/web/assets/architecture-7GRP2DOG-DmaYCP8T.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-D7RC6OS2.js → architectureDiagram-NJMV4G6O-BVAJOucG.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-CmNtTpfK.js → blockDiagram-BEXU5L5S-TJAfSX44.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-CKCcu1qx.js → c4Diagram-YGBWAQC7-5UHyT_tW.js} +1 -1
- package/dist/web/assets/channel-B4_5H-fb.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-Drz6oeKx.js → chunk-3FUC2YCW-CelgLyJi.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-RItzEgV_.js → chunk-5DYCD2WN-bhEAVIbQ.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-D6hKdOv7.js → chunk-742MDFTN-ByLz4EcD.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-Dk7-Eior.js → chunk-7INBJB4K-CK2i0csa.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-Cw8E0Nkn.js → chunk-7M6MHVWA-CoCiujjC.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-t-mTeNXF.js → chunk-7PRAP22T-CY4NCrCY.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-C7j2atV8.js → chunk-GTNCS2PH-DMB7bQGB.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-CH9T9LZO.js → chunk-GWA4HPMP-F6qclPTR.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-yXOO37xD.js → chunk-MBY4JIJT-CSiiBtlX.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-C-s2UtAR.js → chunk-NETBCI7D-CBh7GChL.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-5Me_aV8o.js → chunk-O7XYJQB3-B2aqz0sN.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-BQaibeff.js → chunk-UA2S7LBM-B_-P_eR0.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-UBcOE4Lf.js → chunk-WEXAMYUT-19nMFxzK.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-BsA8o3Pt.js → chunk-Z7XXMR3K-Co7gzRY1.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS--KwSI1U0.js → chunk-ZIGJFQKS-Eoi4K5bK.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-pMF8oCmq.js → classDiagram-v2-NBCMYWYE-CfFGzL-A.js} +1 -1
- package/dist/web/assets/copy-code-DaqEfdXL.js +1 -0
- package/dist/web/assets/create-DedXY-KP.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-DFgtXolF.js → cynefin-OW5HDTMX-DdZXl3KU.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-CPY0JRyq.js → cynefinDiagram-VND7K2PF-WU21nYH-.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-KdjJKAB7.js → dagre-6A5THRUB-48z3F8Pl.js} +1 -1
- package/dist/web/assets/date-DZIuZp-w.js +1 -0
- package/dist/web/assets/devices-store-Bfk2wdrQ.js +1 -0
- package/dist/web/assets/{diagram-22UHCM2B-BDcOcvF_.js → diagram-22UHCM2B-B0AZgJW9.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-6j2nlZL3.js → diagram-3UASUU5V-CzEzrxuO.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-BN8cdxED.js → diagram-ATOU4E4O-w6h1bD3w.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-cWPiXHkr.js → diagram-CDSNMT55-BdTHQd_o.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-DcgkH_Vl.js → diagram-MLGK6HIB-pB4cj6c2.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-D_e86L99.js → diagram-MPIPVDR6-C7C06qyV.js} +1 -1
- package/dist/web/assets/diff-store-DBDL5Hw5.js +93 -0
- package/dist/web/assets/{dist-Bdsq-Ze1.js → dist-B7Cu0FBW.js} +1 -1
- package/dist/web/assets/{dist-BVVhgjWT.js → dist-B8i8y3GY.js} +1 -1
- package/dist/web/assets/{dist-CjkUzU4s.js → dist-BNgPhnKo.js} +1 -1
- package/dist/web/assets/{dist-Bz40Nd9I.js → dist-BWoPV5P3.js} +1 -1
- package/dist/web/assets/{dist-BP40mvwJ.js → dist-CJ7isxli.js} +1 -1
- package/dist/web/assets/{dist-C9enVshI.js → dist-CTJJYvYG.js} +1 -1
- package/dist/web/assets/{dist-BwpyANeW.js → dist-CZz0KXAA.js} +1 -1
- package/dist/web/assets/{dist-CU43ytsh.js → dist-DNpwgDba.js} +1 -1
- package/dist/web/assets/{dist-BMFtkY2y.js → dist-DWCdhk-O.js} +1 -1
- package/dist/web/assets/{dist-MUQaJRtC.js → dist-DqSUdjG9.js} +1 -1
- package/dist/web/assets/{dist-CLCP7f3x.js → dist-Dy9K9LOR.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-D8xHOFOi.js → ebnfDiagram-ZINNZB2B-75vt7539.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-JiDwNyLy.js → elk-276RUBZZ-DXDjJVTy.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-D3J6u81a.js → erDiagram-OPXOYQCR-tNNInTmk.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-QMRehQtM.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-DJAgsbXC.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-nvAVhbgI.js → ganttDiagram-FUAMR5RP-h3E8lnCd.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-Bv_wkB2T.js → gitGraph-4MIJSDKK-BUwY-iv2.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-DxxwadhH.js → gitGraphDiagram-X574FWY7-C0AgQH5k.js} +1 -1
- package/dist/web/assets/i18n-CfTd46OX.js +1 -0
- package/dist/web/assets/icons-BoaZfGBj.js +1 -0
- package/dist/web/assets/index-BQU3KK1E.css +1 -0
- package/dist/web/assets/index-BTpokWxr.js +2 -0
- package/dist/web/assets/{info-A6RAGUB7-Dw18yeIE.js → info-A6RAGUB7-DVMZ4hdK.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-C3BrgjYg.js → infoDiagram-VRGFBTTK-kLaGXGQ2.js} +1 -1
- package/dist/web/assets/init-app-Bxc_l2yT.js +1 -0
- package/dist/web/assets/{is-D0waCELe.js → is-B1kBQ5Va.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-DU2p9szX.js → ishikawaDiagram-OU5B5YK6-Oj7bZlyo.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-Bs2zQnew.js → journeyDiagram-ZHPQQLJL-BG7V2Srs.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-pqXd5uNi.js → kanban-definition-PNTS6WVX-CIJiDE6d.js} +1 -1
- package/dist/web/assets/{line-CN4Dxpei.js → line-D2vEKYRZ.js} +1 -1
- package/dist/web/assets/markdown-Bm2wfvok.js +16 -0
- package/dist/web/assets/mdi-Byw1NZrZ.js +1 -0
- package/dist/web/assets/{mermaid-parser.core-DZqDOGRM.js → mermaid-parser.core-BcBEWjW6.js} +3 -3
- package/dist/web/assets/{mermaid.core-CUndCvkz.js → mermaid.core-DobO0deo.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-DyK7YqB7.js → mindmap-definition-NLK3R4M7-y7Qz5gBF.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-DvlW_0BP.js → packet-AYTQ26CC-oRQgKr9d.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-k2e_-FWQ.js → pegDiagram-GJSIUBJH-RIU8XOHq.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-B99zaYjO.js → pie-WAS4IAKB-qmOlsLkx.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-BPbS05lK.js → pieDiagram-5QR66LMP-BM932TDA.js} +1 -1
- package/dist/web/assets/pinia-BfqEuvsi.js +1 -0
- package/dist/web/assets/pinia-Cmp1GI8k.js +1 -0
- package/dist/web/assets/preview-store-D7qF6SgX.js +2 -0
- package/dist/web/assets/project-files-store-OhGs_H7U.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-DqNNdsd2.js → quadrantDiagram-O4NWA36T-qQZYwTy7.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-BIAAr9LO.js → radar-RG4KPBEZ-Kdglk6eY.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-DYxr-1-f.js → railroad-74A4TZTK-kVhMYx4I.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-MiKgjOXr.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BFL9h7Zx.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-ChGGfIn7.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-TNg-LqRR.js → railroadDiagram-XR7U4H2S-K9SrOGcT.js} +1 -1
- package/dist/web/assets/render-Brao9Nj2.js +1 -0
- package/dist/web/assets/repository-store-DZgQ5H7J.js +1 -0
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-hKAyJR6W.js → requirementDiagram-PLB6GJNP-CtA9ZKEx.js} +1 -1
- package/dist/web/assets/runtime-core.esm-bundler-DDnHUoIy.js +1 -0
- package/dist/web/assets/{safe-storage-B1Aq5CGi.js → safe-storage-CvVvrm4v.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-_MSL-tES.js → sankeyDiagram-IPEJSGJF-BH_t4exx.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-Byq-joBG.js → sequenceDiagram-PO4LG4MO-hPNPqpDa.js} +1 -1
- package/dist/web/assets/single-flight-CyDZB3rb.js +1 -0
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-BR2jAgaa.js → stateDiagram-v2-GCMORJYK-BVmjZDRD.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-Nt8ojGKK.js → swimlanes-2SLR337P-Tgk8-6jj.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-W1zDrK61.js +8 -0
- package/dist/web/assets/{terminal-store-D0abibMa.js → terminal-store-CKgJjtC6.js} +1 -1
- package/dist/web/assets/theme-slSxLNv6.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-BtihLvxk.js → timeline-definition-EJHVYXUP-lAIHEGe8.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-D5HzXeKd.js → treeView-Q6P3EWNA-CdvzKtWf.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-Dovjuutg.js → treemap-WGGIJYW6-BxDSJfrJ.js} +1 -1
- package/dist/web/assets/uid-B1JNhznI.js +1 -0
- package/dist/web/assets/use-add-agent-context-BSHGL5iV.js +1 -0
- package/dist/web/assets/use-app-theme-fmVV2QM8.js +1 -0
- package/dist/web/assets/use-countdown-D6_qSOZJ.js +1 -0
- package/dist/web/assets/use-notify-error-Dx2D9IEP.js +1 -0
- package/dist/web/assets/use-panel-width-BYOPuHat.js +1 -0
- package/dist/web/assets/use-router-link-D7geCGQI.js +1 -0
- package/dist/web/assets/use-row-height-BK8kNihW.js +1 -0
- package/dist/web/assets/use-server-events-CvcbsbIf.js +1 -0
- package/dist/web/assets/use-virtual-scroll-DixIsJk4.js +1 -0
- package/dist/web/assets/use-workspace-navigation-AaRMWmjQ.js +1 -0
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-DSwRE8zj.js → usecaseDiagram-POWQR4AR-BH3KvMpz.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-C-B8yP1K.js → vennDiagram-UO4OBE2U-Dfi-raKp.js} +1 -1
- package/dist/web/assets/viewport-store-NqQS7itT.js +1 -0
- package/dist/web/assets/{vue-i18n.runtime-CEtihHHL.js → vue-i18n.runtime-JOo8b7-t.js} +2 -2
- package/dist/web/assets/{wardley-WFR3VGLG-wWgaZuh6.js → wardley-WFR3VGLG-DmbtSM24.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DbhenrHd.js → wardleyDiagram-VNRHLVJA-DIzhGvI7.js} +1 -1
- package/dist/web/assets/weighted-lru-BFtZKA_y.js +1 -0
- package/dist/web/assets/workspace-navigation-xfxULfM7.js +1 -0
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-CxFDmAEt.js → xychartDiagram-PMCCYNJV-XgqSHH_N.js} +1 -1
- package/dist/web/index.html +10 -8
- package/package.json +5 -3
- package/dist/web/assets/AccessPage-_OKn9U0P.js +0 -1
- package/dist/web/assets/AppPage-oQ9Rz0be.js +0 -1
- package/dist/web/assets/CreateButton-D1EtoicG.js +0 -1
- package/dist/web/assets/CreateReviewFabButton-BxvT3i9e.js +0 -1
- package/dist/web/assets/IndexPage-gdD7BHxA.js +0 -1
- package/dist/web/assets/MainLayout-47PTLKZf.js +0 -6
- package/dist/web/assets/MainLayout-ikSrFqBh.css +0 -1
- package/dist/web/assets/NoteVisualEditor-BQ6mVfj0.js +0 -1054
- package/dist/web/assets/NotesPage-DNR9iW_G.js +0 -4
- package/dist/web/assets/NotesPage-_8naQciW.css +0 -1
- package/dist/web/assets/PanelResize-CAQFVc8b.js +0 -1
- package/dist/web/assets/QBtn-CB-yw_iP.js +0 -1
- package/dist/web/assets/QExpansionItem-Dr5oXO0m.js +0 -4
- package/dist/web/assets/QFab-DPz8RiP0.js +0 -1
- package/dist/web/assets/QForm-CASiKhGm.js +0 -1
- package/dist/web/assets/QInput-k34nCPWg.js +0 -1
- package/dist/web/assets/QSelect-CaKn9Tkg.js +0 -1
- package/dist/web/assets/QSpace-Di_bZQY6.js +0 -1
- package/dist/web/assets/QVirtualScroll-oGN_Tl5J.js +0 -1
- package/dist/web/assets/ReviewDetailPage-BKgIprY0.js +0 -1
- package/dist/web/assets/ReviewDetailPage-CkCilMNW.css +0 -1
- package/dist/web/assets/ReviewsPage-6PJ6wofN.js +0 -1
- package/dist/web/assets/StatusSwitcher-s9w6iKpR.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-D6PShS0_.js +0 -1
- package/dist/web/assets/channel-D6lb5DbY.js +0 -1
- package/dist/web/assets/create-B9Tn5Xvi.js +0 -1
- package/dist/web/assets/devices-store-CrZ4MxyY.js +0 -1
- package/dist/web/assets/diff-store-ByxMCKoa.js +0 -93
- package/dist/web/assets/eventmodeling-NTZA5JFV-QCwZLvmm.js +0 -1
- package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-CgF90hDi.js +0 -1
- package/dist/web/assets/i18n-DDCqtTTW.js +0 -1
- package/dist/web/assets/index-BNHt6zmP.js +0 -2
- package/dist/web/assets/index-BwbM_RKi.css +0 -1
- package/dist/web/assets/init-app-BouXTk8p.js +0 -1
- package/dist/web/assets/markdown-jR0fhe4d.js +0 -15
- package/dist/web/assets/pinia-BETEFLDG.js +0 -1
- package/dist/web/assets/pinia-DO0AdKGg.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-GESjhc3t.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cybu0A7J.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DCHxrtbI.js +0 -1
- package/dist/web/assets/runtime-core.esm-bundler-CAvPiMpd.js +0 -1
- package/dist/web/assets/single-flight-DLCbzLI3.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-Ds1hVYqh.js +0 -8
- package/dist/web/assets/theme-QEcOl0lf.js +0 -1
- package/dist/web/assets/use-add-agent-context-BaPUIO93.js +0 -1
- package/dist/web/assets/use-app-theme-B1cdbnHn.js +0 -1
- package/dist/web/assets/use-countdown-CTag4T13.js +0 -1
- package/dist/web/assets/use-panel-width-CB-0S3zX.js +0 -1
- package/dist/web/assets/use-router-link-Dr9wnnyZ.js +0 -1
- package/dist/web/assets/use-row-height-DQHVMtUe.js +0 -1
- package/dist/web/assets/use-server-events-BrBCbeHu.js +0 -1
- package/dist/web/assets/use-virtual-scroll-DG0UKjVk.js +0 -1
- package/dist/web/assets/weighted-lru-BSxRnMHR.js +0 -1
- /package/dist/web/assets/{PanelResize-XjrbOj2S.css → AddToAgentButton-XjrbOj2S.css} +0 -0
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@ staged/unstaged diff, сравнение веток и коммитов, бра
|
|
|
22
22
|
- Чаты с ACP-агентами (Claude Code, Cursor, Codex, pi): до 5 чатов во
|
|
23
23
|
вкладках, контекст — diff, файл целиком, ревью, вложения и `@`-файлы и папки из
|
|
24
24
|
репозитория. Подробнее — в разделе «Агенты» ниже.
|
|
25
|
+
- Раздел **Artifacts**: неизменяемые Markdown-документы, поиск и справочники,
|
|
26
|
+
комментарии по строкам и редакциям, проверка и подтверждение принятия,
|
|
27
|
+
контекст документов в чате и экспорт оригинала/истории.
|
|
25
28
|
- Ручной/авто выбор тёмной-светлой темы.
|
|
26
29
|
- CLI: `serve` (поднять сервер), `list`/`export` (быстрый доступ к
|
|
27
30
|
сохранённым ревью без веб-интерфейса).
|
|
@@ -130,8 +133,34 @@ Tailscale) разрешается флагом `--allowed-host <имя>`, его
|
|
|
130
133
|
- **Подтверждения.** Запросы разрешений приходят карточкой в чат. Для
|
|
131
134
|
отдельного чата можно включить автоподтверждение (кнопка-щит) — агент
|
|
132
135
|
тогда меняет файлы и выполняет команды без вашего ответа; включайте его
|
|
133
|
-
только для работы, которую готовы проверить и откатить.
|
|
134
|
-
|
|
136
|
+
только для работы, которую готовы проверить и откатить. Native tools pi
|
|
137
|
+
выполняются без ACP-подтверждений; его MCP-вызовы требуют разрешения.
|
|
138
|
+
- **Артефакты через MCP.** Агенты внутри работающего GitHuman получают
|
|
139
|
+
инструменты создания неизменяемых черновиков, чтения содержимого и истории,
|
|
140
|
+
просмотра связей, работы с задачами и типами, [поиска с опечатками](docs/artifact-search.md).
|
|
141
|
+
Проверены Claude Code и штатный локальный мост pi; остальные
|
|
142
|
+
ACP-пресеты пока не проверены на поддержку этих инструментов.
|
|
143
|
+
Самостоятельный MCP без GitHuman не поддерживается. Раздел Artifacts показывает
|
|
144
|
+
документы и позволяет подтверждать точные предложения агента. Статус документа
|
|
145
|
+
меняется кнопкой в шапке: одно «Да» запускает проверку и применение на сервере;
|
|
146
|
+
принятие документов требует проверки и пользовательского разрешения;
|
|
147
|
+
[жизненный цикл и API](docs/artifact-lifecycle.md).
|
|
148
|
+
Ревью артефактов отложено; обсуждения и блокирующие замечания удалены.
|
|
149
|
+
- **Локальный мост pi** — в пакет включена команда
|
|
150
|
+
`githuman pi-acp --stdio`, использующая установленный pi, его авторизацию
|
|
151
|
+
и выбор моделей. MCP-вызовы проходят отдельное ACP-подтверждение;
|
|
152
|
+
валидатор получает серверный профиль с запретом native tools. Штатный
|
|
153
|
+
preset использует этот мост без runtime npx и подбора внешнего адаптера.
|
|
154
|
+
Явные пользовательские command/args overrides сохраняются. Для ручной
|
|
155
|
+
проверки нужен авторизованный pi с настроенной моделью.
|
|
156
|
+
При аварийном падении/SIGKILL pi запущенные native shell-команды могут
|
|
157
|
+
продолжить работу после закрытия чата. Очистка таких процессов — best effort;
|
|
158
|
+
это явно принятое ограничение обычного чата, не ослабление MCP-подтверждений.
|
|
159
|
+
Валидация через прежнюю или произвольную команду pi недоступна.
|
|
160
|
+
Проверки выполняются на Linux; macOS-проверки пропущены по поручению
|
|
161
|
+
пользователя, поддержка macOS не подтверждена. Windows не поддерживается.
|
|
162
|
+
Подробное руководство по MCP-интеграции: [русский](docs/pi-mcp-integration.md) ·
|
|
163
|
+
[English](docs/pi-mcp-integration.en.md).
|
|
135
164
|
- **Свои команды запуска** — `<репозиторий>/.githuman/ght-agents.json`:
|
|
136
165
|
`{ "cursor": { "args": ["--model", "<модель>", "acp"] } }`. Допустимы поля
|
|
137
166
|
`command`, `args`, `env`; любое другое (в том числе опечатка) — ошибка
|
|
@@ -245,6 +274,37 @@ MIT, см. [LICENSE](LICENSE).
|
|
|
245
274
|
|
|
246
275
|
Для разработки на другом порту backend можно передать `GITHUMAN_DEV_API=http://localhost:3848` при запуске Quasar dev-сервера.
|
|
247
276
|
|
|
277
|
+
## Артефакты
|
|
278
|
+
|
|
279
|
+
Раздел **Artifacts** хранит неизменяемые Markdown-документы, созданные агентом
|
|
280
|
+
через MCP. Каталог поддерживает обычный поиск и поиск с опечатками, фильтры по
|
|
281
|
+
статусу, типу, задаче и тегу и группировку загруженных результатов; порядок
|
|
282
|
+
фиксирован (новые сверху), переключателя сортировки нет. Документ показывается
|
|
283
|
+
как Markdown; редактирования body и режима Source нет. Обсуждения, комментарии и
|
|
284
|
+
блокирующие замечания удалены (ADR 0097). Кнопка добавления в чат прикладывает
|
|
285
|
+
контекст документа; задачи чата назначаются через Tasks в панели чата.
|
|
286
|
+
|
|
287
|
+
Статус документа меняется кнопкой в шапке: меню показывает только допустимые
|
|
288
|
+
переходы, диалог — точные последствия и выбор валидатора. Одно «Да» запускает на
|
|
289
|
+
сервере проверку независимым валидатором и применение при свежем pass; повторного
|
|
290
|
+
подтверждения нет, работа продолжается после закрытия вкладки и показывает
|
|
291
|
+
стадию, отмену и результат проверки. Это отдельный путь от MCP/API жизненного
|
|
292
|
+
цикла (`change_sets`, `actions`, `validations`), где агент запрашивает действие
|
|
293
|
+
и использует выданное пользователем разрешение; очереди таких запросов в
|
|
294
|
+
интерфейсе нет. Подробности и ограничения — в
|
|
295
|
+
[жизненном цикле](docs/artifact-lifecycle.md) и
|
|
296
|
+
[матрице возможностей](docs/artifact-capabilities.md).
|
|
297
|
+
|
|
298
|
+
Если текст документа не читается (повреждённый или недоступный файл), метаданные,
|
|
299
|
+
история, связи и выгрузка JSON остаются доступны, а тело заменяется ошибкой с
|
|
300
|
+
кнопкой «Retry»; оригинал при повреждении не выгружается. Исходный путь
|
|
301
|
+
импорта (`sourcePath`), если он есть, показан в метаданных как обычный текст.
|
|
302
|
+
|
|
303
|
+
Download artifact выгружает точный оригинал с историческим frontmatter либо
|
|
304
|
+
JSON актуальных метаданных и полной истории. Текущие метаданные и исходный
|
|
305
|
+
frontmatter показаны отдельно. История и файлы сохраняются в `.githuman/`;
|
|
306
|
+
существующие Markdown-файлы репозитория автоматически не импортируются.
|
|
307
|
+
|
|
248
308
|
## Проектные заметки
|
|
249
309
|
|
|
250
310
|
Раздел **Notes** хранит Markdown-заметки в `.githuman/notes/` относительно
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { Value } from '@sinclair/typebox/value';
|
|
5
|
+
import { ARTIFACT_TASK_INSTRUCTIONS } from "../../shared/artifact-tools/instructions.js";
|
|
6
|
+
import { ARTIFACT_TOOL_DESCRIPTIONS } from "../../shared/artifact-tools/descriptions.js";
|
|
7
|
+
import { ArtifactToolSchemas } from "../../shared/artifact-tools/schemas.js";
|
|
8
|
+
import { ARTIFACT_AGENT_KEY_ENV, ARTIFACT_AGENT_KEY_PATTERN, ARTIFACT_AGENT_URL_ENV, ARTIFACT_BRIDGE_PATH_PREFIX, ARTIFACT_MCP_MAX_MESSAGE_BYTES } from "../../shared/artifacts/mcp-constants.js";
|
|
9
|
+
import { isRecord } from "../../shared/utils/guards.js";
|
|
10
|
+
import { getAppVersion } from "../../server/app-version.js";
|
|
11
|
+
// Fail boundedly when serve disappears; retries use the original request ID.
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
13
|
+
// MCP tool names allow no dots, so operation names map dots to underscores.
|
|
14
|
+
const TOOLS = new Map(Object.entries(ArtifactToolSchemas).map(([operation, schema]) => [
|
|
15
|
+
operation.replaceAll('.', '_'),
|
|
16
|
+
{
|
|
17
|
+
operation,
|
|
18
|
+
schema,
|
|
19
|
+
description: ARTIFACT_TOOL_DESCRIPTIONS[operation] ?? schema.description
|
|
20
|
+
}
|
|
21
|
+
]));
|
|
22
|
+
function toolError(text) {
|
|
23
|
+
return { isError: true, content: [{ type: 'text', text }] };
|
|
24
|
+
}
|
|
25
|
+
export async function runMcp(argv) {
|
|
26
|
+
if (argv.length !== 1 || argv[0] !== '--stdio') {
|
|
27
|
+
throw new Error('Usage: githuman mcp --stdio');
|
|
28
|
+
}
|
|
29
|
+
const endpoint = process.env[ARTIFACT_AGENT_URL_ENV];
|
|
30
|
+
const key = process.env[ARTIFACT_AGENT_KEY_ENV];
|
|
31
|
+
if (!endpoint || !key || !ARTIFACT_AGENT_KEY_PATTERN.test(key)) {
|
|
32
|
+
throw new Error('Artifact MCP requires a running GitHuman agent session.');
|
|
33
|
+
}
|
|
34
|
+
const url = new URL(endpoint);
|
|
35
|
+
if (url.protocol !== 'http:' ||
|
|
36
|
+
url.username ||
|
|
37
|
+
url.password ||
|
|
38
|
+
url.pathname !== '/' ||
|
|
39
|
+
url.search ||
|
|
40
|
+
url.hash) {
|
|
41
|
+
throw new Error('Invalid GitHuman server address.');
|
|
42
|
+
}
|
|
43
|
+
const server = new Server({ name: 'githuman-artifacts', version: getAppVersion() }, { capabilities: { tools: {} }, instructions: ARTIFACT_TASK_INSTRUCTIONS });
|
|
44
|
+
const active = new AbortController();
|
|
45
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
46
|
+
tools: [...TOOLS].map(([name, { schema, description }]) => ({
|
|
47
|
+
name,
|
|
48
|
+
description,
|
|
49
|
+
inputSchema: { ...schema, type: 'object' }
|
|
50
|
+
}))
|
|
51
|
+
}));
|
|
52
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
53
|
+
const tool = TOOLS.get(request.params.name);
|
|
54
|
+
if (!tool) {
|
|
55
|
+
return toolError(`Unknown artifact tool "${request.params.name}".`);
|
|
56
|
+
}
|
|
57
|
+
const args = request.params.arguments ?? {};
|
|
58
|
+
// The message names the field and the rule, never the rejected value.
|
|
59
|
+
const invalid = Value.Errors(tool.schema, args).First();
|
|
60
|
+
if (invalid) {
|
|
61
|
+
return toolError(`Invalid parameters for ${request.params.name}: ${invalid.path || '/'} ${invalid.message}.`);
|
|
62
|
+
}
|
|
63
|
+
let response;
|
|
64
|
+
try {
|
|
65
|
+
response = await fetch(new URL(`${ARTIFACT_BRIDGE_PATH_PREFIX}${tool.operation}`, url), {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
redirect: 'error',
|
|
68
|
+
headers: {
|
|
69
|
+
authorization: `Bearer ${key}`,
|
|
70
|
+
'content-type': 'application/json'
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(args),
|
|
73
|
+
signal: AbortSignal.any([
|
|
74
|
+
active.signal,
|
|
75
|
+
extra.signal,
|
|
76
|
+
AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
77
|
+
])
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const name = error instanceof Error ? error.name : '';
|
|
82
|
+
return toolError(name === 'TimeoutError' || name === 'AbortError'
|
|
83
|
+
? 'The GitHuman artifact request timed out or was cancelled.'
|
|
84
|
+
: 'GitHuman artifact server is unavailable.');
|
|
85
|
+
}
|
|
86
|
+
let result;
|
|
87
|
+
try {
|
|
88
|
+
result = await response.json();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
result = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (!isRecord(result)) {
|
|
94
|
+
return toolError(`GitHuman artifact server returned an unreadable response (HTTP ${response.status}).`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
isError: !response.ok,
|
|
98
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
99
|
+
structuredContent: result
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const transport = new StdioServerTransport(undefined, undefined, {
|
|
103
|
+
maxBufferSize: ARTIFACT_MCP_MAX_MESSAGE_BYTES
|
|
104
|
+
});
|
|
105
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- MCP SDK exposes a callback property, not EventTarget
|
|
106
|
+
server.onclose = () => active.abort();
|
|
107
|
+
process.stdin.once('end', () => {
|
|
108
|
+
active.abort();
|
|
109
|
+
void server.close();
|
|
110
|
+
});
|
|
111
|
+
await server.connect(transport);
|
|
112
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
import { Readable, Writable } from 'node:stream';
|
|
3
|
+
import { createPiAgent } from "../../server/adapters/pi-acp/agent.js";
|
|
4
|
+
export function runPiAcp(argv) {
|
|
5
|
+
if (argv.length !== 1 || argv[0] !== '--stdio') {
|
|
6
|
+
throw new Error('Usage: githuman pi-acp --stdio');
|
|
7
|
+
}
|
|
8
|
+
const profile = process.env.GITHUMAN_PI_PROFILE ?? 'chat';
|
|
9
|
+
if (profile !== 'chat' && profile !== 'validator') {
|
|
10
|
+
throw new Error('Invalid trusted pi launch profile');
|
|
11
|
+
}
|
|
12
|
+
const agent = createPiAgent(profile);
|
|
13
|
+
agent.connect(acp.ndJsonStream(Writable.toWeb(process.stdout),
|
|
14
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Node and DOM streams have equivalent runtime contracts.
|
|
15
|
+
Readable.toWeb(process.stdin)));
|
|
16
|
+
const shutdown = () => {
|
|
17
|
+
agent.close();
|
|
18
|
+
};
|
|
19
|
+
process.once('SIGTERM', shutdown);
|
|
20
|
+
process.once('SIGINT', shutdown);
|
|
21
|
+
}
|
package/dist/cli/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_DB_PREFIX } from "../shared/app/storage-constants.js";
|
|
1
2
|
import { isIP } from 'node:net';
|
|
2
3
|
import { parseArgs } from 'node:util';
|
|
4
|
+
import { isValidFilenamePrefix } from "../shared/utils/is-valid-filename-prefix.js";
|
|
3
5
|
import { isLoopbackHost } from "../shared/network/loopback.js";
|
|
4
6
|
import { WILDCARD_HOSTS } from "../server/utils/lan-addresses.js";
|
|
5
7
|
const DEFAULT_PORT = 3847;
|
|
@@ -13,7 +15,7 @@ const HOST_NAME_PATTERN = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:
|
|
|
13
15
|
* without a prefix, running both tools against the same repository would
|
|
14
16
|
* silently corrupt or crash on the other tool's database file.
|
|
15
17
|
*/
|
|
16
|
-
export
|
|
18
|
+
export { DEFAULT_DB_PREFIX };
|
|
17
19
|
/**
|
|
18
20
|
* `GITHUMAN_DB_PREFIX` is intentionally not one of the env vars the original
|
|
19
21
|
* mcollina/githuman CLI reads (`GITHUMAN_DB_PATH`, `GITHUMAN_TOKEN`), so
|
|
@@ -94,6 +96,10 @@ export function parseServeArgs(argv) {
|
|
|
94
96
|
const host = resolveHost(typeof values.host === 'string' ? values.host : undefined);
|
|
95
97
|
const open = values['no-open'] === true ? false : values.open !== false;
|
|
96
98
|
const dbPrefix = resolveDbPrefix(typeof values['db-prefix'] === 'string' ? values['db-prefix'] : undefined);
|
|
99
|
+
// Only `serve` creates artifact files, so only it needs a filename-safe prefix.
|
|
100
|
+
if (!isValidFilenamePrefix(dbPrefix)) {
|
|
101
|
+
throw new Error(`Invalid --db-prefix value "${dbPrefix}": use at most 100 characters and none of < > : " / \\ | ? * or control characters. Set it with --db-prefix or GITHUMAN_DB_PREFIX.`);
|
|
102
|
+
}
|
|
97
103
|
const apiDocs = values['api-docs'] === true;
|
|
98
104
|
const allowedHosts = resolveAllowedHosts(Array.isArray(values['allowed-host']) ? values['allowed-host'] : []);
|
|
99
105
|
return {
|
package/dist/cli/index.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { errorMessage } from "../shared/utils/error-message.js";
|
|
3
3
|
import { writeFileSync } from 'node:fs';
|
|
4
|
-
import { parseServeArgs } from "./config.js";
|
|
5
|
-
import { startServer, resolveRepositoryPath, resolveReviewsDbPath } from "./server-runtime.js";
|
|
6
|
-
import { formatStartupMessage } from "./startup-message.js";
|
|
7
|
-
import { lanAddresses } from "../server/utils/lan-addresses.js";
|
|
8
|
-
import { parseListArgs, runList } from "./commands/list.js";
|
|
9
|
-
import { parseExportArgs, runExport } from "./commands/export.js";
|
|
10
|
-
import { createFileDatabase } from "../server/db/index.js";
|
|
11
4
|
import { getAppVersion } from "../server/app-version.js";
|
|
12
5
|
// Force exit when a graceful close hangs. It must exceed the terminal kill time: ps timeout plus grace.
|
|
13
6
|
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
14
7
|
/** Opens the reviews DB for the repo at cwd, shared by `list` and `export`. */
|
|
15
8
|
async function openReviewsDb(dbPrefix) {
|
|
9
|
+
const { resolveRepositoryPath, resolveReviewsDbPath } = await import("./server-runtime.js");
|
|
10
|
+
const { createFileDatabase } = await import("../server/db/index.js");
|
|
16
11
|
const repositoryPath = await resolveRepositoryPath(process.cwd());
|
|
17
12
|
return {
|
|
18
13
|
db: createFileDatabase(resolveReviewsDbPath(repositoryPath, dbPrefix)),
|
|
@@ -20,10 +15,24 @@ async function openReviewsDb(dbPrefix) {
|
|
|
20
15
|
};
|
|
21
16
|
}
|
|
22
17
|
async function main(argv) {
|
|
18
|
+
if (argv[0] === 'pi-acp') {
|
|
19
|
+
const { runPiAcp } = await import("./commands/pi-acp.js");
|
|
20
|
+
runPiAcp(argv.slice(1));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (argv[0] === 'mcp') {
|
|
24
|
+
const { runMcp } = await import("./commands/mcp.js");
|
|
25
|
+
await runMcp(argv.slice(1));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
23
28
|
console.log(`githuman v${getAppVersion()}`);
|
|
24
29
|
const [command, ...rest] = argv;
|
|
25
30
|
switch (command) {
|
|
26
31
|
case 'serve': {
|
|
32
|
+
const { parseServeArgs } = await import("./config.js");
|
|
33
|
+
const { startServer } = await import("./server-runtime.js");
|
|
34
|
+
const { formatStartupMessage } = await import("./startup-message.js");
|
|
35
|
+
const { lanAddresses } = await import("../server/utils/lan-addresses.js");
|
|
27
36
|
const options = parseServeArgs(rest);
|
|
28
37
|
const { app, url } = await startServer(options);
|
|
29
38
|
let stopping = false;
|
|
@@ -52,12 +61,14 @@ async function main(argv) {
|
|
|
52
61
|
return;
|
|
53
62
|
}
|
|
54
63
|
case 'list': {
|
|
64
|
+
const { parseListArgs, runList } = await import("./commands/list.js");
|
|
55
65
|
const options = parseListArgs(rest);
|
|
56
66
|
const { db } = await openReviewsDb(options.dbPrefix);
|
|
57
67
|
console.log(runList(db, options));
|
|
58
68
|
return;
|
|
59
69
|
}
|
|
60
70
|
case 'export': {
|
|
71
|
+
const { parseExportArgs, runExport } = await import("./commands/export.js");
|
|
61
72
|
const options = parseExportArgs(rest);
|
|
62
73
|
const { db, repositoryPath } = await openReviewsDb(options.dbPrefix);
|
|
63
74
|
const output = await runExport(db, options.id, options.format, repositoryPath);
|
|
@@ -71,7 +82,7 @@ async function main(argv) {
|
|
|
71
82
|
}
|
|
72
83
|
default:
|
|
73
84
|
console.error(`Unknown command: ${command ?? '(none)'}`);
|
|
74
|
-
console.error('Usage: githuman serve|list|export [options]');
|
|
85
|
+
console.error('Usage: githuman serve|list|export|mcp|pi-acp [options]');
|
|
75
86
|
process.exitCode = 1;
|
|
76
87
|
}
|
|
77
88
|
}
|
|
@@ -66,6 +66,7 @@ export async function startServer(options) {
|
|
|
66
66
|
projectPath: process.cwd(),
|
|
67
67
|
notesDir: options.notesDir,
|
|
68
68
|
db,
|
|
69
|
+
artifactPrefix: resolveDbPrefix(options.dbPrefix),
|
|
69
70
|
watchFiles: true,
|
|
70
71
|
apiDocs: options.apiDocs === true,
|
|
71
72
|
allowedHosts: options.allowedHosts ?? [],
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ArtifactToolSchemas } from "../../../shared/artifact-tools/schemas.js";
|
|
2
|
+
import { ARTIFACT_MCP_SERVER_NAME } from "../../../shared/artifacts/mcp-constants.js";
|
|
3
|
+
import { isRecord } from "../../../shared/utils/guards.js";
|
|
4
|
+
// MCP names replace operation dots with underscores on the stdio bridge.
|
|
5
|
+
const OPERATIONS = new Map(Object.entries(ArtifactToolSchemas).map(([operation, schema]) => [
|
|
6
|
+
operation.replaceAll('.', '_'),
|
|
7
|
+
{ operation, schema }
|
|
8
|
+
]));
|
|
9
|
+
// Claude's qualified name binds an operation to the configured server.
|
|
10
|
+
const CLAUDE_PREFIX = `mcp__${ARTIFACT_MCP_SERVER_NAME}__`;
|
|
11
|
+
export function artifactPermissionCall(request, previous, presetId) {
|
|
12
|
+
const call = request.toolCall;
|
|
13
|
+
const pi = call['_meta']?.githumanPi;
|
|
14
|
+
if (pi !== undefined &&
|
|
15
|
+
(!isRecord(pi) ||
|
|
16
|
+
pi.server !== ARTIFACT_MCP_SERVER_NAME ||
|
|
17
|
+
pi.registryGeneration !== 1 ||
|
|
18
|
+
call.name !== `${CLAUDE_PREFIX}${String(pi.tool)}`)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const claude = call['_meta']?.claudeCode;
|
|
22
|
+
const metadataName = isRecord(claude) ? claude.toolName : undefined;
|
|
23
|
+
const name = call.name ?? metadataName;
|
|
24
|
+
if (name !== undefined) {
|
|
25
|
+
if (typeof name !== 'string' || !name.startsWith(CLAUDE_PREFIX)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (metadataName !== undefined && metadataName !== name)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (previous?.name !== undefined &&
|
|
31
|
+
previous.name !== null &&
|
|
32
|
+
previous.name !== name) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (isRecord(claude) && claude.mcpServer !== undefined) {
|
|
36
|
+
if (!isRecord(claude.mcpServer) ||
|
|
37
|
+
claude.mcpServer.name !== ARTIFACT_MCP_SERVER_NAME) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const operation = OPERATIONS.get(name.slice(CLAUDE_PREFIX.length));
|
|
42
|
+
return operation ? { ...operation, input: call.rawInput } : undefined;
|
|
43
|
+
}
|
|
44
|
+
// Cursor supplies MCP identity in the correlated tool update, not the permission card.
|
|
45
|
+
if (presetId === 'cursor' && call.kind === 'other') {
|
|
46
|
+
if (call.rawInput !== undefined ||
|
|
47
|
+
request['_meta']?.is_mcp_tool_approval !== undefined ||
|
|
48
|
+
!previous ||
|
|
49
|
+
previous.toolCallId !== call.toolCallId ||
|
|
50
|
+
previous.kind !== 'other' ||
|
|
51
|
+
(previous.name !== undefined && previous.name !== null)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const input = previous.rawInput;
|
|
55
|
+
if (!isRecord(input) ||
|
|
56
|
+
input.providerIdentifier !== ARTIFACT_MCP_SERVER_NAME ||
|
|
57
|
+
typeof input.toolName !== 'string' ||
|
|
58
|
+
!isRecord(input.args) ||
|
|
59
|
+
Object.keys(input).some(key => !['providerIdentifier', 'toolName', 'args'].includes(key))) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const operation = OPERATIONS.get(input.toolName);
|
|
63
|
+
return operation ? { ...operation, input: input.args } : undefined;
|
|
64
|
+
}
|
|
65
|
+
// Codex omits call details only for a correlated MCP approval, not an elicitation.
|
|
66
|
+
if (request['_meta']?.is_mcp_tool_approval !== true ||
|
|
67
|
+
call.rawInput !== undefined ||
|
|
68
|
+
call.kind !== 'execute') {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (!previous ||
|
|
72
|
+
previous.toolCallId !== call.toolCallId ||
|
|
73
|
+
previous.kind !== 'execute' ||
|
|
74
|
+
(previous.name !== undefined && previous.name !== null)) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const input = previous.rawInput;
|
|
78
|
+
if (!isRecord(input) ||
|
|
79
|
+
input.server !== ARTIFACT_MCP_SERVER_NAME ||
|
|
80
|
+
typeof input.tool !== 'string') {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
if (Object.keys(input).some(key => !['server', 'tool', 'arguments'].includes(key))) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const operation = OPERATIONS.get(input.tool);
|
|
87
|
+
return operation ? { ...operation, input: input.arguments } : undefined;
|
|
88
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { errorMessage } from "../../../shared/utils/error-message.js";
|
|
2
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
3
|
+
import { PiSession } from "./session.js";
|
|
4
|
+
export function createPiAgent(profile) {
|
|
5
|
+
let session;
|
|
6
|
+
let used = false;
|
|
7
|
+
let connection;
|
|
8
|
+
const app = acp
|
|
9
|
+
.agent({ name: 'githuman-pi' })
|
|
10
|
+
.onRequest(acp.methods.agent.initialize, () => ({
|
|
11
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
12
|
+
agentCapabilities: {
|
|
13
|
+
promptCapabilities: { image: true, embeddedContext: true },
|
|
14
|
+
mcpCapabilities: { http: false, sse: false },
|
|
15
|
+
loadSession: false
|
|
16
|
+
}
|
|
17
|
+
}))
|
|
18
|
+
.onRequest(acp.methods.agent.session.new, async (ctx) => {
|
|
19
|
+
if (used) {
|
|
20
|
+
throw new acp.RequestError(-32602, 'One pi bridge process supports exactly one session/new');
|
|
21
|
+
}
|
|
22
|
+
used = true;
|
|
23
|
+
session = new PiSession(profile, {
|
|
24
|
+
update: (sessionId, update) => ctx.client.notify(acp.methods.client.session.update, {
|
|
25
|
+
sessionId,
|
|
26
|
+
update
|
|
27
|
+
}),
|
|
28
|
+
permission: (request, signal) => ctx.client.request(acp.methods.client.session.requestPermission, request, { cancellationSignal: signal }),
|
|
29
|
+
failure: () => connection?.close(new Error('Pi bridge runtime or MCP failed'))
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
await session.start(ctx.params.cwd, ctx.params.mcpServers);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new acp.RequestError(-32000, errorMessage(error), {
|
|
36
|
+
githumanBridge: { version: 1, profile, phase: 'startup' }
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
sessionId: session.id,
|
|
41
|
+
configOptions: session.config,
|
|
42
|
+
_meta: {
|
|
43
|
+
githumanBridge: { version: 1, profile, registryGeneration: 1 }
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
})
|
|
47
|
+
.onRequest(acp.methods.agent.session.prompt, async (ctx) => {
|
|
48
|
+
if (!session) {
|
|
49
|
+
throw new acp.RequestError(-32602, 'Pi session has not been created');
|
|
50
|
+
}
|
|
51
|
+
session.assertSession(ctx.params.sessionId);
|
|
52
|
+
try {
|
|
53
|
+
return await session.prompt(ctx.params.prompt);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new acp.RequestError(-32000, errorMessage(error), {
|
|
57
|
+
githumanBridge: { version: 1, profile, phase: 'prompt' }
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.onNotification(acp.methods.agent.session.cancel, async (ctx) => {
|
|
62
|
+
if (!session) {
|
|
63
|
+
throw new acp.RequestError(-32602, 'Pi session has not been created');
|
|
64
|
+
}
|
|
65
|
+
session.assertSession(ctx.params.sessionId);
|
|
66
|
+
await session.cancel();
|
|
67
|
+
})
|
|
68
|
+
.onRequest(acp.methods.agent.session.setMode, async (ctx) => {
|
|
69
|
+
if (!session) {
|
|
70
|
+
throw new acp.RequestError(-32602, 'Pi session has not been created');
|
|
71
|
+
}
|
|
72
|
+
session.assertSession(ctx.params.sessionId);
|
|
73
|
+
await session.setConfig('thought_level', ctx.params.modeId);
|
|
74
|
+
return {};
|
|
75
|
+
})
|
|
76
|
+
.onRequest(acp.methods.agent.session.setConfigOption, async (ctx) => {
|
|
77
|
+
if (!session) {
|
|
78
|
+
throw new acp.RequestError(-32602, 'Pi session has not been created');
|
|
79
|
+
}
|
|
80
|
+
session.assertSession(ctx.params.sessionId);
|
|
81
|
+
if (typeof ctx.params.value !== 'string') {
|
|
82
|
+
throw new acp.RequestError(-32602, 'Invalid pi setting');
|
|
83
|
+
}
|
|
84
|
+
await session.setConfig(ctx.params.configId, ctx.params.value);
|
|
85
|
+
return { configOptions: session.config };
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
connect(stream) {
|
|
89
|
+
connection = app.connect(stream);
|
|
90
|
+
void connection.closed.then(() => session?.close());
|
|
91
|
+
return connection;
|
|
92
|
+
},
|
|
93
|
+
close() {
|
|
94
|
+
session?.close();
|
|
95
|
+
connection?.close();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
// Probe only flags needed for isolation. Do not infer capabilities from semver.
|
|
7
|
+
const REQUIRED_FLAGS = [
|
|
8
|
+
'--mode',
|
|
9
|
+
'--no-session',
|
|
10
|
+
'--no-extensions',
|
|
11
|
+
'--no-skills',
|
|
12
|
+
'--no-prompt-templates',
|
|
13
|
+
'--no-themes',
|
|
14
|
+
'--no-context-files',
|
|
15
|
+
'--no-builtin-tools',
|
|
16
|
+
'--offline'
|
|
17
|
+
];
|
|
18
|
+
// Bound runtime discovery before opening a session.
|
|
19
|
+
const PROBE_TIMEOUT_MS = 10000;
|
|
20
|
+
const execute = promisify(execFile);
|
|
21
|
+
export async function piLaunchArgs(command, profile, extension) {
|
|
22
|
+
if (process.platform !== 'linux' && process.platform !== 'darwin') {
|
|
23
|
+
throw new Error('Pi bridge requires Linux or macOS (macOS verification was skipped).');
|
|
24
|
+
}
|
|
25
|
+
let help;
|
|
26
|
+
const probeDir = await mkdtemp(join(tmpdir(), 'githuman-pi-probe-'));
|
|
27
|
+
try {
|
|
28
|
+
const result = await execute(command, ['--help'], {
|
|
29
|
+
cwd: probeDir,
|
|
30
|
+
env: {
|
|
31
|
+
PATH: process.env.PATH,
|
|
32
|
+
HOME: probeDir,
|
|
33
|
+
PI_CODING_AGENT_DIR: probeDir,
|
|
34
|
+
PI_OFFLINE: '1'
|
|
35
|
+
},
|
|
36
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
37
|
+
maxBuffer: 256 * 1024
|
|
38
|
+
});
|
|
39
|
+
help = result.stdout;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error('Pi bridge could not probe the installed pi executable.');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await rm(probeDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
if (REQUIRED_FLAGS.some(flag => !help.includes(flag))) {
|
|
48
|
+
throw new Error('Unsupported pi runtime: required RPC/isolation flags are missing. Control versions start at pi 0.81.0; install a compatible runtime.');
|
|
49
|
+
}
|
|
50
|
+
const args = ['--mode', 'rpc', '--no-session', '--no-themes', '-e', extension];
|
|
51
|
+
if (profile === 'validator') {
|
|
52
|
+
args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--no-context-files', '--no-builtin-tools', '--offline');
|
|
53
|
+
if (help.includes('--no-mcp'))
|
|
54
|
+
args.push('--no-mcp');
|
|
55
|
+
}
|
|
56
|
+
return args;
|
|
57
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isRecord } from "../../../shared/utils/guards.js";
|
|
2
|
+
// Large artifact pages can expand when escaped as JSON.
|
|
3
|
+
export const MAX_CONTROL_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
// Bound permissions and outstanding RPC calls for one session.
|
|
5
|
+
export const MAX_PENDING_CALLS = 64;
|
|
6
|
+
// Startup must not leave a partial context alive indefinitely.
|
|
7
|
+
export const BRIDGE_TIMEOUT_MS = 30000;
|
|
8
|
+
// Version both peers together when the control contract changes.
|
|
9
|
+
export const CONTROL_VERSION = 1;
|
|
10
|
+
export class JsonChannel {
|
|
11
|
+
#buffer = Buffer.alloc(0);
|
|
12
|
+
#closed = false;
|
|
13
|
+
input;
|
|
14
|
+
output;
|
|
15
|
+
failed;
|
|
16
|
+
constructor(input, output, receive, failed) {
|
|
17
|
+
this.input = input;
|
|
18
|
+
this.output = output;
|
|
19
|
+
this.failed = failed;
|
|
20
|
+
input.on('data', (chunk) => {
|
|
21
|
+
if (this.#closed)
|
|
22
|
+
return;
|
|
23
|
+
if (this.#buffer.length + chunk.length > MAX_CONTROL_BYTES) {
|
|
24
|
+
return this.close(true);
|
|
25
|
+
}
|
|
26
|
+
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
27
|
+
let index;
|
|
28
|
+
while ((index = this.#buffer.indexOf(10)) >= 0) {
|
|
29
|
+
const line = this.#buffer.subarray(0, index);
|
|
30
|
+
this.#buffer = this.#buffer.subarray(index + 1);
|
|
31
|
+
try {
|
|
32
|
+
const frame = JSON.parse(line.toString());
|
|
33
|
+
if (!isRecord(frame) ||
|
|
34
|
+
typeof frame.type !== 'string' ||
|
|
35
|
+
typeof frame.nonce !== 'string' ||
|
|
36
|
+
frame.version !== CONTROL_VERSION) {
|
|
37
|
+
throw new Error('Invalid control frame');
|
|
38
|
+
}
|
|
39
|
+
receive({
|
|
40
|
+
...frame,
|
|
41
|
+
type: frame.type,
|
|
42
|
+
nonce: frame.nonce,
|
|
43
|
+
version: CONTROL_VERSION
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
this.close(true);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
input.on('end', () => this.close(true));
|
|
53
|
+
input.on('error', () => this.close(true));
|
|
54
|
+
output.on('error', () => this.close(true));
|
|
55
|
+
}
|
|
56
|
+
send(frame) {
|
|
57
|
+
if (this.#closed)
|
|
58
|
+
throw new Error('Pi control channel is closed');
|
|
59
|
+
const line = JSON.stringify(frame) + '\n';
|
|
60
|
+
if (Buffer.byteLength(line) > MAX_CONTROL_BYTES ||
|
|
61
|
+
this.output.writableLength > MAX_CONTROL_BYTES) {
|
|
62
|
+
throw new Error('Pi control output exceeds the limit');
|
|
63
|
+
}
|
|
64
|
+
this.output.write(line);
|
|
65
|
+
}
|
|
66
|
+
close(failure = false) {
|
|
67
|
+
if (this.#closed)
|
|
68
|
+
return;
|
|
69
|
+
this.#closed = true;
|
|
70
|
+
this.#buffer = Buffer.alloc(0);
|
|
71
|
+
this.input.destroy();
|
|
72
|
+
this.output.destroy();
|
|
73
|
+
if (failure)
|
|
74
|
+
this.failed();
|
|
75
|
+
}
|
|
76
|
+
}
|