specrails-desktop 2.36.0 → 2.42.2
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 +94 -247
- package/client/dist/assets/ActivityFeedPage-DoEMumQS.js +1 -0
- package/client/dist/assets/AgentBrowserCapture-DIh4K0gh.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-mbq4WEoO.js +2 -0
- package/client/dist/assets/AgentModeCodePane-CKh7zuA4.js +2 -0
- package/client/dist/assets/AgentModeJobsPane-BDokPiLJ.js +1 -0
- package/client/dist/assets/AgentsPage-BFVAZbBj.js +87 -0
- package/client/dist/assets/AnalyticsPage-3Y9LuWwz.js +1 -0
- package/client/dist/assets/CodePage-C9KyuV_C.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-Bhi54bXQ.js +1 -0
- package/client/dist/assets/{DocsDialog-C76VdBJC.js → DocsDialog-Dj6mt4ga.js} +2 -2
- package/client/dist/assets/{DocsPage-ziMlj6B6.js → DocsPage-24yWglNI.js} +1 -1
- package/client/dist/assets/ExportDropdown-ophapHrT.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-C1ucmxQh.js +19 -0
- package/client/dist/assets/JobDetailModal-DNWmgVHD.js +1 -0
- package/client/dist/assets/JobDetailPage-BcajZyuu.js +1 -0
- package/client/dist/assets/JobsPage-B9nTZw6e.js +1 -0
- package/client/dist/assets/LoopBuilderPage-Oc48iOFW.js +7 -0
- package/client/dist/assets/LoopPreviewModal-ae0Fr6SU.js +1 -0
- package/client/dist/assets/LoopsPage-CUEqzUBI.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-Deg39peL.js +1 -0
- package/client/dist/assets/PluginsPage-C1xtARIx.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-JZzrXFBN.js +1 -0
- package/client/dist/assets/RepositoryDeliveries-CSDsiVUz.js +5 -0
- package/client/dist/assets/RepositoryScopeSelector-D-RCaknl.js +1 -0
- package/client/dist/assets/ReviewPacketPage-x5EfGTiL.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-BSyuZxGq.js +1 -0
- package/client/dist/assets/TicketDetailModalContext-DqyF0so-.js +14 -0
- package/client/dist/assets/Trans-SnMX4MhW.js +1 -0
- package/client/dist/assets/agent-BdT0SB1D.js +1 -0
- package/client/dist/assets/agent-Bh9K7iEt.js +1 -0
- package/client/dist/assets/agent-C4ibdFk9.js +1 -0
- package/client/dist/assets/agent-CSFY22Zo.js +1 -0
- package/client/dist/assets/agent-DAnqxUn8.js +1 -0
- package/client/dist/assets/agent-DTQiu0Mi.js +1 -0
- package/client/dist/assets/agent-T54WuR_Y.js +1 -0
- package/client/dist/assets/agent-i75WDZOA.js +1 -0
- package/client/dist/assets/browser-05vRDg_y.js +1 -0
- package/client/dist/assets/browser-Bulb-cB1.js +1 -0
- package/client/dist/assets/browser-C7f-Jsg1.js +1 -0
- package/client/dist/assets/browser-CG0PE1j_.js +1 -0
- package/client/dist/assets/browser-CMzaqAjo.js +1 -0
- package/client/dist/assets/browser-DMajYdNX.js +1 -0
- package/client/dist/assets/browser-DxcOHrdP.js +1 -0
- package/client/dist/assets/browser-ObE7xVo3.js +1 -0
- package/client/dist/assets/builder-765NeYTp.js +1 -0
- package/client/dist/assets/builder-8MG3V6ea.js +1 -0
- package/client/dist/assets/builder-B-1UBs5K.js +1 -0
- package/client/dist/assets/builder-BTiGnPT0.js +1 -0
- package/client/dist/assets/builder-Cx9Rz-7u.js +1 -0
- package/client/dist/assets/builder-DKFMELBl.js +1 -0
- package/client/dist/assets/builder-OcmKrC0h.js +1 -0
- package/client/dist/assets/builder-P9qVBXFZ.js +1 -0
- package/client/dist/assets/{button-CJqFnUOL.js → button-DCBqEaZl.js} +1 -1
- package/client/dist/assets/{code-CkUuW4m9.js → code-B37GRg9C.js} +1 -1
- package/client/dist/assets/{code-B0O8-p8k.js → code-BBOEZbf9.js} +1 -1
- package/client/dist/assets/{code-B1VLZDhS.js → code-BRm-jcyx.js} +1 -1
- package/client/dist/assets/{code-Bpj4Ddml.js → code-CF7w62CR.js} +1 -1
- package/client/dist/assets/{code-BykBZ5-e.js → code-CLuvJt2r.js} +1 -1
- package/client/dist/assets/{code-B86IMAfz.js → code-D0RCm59f.js} +1 -1
- package/client/dist/assets/code-DaZ-ZwCT.js +1 -0
- package/client/dist/assets/{code-cAqaqMTx.js → code-iIHkGaCL.js} +1 -1
- package/client/dist/assets/commands-0XjAftla.js +1 -0
- package/client/dist/assets/commands-BAWSYHjQ.js +1 -0
- package/client/dist/assets/commands-BBwowgiE.js +1 -0
- package/client/dist/assets/commands-BacjIZLN.js +1 -0
- package/client/dist/assets/commands-Bs62IAZ3.js +1 -0
- package/client/dist/assets/commands-CLPdh-Hh.js +1 -0
- package/client/dist/assets/commands-DlbFzgrp.js +1 -0
- package/client/dist/assets/commands-LjPSsi5F.js +1 -0
- package/client/dist/assets/common-B5AZNYMH.js +1 -0
- package/client/dist/assets/common-BFZ0c_vN.js +1 -0
- package/client/dist/assets/common-BrlwEnL7.js +1 -0
- package/client/dist/assets/common-BttQx3z4.js +1 -0
- package/client/dist/assets/common-CM65k_-e.js +1 -0
- package/client/dist/assets/common-D4mvFFlu.js +1 -0
- package/client/dist/assets/common-TZ9Ny96S.js +1 -0
- package/client/dist/assets/common-nFpSF4GC.js +1 -0
- package/client/dist/assets/dashboard-B14uYXkG.js +1 -0
- package/client/dist/assets/dashboard-BK_mez-z.js +1 -0
- package/client/dist/assets/dashboard-BfBir_6o.js +1 -0
- package/client/dist/assets/dashboard-Bt0cCzeG.js +1 -0
- package/client/dist/assets/dashboard-BwkjxI6H.js +1 -0
- package/client/dist/assets/{dashboard-C8WOEiPu.js → dashboard-C3bDKf7s.js} +1 -1
- package/client/dist/assets/dashboard-CdvjGfog.js +1 -0
- package/client/dist/assets/dashboard-fRrP0Kv0.js +1 -0
- package/client/dist/assets/dialog-SRw01_QU.js +45 -0
- package/client/dist/assets/dist-js-CJ_2XX1Y.js +1 -0
- package/client/dist/assets/formatDistanceToNow-DSj5DprF.js +1 -0
- package/client/dist/assets/getTimezoneOffsetInMilliseconds-YhD8rIEs.js +1 -0
- package/client/dist/assets/index-DwAmL7m5.css +2 -0
- package/client/dist/assets/index-EzrkUb8B.js +76 -0
- package/client/dist/assets/{jira-api-B1-MUehg.js → jira-api-CSCqGr-Q.js} +1 -1
- package/client/dist/assets/{jobs-CN_zv4Gr.js → jobs-B8rrzfyZ.js} +1 -1
- package/client/dist/assets/{jobs-DUlWSYq2.js → jobs-BS6RTurf.js} +1 -1
- package/client/dist/assets/{jobs-Bpks6doU.js → jobs-BTT0Bsks.js} +1 -1
- package/client/dist/assets/{jobs-Bgq-PZkC.js → jobs-CezU69AE.js} +1 -1
- package/client/dist/assets/{jobs-SwYS2M-B.js → jobs-ClFJae0s.js} +1 -1
- package/client/dist/assets/{jobs-BAls3bWq.js → jobs-DJSFIFVy.js} +1 -1
- package/client/dist/assets/{jobs-DhxKgWFq.js → jobs-DhgGr8vm.js} +1 -1
- package/client/dist/assets/{jobs-B_TeSQxb.js → jobs-DzMjUcaN.js} +1 -1
- package/client/dist/assets/mcp-6rMoZfEO.js +1 -0
- package/client/dist/assets/mcp-BD8sL5mP.js +1 -0
- package/client/dist/assets/mcp-BkcA3I5s.js +1 -0
- package/client/dist/assets/mcp-CCpSelOh.js +1 -0
- package/client/dist/assets/mcp-CUAN6ORK.js +1 -0
- package/client/dist/assets/mcp-CqbRxRTE.js +1 -0
- package/client/dist/assets/mcp-DP2YfVhO.js +1 -0
- package/client/dist/assets/mcp-DUVQamDz.js +1 -0
- package/client/dist/assets/narration-B-ynWTpn.js +1 -0
- package/client/dist/assets/narration-Bj_laZwg.js +1 -0
- package/client/dist/assets/narration-BxHpmMv_.js +1 -0
- package/client/dist/assets/narration-C1Px0-0j.js +1 -0
- package/client/dist/assets/narration-CAXhGbzM.js +1 -0
- package/client/dist/assets/narration-DQ1l_Kq6.js +1 -0
- package/client/dist/assets/narration-XISXwqZd.js +1 -0
- package/client/dist/assets/narration-kwzfglQo.js +1 -0
- package/client/dist/assets/nav-1-Hh2kOz.js +1 -0
- package/client/dist/assets/nav-B34QkESm.js +1 -0
- package/client/dist/assets/nav-Bap_UXAq.js +1 -0
- package/client/dist/assets/{nav-pYuXq7sf.js → nav-BwxXIB24.js} +1 -1
- package/client/dist/assets/nav-Ch-zHBG5.js +1 -0
- package/client/dist/assets/nav-CygC_WN-.js +1 -0
- package/client/dist/assets/{nav-iwjCg5FR.js → nav-KnnLQtOQ.js} +1 -1
- package/client/dist/assets/nav-m00uxYUG.js +1 -0
- package/client/dist/assets/packet-BQ2tXfJI.js +1 -0
- package/client/dist/assets/packet-BdpOJ1Cy.js +1 -0
- package/client/dist/assets/packet-C3Ievb9R.js +1 -0
- package/client/dist/assets/packet-C4zJIG0Y.js +1 -0
- package/client/dist/assets/packet-Cx-zRuWn.js +1 -0
- package/client/dist/assets/packet-DUTGX52M.js +1 -0
- package/client/dist/assets/packet-DhsKtUPB.js +1 -0
- package/client/dist/assets/packet-z3LJ8Zyy.js +1 -0
- package/client/dist/assets/project-repositories-DYhTvwQ1.js +1 -0
- package/client/dist/assets/provider-capabilities-B-wOHfLP.js +1 -0
- package/client/dist/assets/settings-BBt2xtDQ.js +1 -0
- package/client/dist/assets/settings-BJB9RKJj.js +1 -0
- package/client/dist/assets/settings-CHwQR2rB.js +1 -0
- package/client/dist/assets/settings-CrMi3kSi.js +1 -0
- package/client/dist/assets/settings-DKyhDGD1.js +1 -0
- package/client/dist/assets/settings-Deupji9c.js +1 -0
- package/client/dist/assets/settings-DqRMmwAM.js +1 -0
- package/client/dist/assets/settings-J4rj6vo-.js +1 -0
- package/client/dist/assets/setup-2xGgu4Sk.js +1 -0
- package/client/dist/assets/setup-Ba11l2JA.js +1 -0
- package/client/dist/assets/setup-BzW4kMok.js +1 -0
- package/client/dist/assets/setup-COzwYhkJ.js +1 -0
- package/client/dist/assets/setup-CRVULhtF.js +1 -0
- package/client/dist/assets/setup-DgvlxwpE.js +1 -0
- package/client/dist/assets/setup-Dw_wYeEY.js +1 -0
- package/client/dist/assets/setup-ZPtEeJPW.js +1 -0
- package/client/dist/assets/spending-BqbdPm_4.js +1 -0
- package/client/dist/assets/square-Ci-PFcFe.js +1 -0
- package/client/dist/assets/{terminal-BFRZnMPP.js → terminal-BAzpHtpY.js} +1 -1
- package/client/dist/assets/{terminal-IahE4LW9.js → terminal-C7dHeR3x.js} +1 -1
- package/client/dist/assets/{terminal-tITa81iz.js → terminal-CMla5Qsz.js} +1 -1
- package/client/dist/assets/{terminal-BZiIu3rf.js → terminal-CdNXH-n9.js} +1 -1
- package/client/dist/assets/{terminal-ScjyXNxQ.js → terminal-Cw5bZsmV.js} +1 -1
- package/client/dist/assets/{terminal-C2NAd7BW.js → terminal-DFUP4eNv.js} +1 -1
- package/client/dist/assets/{terminal-d7GadXbN.js → terminal-DGqljeK_.js} +1 -1
- package/client/dist/assets/{terminal-QpXZU2Bn.js → terminal-cU70NodO.js} +1 -1
- package/client/dist/assets/useDesktop-RSMTyy7V.js +1 -0
- package/client/dist/assets/useSharedWebSocket-BOdS5_Iz.js +2 -0
- package/client/dist/index.html +34 -35
- package/docs/README.md +3 -0
- package/docs/agent-live-steering.md +57 -0
- package/docs/ci-cd.md +57 -0
- package/docs/code-explorer.md +48 -0
- package/docs/codex.md +12 -1
- package/docs/features/detachable-mission-windows.md +59 -0
- package/docs/guide/de/integrations/5-mcp-server.md +13 -9
- package/docs/guide/de/integrations/6-agent-chat.md +1 -0
- package/docs/guide/en/integrations/5-mcp-server.md +13 -9
- package/docs/guide/en/integrations/6-agent-chat.md +1 -0
- package/docs/guide/es/integrations/5-mcp-server.md +13 -9
- package/docs/guide/es/integrations/6-agent-chat.md +1 -0
- package/docs/guide/fr/integrations/5-mcp-server.md +13 -9
- package/docs/guide/fr/integrations/6-agent-chat.md +1 -0
- package/docs/guide/it/integrations/5-mcp-server.md +13 -9
- package/docs/guide/it/integrations/6-agent-chat.md +1 -0
- package/docs/guide/ja/integrations/5-mcp-server.md +13 -9
- package/docs/guide/ja/integrations/6-agent-chat.md +1 -0
- package/docs/guide/pt/integrations/5-mcp-server.md +13 -9
- package/docs/guide/pt/integrations/6-agent-chat.md +1 -0
- package/docs/guide/zh/integrations/5-mcp-server.md +13 -9
- package/docs/guide/zh/integrations/6-agent-chat.md +1 -0
- package/docs/internals/README.md +1 -0
- package/docs/internals/browser-capture-performance.md +55 -3
- package/docs/internals/browser-login-popups.md +45 -0
- package/docs/internals/browser-native-retina-audit.md +39 -0
- package/docs/internals/ci-cd-audit.md +39 -0
- package/docs/internals/core-runtime-updates.md +68 -0
- package/docs/internals/embedded-browser-native-webview-evaluation.md +2 -0
- package/docs/internals/implementation-reliability-audit.md +93 -0
- package/docs/internals/interactive-jobs.md +110 -6
- package/docs/internals/mcp-mission-audit.md +76 -0
- package/docs/internals/project-builder.md +404 -42
- package/docs/internals/safe-pr-review-flow.md +52 -10
- package/docs/internals/startup-recovery-audit.md +58 -0
- package/docs/mcp.md +74 -41
- package/docs/mission-processes.md +59 -0
- package/docs/multi-repo-projects.md +51 -0
- package/docs/platforms/windows-parity.md +70 -0
- package/docs/platforms/windows.md +13 -11
- package/docs/running-pipelines.md +14 -0
- package/mcp-bridge/dist/specrails-mcp.js +7186 -0
- package/package.json +9 -4
- package/server/dist/agent-chat-manager.js +537 -68
- package/server/dist/agent-chat-router.js +87 -6
- package/server/dist/agent-context-resolver.js +129 -25
- package/server/dist/agent-defaults.js +19 -11
- package/server/dist/agent-input-store.js +206 -0
- package/server/dist/agent-mcp-config.js +54 -15
- package/server/dist/agent-operator-prompt.js +255 -65
- package/server/dist/agent-spec-framing.js +202 -0
- package/server/dist/agent-steering.js +218 -0
- package/server/dist/agent-store.js +153 -0
- package/server/dist/api-not-found.js +8 -0
- package/server/dist/background-process-control.js +164 -0
- package/server/dist/background-process-service.js +61 -0
- package/server/dist/background-process-store.js +242 -0
- package/server/dist/background-windows-bootstrap.js +238 -0
- package/server/dist/blueprint-chat-manager.js +476 -62
- package/server/dist/blueprint-commit.js +21 -1
- package/server/dist/blueprint-draft-parser.js +122 -17
- package/server/dist/blueprint-generation.js +238 -0
- package/server/dist/blueprint-operator-prompt.js +91 -63
- package/server/dist/blueprint-router.js +55 -3
- package/server/dist/blueprint-spec-fixtures.js +85 -0
- package/server/dist/blueprint-spec-quality.js +57 -16
- package/server/dist/blueprint-store.js +92 -1
- package/server/dist/browser-capture-manager.js +86 -30
- package/server/dist/browser-playwright.js +168 -45
- package/server/dist/browser-viewport.js +26 -0
- package/server/dist/chat-manager.js +23 -15
- package/server/dist/chromium-archive.cjs +83 -0
- package/server/dist/chromium-resolver.js +72 -64
- package/server/dist/code-activity.js +145 -0
- package/server/dist/code-explorer-router.js +714 -114
- package/server/dist/core-compat.js +28 -1
- package/server/dist/core-execution.js +111 -0
- package/server/dist/core-package.js +4 -16
- package/server/dist/core-runtime.js +202 -0
- package/server/dist/core-update-manager.js +161 -60
- package/server/dist/core-update-state.js +29 -0
- package/server/dist/db.js +66 -35
- package/server/dist/delivery-evidence.js +142 -39
- package/server/dist/desktop-db.js +239 -43
- package/server/dist/desktop-router.js +109 -9
- package/server/dist/dev-ports.js +8 -3
- package/server/dist/file-provenance.js +34 -22
- package/server/dist/file-story-manager.js +62 -64
- package/server/dist/file-story.js +118 -35
- package/server/dist/file-summary-generator.js +46 -5
- package/server/dist/file-summary-manager.js +355 -106
- package/server/dist/framework-manager.js +98 -110
- package/server/dist/framework-reseed.js +76 -10
- package/server/dist/git-diagnostics.js +2 -2
- package/server/dist/headroom-manager.js +38 -3
- package/server/dist/host-control.js +52 -0
- package/server/dist/index.js +43 -2
- package/server/dist/interactive-job-session.js +149 -16
- package/server/dist/internal-api.js +48 -0
- package/server/dist/jira/jira-materializer.js +1 -0
- package/server/dist/json-tolerant.js +174 -0
- package/server/dist/legacy-migration.js +4 -2
- package/server/dist/loop-command-catalog.js +83 -21
- package/server/dist/loop-constants.js +6 -1
- package/server/dist/loop-decider.js +11 -1
- package/server/dist/loop-executors.js +194 -45
- package/server/dist/loop-factory.js +11 -4
- package/server/dist/loop-graph.js +90 -4
- package/server/dist/loop-run-manager.js +301 -69
- package/server/dist/loop-shell-invocation.js +21 -0
- package/server/dist/loop-step-idle.js +55 -0
- package/server/dist/loop-templates.js +31 -7
- package/server/dist/mcp/agent-capability.js +37 -4
- package/server/dist/mcp/guide.js +149 -36
- package/server/dist/mcp/mcp-admin-router.js +20 -4
- package/server/dist/mcp/mcp-server.js +89 -43
- package/server/dist/mcp/mcp-tiers.js +6 -5
- package/server/dist/mcp/mcp-token.js +3 -1
- package/server/dist/mcp/resources.js +5 -4
- package/server/dist/mcp/tools/catalog.js +4 -0
- package/server/dist/mcp/tools/code.js +125 -14
- package/server/dist/mcp/tools/context.js +153 -0
- package/server/dist/mcp/tools/git.js +15 -5
- package/server/dist/mcp/tools/jobs.js +84 -71
- package/server/dist/mcp/tools/loops.js +17 -6
- package/server/dist/mcp/tools/meta.js +74 -27
- package/server/dist/mcp/tools/mission.js +29 -0
- package/server/dist/mcp/tools/projects.js +22 -6
- package/server/dist/mcp/tools/rails.js +48 -11
- package/server/dist/mcp/tools/specs.js +86 -14
- package/server/dist/mcp/tools/support.js +1 -1
- package/server/dist/mcp/tools/types.js +117 -43
- package/server/dist/mcp/tools/watch.js +144 -51
- package/server/dist/milestone-chain-store.js +164 -0
- package/server/dist/milestone-chain.js +487 -0
- package/server/dist/milestone-progress.js +348 -0
- package/server/dist/mobile/mobile-devices.js +10 -7
- package/server/dist/mobile/mobile-gateway.js +2 -0
- package/server/dist/mobile/mobile-missions.js +214 -0
- package/server/dist/mobile/mobile-redact.js +17 -3
- package/server/dist/mobile/mobile-router.js +46 -4
- package/server/dist/mobile/mobile-ws.js +49 -4
- package/server/dist/multi-repo-bases.js +56 -0
- package/server/dist/multi-repo-checkout.js +101 -0
- package/server/dist/multi-repo-delivery.js +100 -0
- package/server/dist/multi-repo-execution-store.js +65 -0
- package/server/dist/multi-repo-execution.js +453 -0
- package/server/dist/offline-assemble.js +10 -8
- package/server/dist/openspec-runtime-plugin-commands.json +18 -0
- package/server/dist/openspec-runtime-plugin.js +120 -0
- package/server/dist/plugins/codex-spawn.js +49 -0
- package/server/dist/plugins/serena/manifest.js +2 -2
- package/server/dist/plugins/serena/templates/instructions.md +16 -0
- package/server/dist/pr-publisher.js +1 -1
- package/server/dist/profiles-router.js +3 -18
- package/server/dist/project-code-discovery.js +142 -0
- package/server/dist/project-git.js +19 -21
- package/server/dist/project-registry.js +272 -20
- package/server/dist/project-repositories.js +157 -0
- package/server/dist/project-repository-provenance.js +31 -0
- package/server/dist/project-router-background-processes.js +105 -0
- package/server/dist/project-router-git.js +75 -16
- package/server/dist/project-router-jobs.js +2 -126
- package/server/dist/project-router-loop-runs.js +72 -2
- package/server/dist/project-router-tickets.js +53 -8
- package/server/dist/project-router.js +239 -38
- package/server/dist/provider-detection.js +18 -6
- package/server/dist/provider-limit.js +64 -0
- package/server/dist/providers/claude-adapter.js +26 -1
- package/server/dist/providers/claude-background-tasks.js +18 -0
- package/server/dist/providers/claude-live-session.js +383 -0
- package/server/dist/providers/codex-adapter.js +19 -0
- package/server/dist/providers/codex-live-session.js +545 -0
- package/server/dist/providers/gemini-adapter.js +1 -0
- package/server/dist/providers/live-session-types.js +12 -0
- package/server/dist/providers/live-session.js +13 -0
- package/server/dist/providers/terminal-result.js +24 -0
- package/server/dist/queue-manager.js +2 -0
- package/server/dist/rail-isolated-launch.js +141 -16
- package/server/dist/rail-pr-decision.js +364 -99
- package/server/dist/rail-pr-store.js +24 -10
- package/server/dist/rail-worktree-release.js +34 -2
- package/server/dist/rail-worktrees-store.js +4 -4
- package/server/dist/rails-router.js +249 -55
- package/server/dist/repo-lock.js +92 -17
- package/server/dist/review-packet.js +23 -13
- package/server/dist/schemas/file-summary.v1.json +59 -59
- package/server/dist/schemas/profile.v1.json +137 -137
- package/server/dist/setup-manager.js +52 -8
- package/server/dist/shell-integration/bash-shim.bash +45 -0
- package/server/dist/shell-integration/fish-shim.fish +36 -0
- package/server/dist/shell-integration/powershell-shim.ps1 +48 -0
- package/server/dist/shell-integration/zsh-shim.zsh +47 -0
- package/server/dist/smash-runner.js +1 -0
- package/server/dist/spawn-lifecycle.js +44 -27
- package/server/dist/spec-contract-prompt.js +143 -0
- package/server/dist/spec-draft-parser.js +6 -1
- package/server/dist/spec-models.js +1 -0
- package/server/dist/stuck-run-detector.js +1 -0
- package/server/dist/terminal-manager.js +21 -1
- package/server/dist/terminal-shell-integration.js +4 -1
- package/server/dist/transient-children.js +619 -230
- package/server/dist/util/cli-prompt.js +78 -5
- package/server/dist/util/sqlite-migrations.js +20 -0
- package/server/dist/util/win-spawn.js +18 -3
- package/server/dist/util/windows-provider-shell.js +29 -0
- package/server/dist/verification-sentinel.js +14 -0
- package/server/dist/vitest-setup.js +2 -0
- package/server/dist/windows-job-supervisor.js +133 -0
- package/server/dist/workspace-manager.js +9 -30
- package/server/dist/worktree-manager.js +17 -2
- package/server/dist/worktree-overlay.js +3 -1
- package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +0 -1
- package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +0 -2
- package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +0 -1
- package/client/dist/assets/AgentsPage-C8J_42sm.js +0 -87
- package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +0 -1
- package/client/dist/assets/CodePage-BZ2MwdCu.js +0 -2
- package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +0 -1
- package/client/dist/assets/ExportDropdown-BrtfWzEG.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +0 -19
- package/client/dist/assets/JobDetailModal-NAJmuYIT.js +0 -1
- package/client/dist/assets/JobDetailPage-fE0ZycUX.js +0 -1
- package/client/dist/assets/JobsPage-z5M_HVJG.js +0 -1
- package/client/dist/assets/LoopBuilderPage-j1FQS47c.js +0 -7
- package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +0 -1
- package/client/dist/assets/LoopsPage-Ll7zORZb.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +0 -1
- package/client/dist/assets/PluginsPage-DN4kGvJt.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-OgsgxjzF.js +0 -1
- package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +0 -1
- package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +0 -14
- package/client/dist/assets/Trans-Bsz9_gyq.js +0 -1
- package/client/dist/assets/agent-BQgKv_2F.js +0 -1
- package/client/dist/assets/agent-C1NRDJqL.js +0 -1
- package/client/dist/assets/agent-CLNleiy6.js +0 -1
- package/client/dist/assets/agent-CmceArkQ.js +0 -1
- package/client/dist/assets/agent-D3UxQ3SR.js +0 -1
- package/client/dist/assets/agent-DB1JeE64.js +0 -1
- package/client/dist/assets/agent-DXkHcQ-D.js +0 -1
- package/client/dist/assets/agent-DjJJJ2M8.js +0 -1
- package/client/dist/assets/auth-D_5bQv5W.js +0 -1
- package/client/dist/assets/ban-E4EaZe3a.js +0 -1
- package/client/dist/assets/bot-DzuuuC5R.js +0 -1
- package/client/dist/assets/browser-BgqHDMXP.js +0 -1
- package/client/dist/assets/browser-CsKJtrpv.js +0 -1
- package/client/dist/assets/browser-Cu58EpZU.js +0 -1
- package/client/dist/assets/browser-CvXOJm-j.js +0 -1
- package/client/dist/assets/browser-DOrFh1c7.js +0 -1
- package/client/dist/assets/browser-DQv_Jo7x.js +0 -1
- package/client/dist/assets/browser-LPgxvJu2.js +0 -1
- package/client/dist/assets/browser-eL-3MIEI.js +0 -1
- package/client/dist/assets/builder-0IWtosUm.js +0 -1
- package/client/dist/assets/builder-Bf2N6XWn.js +0 -1
- package/client/dist/assets/builder-BnqaaBkT.js +0 -1
- package/client/dist/assets/builder-BxxfGgAO.js +0 -1
- package/client/dist/assets/builder-C5IIgWsq.js +0 -1
- package/client/dist/assets/builder-CHuahItu.js +0 -1
- package/client/dist/assets/builder-CjWosXL_.js +0 -1
- package/client/dist/assets/builder-CpCIOoKj.js +0 -1
- package/client/dist/assets/code-CHPkOkDS.js +0 -1
- package/client/dist/assets/commands-6tYQih5m.js +0 -1
- package/client/dist/assets/commands-BK2QinXN.js +0 -1
- package/client/dist/assets/commands-C2Nca76h.js +0 -1
- package/client/dist/assets/commands-CKdLihNX.js +0 -1
- package/client/dist/assets/commands-CYIBTAlc.js +0 -1
- package/client/dist/assets/commands-CjUtYJIM.js +0 -1
- package/client/dist/assets/commands-Cq_u_8y9.js +0 -1
- package/client/dist/assets/commands-D_ZHYQug.js +0 -1
- package/client/dist/assets/common-6MLbD-jM.js +0 -1
- package/client/dist/assets/common-BKGEu4rm.js +0 -1
- package/client/dist/assets/common-BNbnv8cE.js +0 -1
- package/client/dist/assets/common-BqGWtDbt.js +0 -1
- package/client/dist/assets/common-DBtTzeYm.js +0 -1
- package/client/dist/assets/common-DEW3Nr8n.js +0 -1
- package/client/dist/assets/common-DHsv9N5d.js +0 -1
- package/client/dist/assets/common-mkrEcW-Y.js +0 -1
- package/client/dist/assets/dashboard-B9GhC5ku.js +0 -1
- package/client/dist/assets/dashboard-CBnv6zOI.js +0 -1
- package/client/dist/assets/dashboard-CaP0fU87.js +0 -1
- package/client/dist/assets/dashboard-D4ZZ4x84.js +0 -1
- package/client/dist/assets/dashboard-DmuTQBX8.js +0 -1
- package/client/dist/assets/dashboard-DynLYj3d.js +0 -1
- package/client/dist/assets/dashboard-zj4gGFJ1.js +0 -1
- package/client/dist/assets/dialog-CdWCb02j.js +0 -45
- package/client/dist/assets/dist-bVqfYmby.js +0 -1
- package/client/dist/assets/dist-js-CWeUdf5Z.js +0 -1
- package/client/dist/assets/formatDistanceToNow-DQCvMgUc.js +0 -1
- package/client/dist/assets/getTimezoneOffsetInMilliseconds-C16cxSe1.js +0 -1
- package/client/dist/assets/git-refresh-wTW0rO0Z.js +0 -1
- package/client/dist/assets/i18n-hnX2DPmo.js +0 -2
- package/client/dist/assets/index-DIhMEAVw.js +0 -66
- package/client/dist/assets/index-DwygcU7s.css +0 -2
- package/client/dist/assets/mcp-B8X8wBLu.js +0 -1
- package/client/dist/assets/mcp-C2AojZZN.js +0 -1
- package/client/dist/assets/mcp-D8BT78_L.js +0 -1
- package/client/dist/assets/mcp-Df5RWBNk.js +0 -1
- package/client/dist/assets/mcp-Dj7BgMQW.js +0 -1
- package/client/dist/assets/mcp-Q7iT7rcz.js +0 -1
- package/client/dist/assets/mcp-YlvNvJbW.js +0 -1
- package/client/dist/assets/mcp-mKFpTSRb.js +0 -1
- package/client/dist/assets/narration-4Z2VUhZw.js +0 -1
- package/client/dist/assets/narration-BGuiEDj3.js +0 -1
- package/client/dist/assets/narration-BuSoqRmv.js +0 -1
- package/client/dist/assets/narration-CB9t29fy.js +0 -1
- package/client/dist/assets/narration-CU3x8FHD.js +0 -1
- package/client/dist/assets/narration-CWeXhfP5.js +0 -1
- package/client/dist/assets/narration-DRaDnhT5.js +0 -1
- package/client/dist/assets/narration-DtEbQgCw.js +0 -1
- package/client/dist/assets/nav-ApJVg3xu.js +0 -1
- package/client/dist/assets/nav-BKNky1Bg.js +0 -1
- package/client/dist/assets/nav-C6vWWKYx.js +0 -1
- package/client/dist/assets/nav-CXJ6Aw89.js +0 -1
- package/client/dist/assets/nav-CsmFBIDf.js +0 -1
- package/client/dist/assets/nav-DjkmQJjd.js +0 -1
- package/client/dist/assets/packet-B1IxaL5f.js +0 -1
- package/client/dist/assets/packet-Bh5kkbiB.js +0 -1
- package/client/dist/assets/packet-By8fqYxW.js +0 -1
- package/client/dist/assets/packet-CZdvA44E.js +0 -1
- package/client/dist/assets/packet-CrjHRQCP.js +0 -1
- package/client/dist/assets/packet-Dwi2pTtx.js +0 -1
- package/client/dist/assets/packet-_-ZIa0IR.js +0 -1
- package/client/dist/assets/packet-n0W6E1Hn.js +0 -1
- package/client/dist/assets/play-DNlrawbS.js +0 -1
- package/client/dist/assets/provider-capabilities-Doej9lfa.js +0 -1
- package/client/dist/assets/settings-Bd0x4TBa.js +0 -1
- package/client/dist/assets/settings-C1-7cXzC.js +0 -1
- package/client/dist/assets/settings-CAFaT38b.js +0 -1
- package/client/dist/assets/settings-Ct3v1nW2.js +0 -1
- package/client/dist/assets/settings-DHh7_IF5.js +0 -1
- package/client/dist/assets/settings-DQXAmdcv.js +0 -1
- package/client/dist/assets/settings-guzlhpup.js +0 -1
- package/client/dist/assets/settings-sZfTPMEH.js +0 -1
- package/client/dist/assets/setup-3jWaoVUO.js +0 -1
- package/client/dist/assets/setup-BjgRneb2.js +0 -1
- package/client/dist/assets/setup-BzFIjv-f.js +0 -1
- package/client/dist/assets/setup-C4VfQsxE.js +0 -1
- package/client/dist/assets/setup-DfUkpDbN.js +0 -1
- package/client/dist/assets/setup-ECe90Pmu.js +0 -1
- package/client/dist/assets/setup-dlqC4Jmm.js +0 -1
- package/client/dist/assets/setup-jwfLMiM2.js +0 -1
- package/client/dist/assets/spending-EC9xqio-.js +0 -1
- package/client/dist/assets/useDesktop-BQVt8lFd.js +0 -1
|
@@ -3,9 +3,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.filterCodeExplorerActivityPaths = void 0;
|
|
7
|
+
exports.readBoundedSource = readBoundedSource;
|
|
8
|
+
exports.isDeniedRelPath = isDeniedRelPath;
|
|
9
|
+
exports.gitIgnoredSet = gitIgnoredSet;
|
|
10
|
+
exports.rankFindMatches = rankFindMatches;
|
|
11
|
+
exports.listAllEntries = listAllEntries;
|
|
6
12
|
exports.createCodeExplorerRouter = createCodeExplorerRouter;
|
|
13
|
+
exports.resolveSafePath = resolveSafePath;
|
|
7
14
|
const fs_1 = __importDefault(require("fs"));
|
|
8
15
|
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
17
|
const child_process_1 = require("child_process");
|
|
10
18
|
const express_1 = require("express");
|
|
11
19
|
const feature_flags_1 = require("./feature-flags");
|
|
@@ -13,10 +21,56 @@ const build_dirs_1 = require("./build-dirs");
|
|
|
13
21
|
const file_provenance_1 = require("./file-provenance");
|
|
14
22
|
const file_summary_manager_1 = require("./file-summary-manager");
|
|
15
23
|
const file_story_1 = require("./file-story");
|
|
24
|
+
const project_repository_provenance_1 = require("./project-repository-provenance");
|
|
16
25
|
const providers_1 = require("./providers");
|
|
26
|
+
const project_repositories_1 = require("./project-repositories");
|
|
27
|
+
const code_activity_1 = require("./code-activity");
|
|
17
28
|
const MAX_TREE_PAGE = 2000;
|
|
29
|
+
const DEFAULT_MAX_TREE_ENTRIES = 20_000;
|
|
30
|
+
const DEFAULT_TREE_SCAN_MS = 5_000;
|
|
31
|
+
const TREE_YIELD_EVERY = 128;
|
|
32
|
+
const GIT_IGNORE_CHUNK = 1_000;
|
|
18
33
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
19
34
|
const BINARY_PROBE_BYTES = 8 * 1024;
|
|
35
|
+
const MAX_CODE_PAGE_LINES = 500;
|
|
36
|
+
const MAX_CODE_PAGE_CHARS = 20_000;
|
|
37
|
+
const MAX_SEARCH_FILES = 1_000;
|
|
38
|
+
const MAX_SEARCH_BYTES = 8 * 1024 * 1024;
|
|
39
|
+
const MAX_SEARCH_MS = 2_000;
|
|
40
|
+
/** Read a regular file through one descriptor with a hard byte ceiling, even
|
|
41
|
+
* if a writer grows it after stat. Never interpret a truncated read as text. */
|
|
42
|
+
async function readBoundedSource(abs, maxBytes) {
|
|
43
|
+
const before = await fs_1.default.promises.stat(abs);
|
|
44
|
+
if (!before.isFile())
|
|
45
|
+
return 'not-file';
|
|
46
|
+
if (before.size > maxBytes)
|
|
47
|
+
return 'too-large';
|
|
48
|
+
// A FIFO swapped in between stat/open must not hang an agent request. fstat
|
|
49
|
+
// below still validates the descriptor before any bytes are read.
|
|
50
|
+
const flags = fs_1.default.constants.O_RDONLY | (process.platform === 'win32' ? 0 : fs_1.default.constants.O_NONBLOCK);
|
|
51
|
+
const handle = await fs_1.default.promises.open(abs, flags);
|
|
52
|
+
try {
|
|
53
|
+
const stat = await handle.stat();
|
|
54
|
+
if (!stat.isFile())
|
|
55
|
+
return 'not-file';
|
|
56
|
+
if (stat.size > maxBytes)
|
|
57
|
+
return 'too-large';
|
|
58
|
+
const buffer = Buffer.alloc(Math.min(stat.size + 1, maxBytes + 1));
|
|
59
|
+
let offset = 0;
|
|
60
|
+
while (offset < buffer.length) {
|
|
61
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
62
|
+
if (!bytesRead)
|
|
63
|
+
return buffer.subarray(0, offset);
|
|
64
|
+
offset += bytesRead;
|
|
65
|
+
}
|
|
66
|
+
// Filled the extra byte: the file changed while reading. Retry a fresh
|
|
67
|
+
// snapshot instead of serving content whose completeness is unknown.
|
|
68
|
+
return 'too-large';
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await handle.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
20
74
|
// Hard-coded app deny-list (mirrors design D8). Dotfiles are excluded by name
|
|
21
75
|
// prefix; build/dep dirs come from the shared BUILD_DIRS set (node_modules, dist,
|
|
22
76
|
// build, out, coverage, target, vendor) so the on-demand tree walk skips the same
|
|
@@ -49,7 +103,7 @@ function isDenied(entryName) {
|
|
|
49
103
|
// single source of truth across every surface (tree walk, touched-by-ai list,
|
|
50
104
|
// and the content endpoints) — not just the top-level `all` walk.
|
|
51
105
|
function isDeniedRelPath(rel) {
|
|
52
|
-
return rel.split(/[\\/]/).filter(
|
|
106
|
+
return rel.split(/[\\/]/).filter((segment) => segment !== '' && segment !== '.').some(isDenied);
|
|
53
107
|
}
|
|
54
108
|
// Normalize a client-supplied relative path to POSIX separators so summary
|
|
55
109
|
// (sha256 of relPath), provenance (git always emits '/'), and content lookups
|
|
@@ -63,27 +117,77 @@ function normalizeRel(rel) {
|
|
|
63
117
|
// paths reported, so the deny-list remains the only filter. `check-ignore` exits
|
|
64
118
|
// 1 ("none ignored") which execFileSync throws on — the matched list still lands
|
|
65
119
|
// on stdout, so both branches read stdout.
|
|
66
|
-
function gitIgnoredSet(projectPath, relPaths) {
|
|
120
|
+
async function gitIgnoredSet(projectPath, relPaths, maxDurationMs = Number.POSITIVE_INFINITY) {
|
|
67
121
|
if (relPaths.length === 0)
|
|
68
122
|
return new Set();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
123
|
+
if ((0, project_repositories_1.legacyRepositoryKind)(projectPath) === 'folder')
|
|
124
|
+
return new Set();
|
|
125
|
+
const ignored = new Set();
|
|
126
|
+
const excludeUnverified = (offset) => {
|
|
127
|
+
ignored.incomplete = true;
|
|
128
|
+
for (const rel of relPaths.slice(offset))
|
|
129
|
+
ignored.add(rel);
|
|
130
|
+
};
|
|
131
|
+
const deadline = Date.now() + maxDurationMs;
|
|
132
|
+
for (let offset = 0; offset < relPaths.length; offset += GIT_IGNORE_CHUNK) {
|
|
133
|
+
if (Date.now() >= deadline) {
|
|
134
|
+
excludeUnverified(offset);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const chunk = relPaths.slice(offset, offset + GIT_IGNORE_CHUNK);
|
|
138
|
+
const result = await new Promise((resolve) => {
|
|
139
|
+
let stdout = '';
|
|
140
|
+
let settled = false;
|
|
141
|
+
let overflow = false;
|
|
142
|
+
const child = (0, child_process_1.spawn)('git', ['check-ignore', '--stdin', '-z'], { cwd: projectPath, stdio: ['pipe', 'pipe', 'ignore'], env: (0, file_provenance_1.gitExecEnv)() });
|
|
143
|
+
const finish = (complete) => { if (!settled) {
|
|
144
|
+
settled = true;
|
|
145
|
+
resolve({ stdout, complete: complete && !overflow });
|
|
146
|
+
} };
|
|
147
|
+
const timer = setTimeout(() => { try {
|
|
148
|
+
child.kill('SIGTERM');
|
|
149
|
+
}
|
|
150
|
+
catch { /* gone */ } ; finish(false); }, Math.min(5_000, Math.max(1, deadline - Date.now())));
|
|
151
|
+
timer.unref?.();
|
|
152
|
+
child.stdout?.on('data', (data) => {
|
|
153
|
+
if (stdout.length < 4 * 1024 * 1024)
|
|
154
|
+
stdout += data.toString('utf8');
|
|
155
|
+
else
|
|
156
|
+
overflow = true;
|
|
157
|
+
});
|
|
158
|
+
child.once('error', () => { clearTimeout(timer); finish(false); });
|
|
159
|
+
child.once('close', code => { clearTimeout(timer); finish(code === 0 || code === 1); });
|
|
160
|
+
child.stdin?.on('error', () => { });
|
|
161
|
+
child.stdin?.end(chunk.join('\0') + '\0');
|
|
78
162
|
});
|
|
163
|
+
if (!result.complete) {
|
|
164
|
+
excludeUnverified(offset);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
for (const rel of result.stdout.split('\0'))
|
|
168
|
+
if (rel)
|
|
169
|
+
ignored.add(rel);
|
|
79
170
|
}
|
|
80
|
-
|
|
81
|
-
out = (err.stdout ?? '').toString();
|
|
82
|
-
}
|
|
83
|
-
return new Set(out.split('\0').filter((p) => p.length > 0));
|
|
171
|
+
return ignored;
|
|
84
172
|
}
|
|
85
|
-
|
|
86
|
-
|
|
173
|
+
/** One policy for current source and historical metadata. Missing files remain
|
|
174
|
+
* eligible; unavailable roots or unverified ignore rules fail closed explicitly. */
|
|
175
|
+
const filterCodeExplorerActivityPaths = async (repository, paths, remainingMs) => {
|
|
176
|
+
try {
|
|
177
|
+
if (!(await fs_1.default.promises.stat(repository.path)).isDirectory())
|
|
178
|
+
throw new Error('not a directory');
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return { allowed: new Set(), unavailable: true };
|
|
182
|
+
}
|
|
183
|
+
const candidates = paths.filter(rel => !isDeniedRelPath(rel) && resolveSafePath(repository.path, rel) !== null);
|
|
184
|
+
const normalized = candidates.map(normalizeRel);
|
|
185
|
+
const ignored = await gitIgnoredSet(repository.path, normalized, remainingMs);
|
|
186
|
+
return { allowed: new Set(candidates.filter((_, index) => !ignored.has(normalized[index]))), incomplete: ignored.incomplete };
|
|
187
|
+
};
|
|
188
|
+
exports.filterCodeExplorerActivityPaths = filterCodeExplorerActivityPaths;
|
|
189
|
+
async function isGitIgnored(projectPath, relPath) {
|
|
190
|
+
return (await gitIgnoredSet(projectPath, [relPath])).has(relPath);
|
|
87
191
|
}
|
|
88
192
|
function languageForExt(ext) {
|
|
89
193
|
const e = ext.toLowerCase();
|
|
@@ -112,21 +216,25 @@ function languageForExt(ext) {
|
|
|
112
216
|
}
|
|
113
217
|
}
|
|
114
218
|
function decodeCursor(raw) {
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
219
|
+
if (raw === undefined)
|
|
220
|
+
return null;
|
|
221
|
+
if (typeof raw !== 'string' || raw.length > 1024)
|
|
222
|
+
throw new Error('invalid_tree_cursor');
|
|
117
223
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
224
|
+
const parsed = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));
|
|
225
|
+
// Offset-only cursors from an older server cannot identify their original
|
|
226
|
+
// list. Ask the caller to refresh instead of silently mixing snapshots.
|
|
227
|
+
if (parsed.v === undefined && Number.isSafeInteger(parsed.skip))
|
|
228
|
+
return 'expired';
|
|
229
|
+
if (parsed.v === 2 && typeof parsed.snapshot === 'string' && /^[a-f0-9-]{36}$/.test(parsed.snapshot) &&
|
|
230
|
+
Number.isSafeInteger(parsed.skip) && parsed.skip > 0)
|
|
231
|
+
return parsed;
|
|
125
232
|
}
|
|
126
|
-
|
|
233
|
+
catch { /* Invalid input is never treated as page zero. */ }
|
|
234
|
+
throw new Error('invalid_tree_cursor');
|
|
127
235
|
}
|
|
128
|
-
function encodeCursor(skip) {
|
|
129
|
-
return Buffer.from(JSON.stringify({ skip }), 'utf8').toString('
|
|
236
|
+
function encodeCursor(snapshot, skip) {
|
|
237
|
+
return Buffer.from(JSON.stringify({ v: 2, snapshot, skip }), 'utf8').toString('base64url');
|
|
130
238
|
}
|
|
131
239
|
function rollupProvenance(rows) {
|
|
132
240
|
let createdByTicketId = null;
|
|
@@ -208,9 +316,10 @@ function parsePositiveInt(raw) {
|
|
|
208
316
|
function parseNonEmptyString(raw) {
|
|
209
317
|
return typeof raw === 'string' && raw.trim() !== '' ? raw.trim() : null;
|
|
210
318
|
}
|
|
211
|
-
function listTouchedRows(db, filters) {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
319
|
+
function listTouchedRows(db, filters, repository) {
|
|
320
|
+
const scope = (0, project_repository_provenance_1.provenanceRepositoryFilter)(db, repository);
|
|
321
|
+
const where = [scope.sql];
|
|
322
|
+
const args = [...scope.params];
|
|
214
323
|
if (filters.ticketId != null) {
|
|
215
324
|
where.push('ticket_id = ?');
|
|
216
325
|
args.push(filters.ticketId);
|
|
@@ -228,19 +337,86 @@ function listTouchedRows(db, filters) {
|
|
|
228
337
|
FROM file_provenance ${whereSql}
|
|
229
338
|
ORDER BY file_path ASC, at DESC`).all(...args);
|
|
230
339
|
}
|
|
231
|
-
|
|
340
|
+
const DEFAULT_FIND_LIMIT = 20;
|
|
341
|
+
const MAX_FIND_LIMIT = 50;
|
|
342
|
+
const MAX_FIND_QUERY_LENGTH = 256;
|
|
343
|
+
/**
|
|
344
|
+
* Rank the files of a scan against a name / path-suffix / fragment query.
|
|
345
|
+
* Case-insensitive, POSIX-normalised. Order: `exact` (whole relpath), `suffix`
|
|
346
|
+
* (the query is the tail of the relpath — the "path copied from a stack trace
|
|
347
|
+
* or import that is relative to some subdirectory" case), `basename` (same
|
|
348
|
+
* file name under another directory), `substring`; ties break on the shorter
|
|
349
|
+
* path. Directories never match — the caller wants something to read.
|
|
350
|
+
*/
|
|
351
|
+
function rankFindMatches(entries, query) {
|
|
352
|
+
const needle = query.trim().replace(/\\/g, '/').replace(/^(\.\/|\/)+/, '').toLowerCase();
|
|
353
|
+
if (!needle)
|
|
354
|
+
return [];
|
|
355
|
+
const needleBase = needle.slice(needle.lastIndexOf('/') + 1);
|
|
356
|
+
const order = { exact: 0, suffix: 1, basename: 2, substring: 3 };
|
|
357
|
+
const out = [];
|
|
358
|
+
for (const e of entries) {
|
|
359
|
+
if (e.isDir)
|
|
360
|
+
continue;
|
|
361
|
+
const rel = e.rel.toLowerCase();
|
|
362
|
+
const base = rel.slice(rel.lastIndexOf('/') + 1);
|
|
363
|
+
let match = null;
|
|
364
|
+
if (rel === needle)
|
|
365
|
+
match = 'exact';
|
|
366
|
+
else if (rel.endsWith(`/${needle}`))
|
|
367
|
+
match = 'suffix';
|
|
368
|
+
else if (needleBase && base === needleBase)
|
|
369
|
+
match = 'basename';
|
|
370
|
+
else if (rel.includes(needle))
|
|
371
|
+
match = 'substring';
|
|
372
|
+
if (match)
|
|
373
|
+
out.push({ rel: e.rel, size: e.size, match });
|
|
374
|
+
}
|
|
375
|
+
out.sort((a, b) => order[a.match] - order[b.match] || a.rel.length - b.rel.length || (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
function positiveEnvInt(name, fallback) {
|
|
379
|
+
const parsed = Number.parseInt(process.env[name] ?? '', 10);
|
|
380
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
381
|
+
}
|
|
382
|
+
async function listAllEntries(projectPath, budget) {
|
|
383
|
+
const started = Date.now();
|
|
384
|
+
const maxEntries = Math.min(positiveEnvInt('SPECRAILS_CODE_TREE_MAX_ENTRIES', DEFAULT_MAX_TREE_ENTRIES), budget?.maxEntries ?? Infinity);
|
|
385
|
+
const maxMs = Math.min(positiveEnvInt('SPECRAILS_CODE_TREE_MAX_MS', DEFAULT_TREE_SCAN_MS), budget?.maxDurationMs ?? Infinity);
|
|
232
386
|
const out = [];
|
|
233
387
|
const stack = [projectPath];
|
|
388
|
+
let visited = 0;
|
|
389
|
+
let reason = null;
|
|
234
390
|
while (stack.length > 0) {
|
|
391
|
+
if (visited >= maxEntries) {
|
|
392
|
+
reason = 'entry-limit';
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if (Date.now() - started >= maxMs) {
|
|
396
|
+
reason = 'time-limit';
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
235
399
|
const dir = stack.pop();
|
|
236
400
|
let entries;
|
|
237
401
|
try {
|
|
238
|
-
entries = fs_1.default.
|
|
402
|
+
entries = await fs_1.default.promises.readdir(dir, { withFileTypes: true });
|
|
239
403
|
}
|
|
240
404
|
catch {
|
|
405
|
+
reason ??= 'read-errors';
|
|
241
406
|
continue;
|
|
242
407
|
}
|
|
243
408
|
for (const entry of entries) {
|
|
409
|
+
visited += 1;
|
|
410
|
+
if (visited % TREE_YIELD_EVERY === 0)
|
|
411
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
412
|
+
if (visited >= maxEntries) {
|
|
413
|
+
reason = 'entry-limit';
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
if (Date.now() - started >= maxMs) {
|
|
417
|
+
reason = 'time-limit';
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
244
420
|
if (isDenied(entry.name))
|
|
245
421
|
continue;
|
|
246
422
|
const abs = path_1.default.join(dir, entry.name);
|
|
@@ -255,7 +431,7 @@ function listAllEntries(projectPath) {
|
|
|
255
431
|
let size = null;
|
|
256
432
|
let mtime = null;
|
|
257
433
|
try {
|
|
258
|
-
const st = fs_1.default.
|
|
434
|
+
const st = await fs_1.default.promises.stat(abs);
|
|
259
435
|
size = st.size;
|
|
260
436
|
mtime = st.mtimeMs;
|
|
261
437
|
}
|
|
@@ -270,12 +446,22 @@ function listAllEntries(projectPath) {
|
|
|
270
446
|
// Directories are kept — git can't report an ignored dir without its files, and
|
|
271
447
|
// an empty dir node is harmless; its ignored children are already filtered.
|
|
272
448
|
const files = out.filter((e) => !e.isDir).map((e) => e.rel);
|
|
273
|
-
const ignored = gitIgnoredSet(projectPath, files);
|
|
449
|
+
const ignored = await gitIgnoredSet(projectPath, files, budget ? Math.max(1, maxMs - (Date.now() - started)) : Infinity);
|
|
450
|
+
if (ignored.incomplete)
|
|
451
|
+
reason ??= 'read-errors';
|
|
274
452
|
const filtered = ignored.size > 0 ? out.filter((e) => e.isDir || !ignored.has(e.rel)) : out;
|
|
275
453
|
filtered.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
276
|
-
return
|
|
454
|
+
return {
|
|
455
|
+
entries: filtered,
|
|
456
|
+
truncated: reason !== null,
|
|
457
|
+
reason,
|
|
458
|
+
visited,
|
|
459
|
+
durationMs: Date.now() - started,
|
|
460
|
+
maxEntries,
|
|
461
|
+
maxDurationMs: maxMs,
|
|
462
|
+
};
|
|
277
463
|
}
|
|
278
|
-
function listTouchedEntries(projectPath, rowsByPath) {
|
|
464
|
+
async function listTouchedEntries(projectPath, rowsByPath) {
|
|
279
465
|
const seen = new Set();
|
|
280
466
|
const out = [];
|
|
281
467
|
// Drop gitignored files too — mirroring listAllEntries — so a gitignored
|
|
@@ -283,7 +469,9 @@ function listTouchedEntries(projectPath, rowsByPath) {
|
|
|
283
469
|
// not deny-listed never surfaces its path / ticket-attribution / mtime. One
|
|
284
470
|
// batched git check-ignore over the not-already-denied touched files.
|
|
285
471
|
const candidateFiles = [...rowsByPath.keys()].filter((p) => !isDeniedRelPath(p));
|
|
286
|
-
const ignored = gitIgnoredSet(projectPath, candidateFiles);
|
|
472
|
+
const ignored = await gitIgnoredSet(projectPath, candidateFiles, DEFAULT_TREE_SCAN_MS);
|
|
473
|
+
if (ignored.incomplete)
|
|
474
|
+
throw new Error('tree_policy_unavailable');
|
|
287
475
|
for (const filePath of rowsByPath.keys()) {
|
|
288
476
|
// Keep touched-by-ai consistent with the `all` tree (and never surface
|
|
289
477
|
// secrets like .env that an AI job happened to touch).
|
|
@@ -343,6 +531,27 @@ function readSummaryHashSet(projectPath) {
|
|
|
343
531
|
}
|
|
344
532
|
function createCodeExplorerRouter(deps) {
|
|
345
533
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
534
|
+
const repository = deps.repositoryId ? { repositoryId: deps.repositoryId, includeLegacy: deps.includeLegacyProvenance === true } : undefined;
|
|
535
|
+
if (deps.repositoryId) {
|
|
536
|
+
router.use((_req, res, next) => {
|
|
537
|
+
const json = res.json.bind(res);
|
|
538
|
+
const identify = (item) => item && typeof item === 'object'
|
|
539
|
+
? { ...item, repositoryId: deps.repositoryId } : item;
|
|
540
|
+
res.json = (body) => {
|
|
541
|
+
if (Array.isArray(body))
|
|
542
|
+
return json(body.map(identify));
|
|
543
|
+
if (!body || typeof body !== 'object')
|
|
544
|
+
return json(body);
|
|
545
|
+
const result = { ...body, repositoryId: deps.repositoryId };
|
|
546
|
+
for (const key of ['entries', 'matches', 'provenance', 'story']) {
|
|
547
|
+
if (Array.isArray(result[key]))
|
|
548
|
+
result[key] = result[key].map(identify);
|
|
549
|
+
}
|
|
550
|
+
return json(result);
|
|
551
|
+
};
|
|
552
|
+
next();
|
|
553
|
+
});
|
|
554
|
+
}
|
|
346
555
|
const listByPath = deps.listProvenanceByPath ?? file_provenance_1.listProvenanceByPath;
|
|
347
556
|
const listByTicket = deps.listProvenanceByTicket ?? file_provenance_1.listProvenanceByTicket;
|
|
348
557
|
// Summary OUTPUT root (workspace when relocated). Source reads use projectPath.
|
|
@@ -359,21 +568,50 @@ function createCodeExplorerRouter(deps) {
|
|
|
359
568
|
});
|
|
360
569
|
return false;
|
|
361
570
|
}
|
|
362
|
-
// Short-
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
// short enough that tree edits surface promptly.
|
|
571
|
+
// Short-lived discovery cache deduplicates initial tree/find/search scans.
|
|
572
|
+
// Separate bounded path snapshots keep tree pagination stable after this
|
|
573
|
+
// cache expires. Provenance and summaries are enriched per page, not retained
|
|
574
|
+
// as unbounded copies inside every snapshot.
|
|
367
575
|
const WALK_CACHE_TTL_MS = 5000;
|
|
368
|
-
|
|
576
|
+
const allEntriesCache = new Map();
|
|
577
|
+
const allEntriesInFlight = new Map();
|
|
578
|
+
const TREE_SNAPSHOT_TTL_MS = 120_000;
|
|
579
|
+
const MAX_TREE_SNAPSHOTS = 4;
|
|
580
|
+
const treeSnapshots = new Map();
|
|
581
|
+
let watcherScheduled = false;
|
|
369
582
|
let summaryHashCache = null;
|
|
370
583
|
const nowMs = () => Date.now();
|
|
371
|
-
function getAllEntriesCached() {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
584
|
+
async function getAllEntriesCached(relativeRoot = '') {
|
|
585
|
+
const cached = allEntriesCache.get(relativeRoot);
|
|
586
|
+
if (cached && nowMs() - cached.at < WALK_CACHE_TTL_MS) {
|
|
587
|
+
return { scan: cached.scan, cache: 'hit' };
|
|
588
|
+
}
|
|
589
|
+
const existing = allEntriesInFlight.get(relativeRoot);
|
|
590
|
+
if (existing)
|
|
591
|
+
return { scan: await existing, cache: 'shared' };
|
|
592
|
+
const pending = listAllEntries(relativeRoot ? path_1.default.join(deps.projectPath, relativeRoot) : deps.projectPath).then((scan) => relativeRoot
|
|
593
|
+
? { ...scan, entries: scan.entries.map((entry) => ({ ...entry, rel: `${relativeRoot}/${entry.rel}` })) }
|
|
594
|
+
: scan);
|
|
595
|
+
allEntriesInFlight.set(relativeRoot, pending);
|
|
596
|
+
try {
|
|
597
|
+
const scan = await pending;
|
|
598
|
+
// Bound retained trees when an agent searches many different subfolders.
|
|
599
|
+
if (allEntriesCache.size >= 4 && !allEntriesCache.has(relativeRoot))
|
|
600
|
+
allEntriesCache.delete(allEntriesCache.keys().next().value);
|
|
601
|
+
allEntriesCache.set(relativeRoot, { at: nowMs(), scan });
|
|
602
|
+
console.info('[code-explorer] tree scan', JSON.stringify({
|
|
603
|
+
projectId: deps.projectId,
|
|
604
|
+
durationMs: scan.durationMs,
|
|
605
|
+
visited: scan.visited,
|
|
606
|
+
returned: scan.entries.length,
|
|
607
|
+
truncated: scan.truncated,
|
|
608
|
+
reason: scan.reason,
|
|
609
|
+
}));
|
|
610
|
+
return { scan, cache: 'miss' };
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
allEntriesInFlight.delete(relativeRoot);
|
|
614
|
+
}
|
|
377
615
|
}
|
|
378
616
|
function getSummaryHashesCached() {
|
|
379
617
|
if (summaryHashCache && nowMs() - summaryHashCache.at < WALK_CACHE_TTL_MS)
|
|
@@ -392,24 +630,70 @@ function createCodeExplorerRouter(deps) {
|
|
|
392
630
|
// provider without a native pure-output boundary must not attach it:
|
|
393
631
|
// implicit file-change work and explicit POSTs both fail closed before a
|
|
394
632
|
// manager or provider process can run.
|
|
395
|
-
if (aiTransformsAvailable) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
633
|
+
if (aiTransformsAvailable && !watcherScheduled) {
|
|
634
|
+
watcherScheduled = true;
|
|
635
|
+
setImmediate(() => {
|
|
636
|
+
try {
|
|
637
|
+
deps.fileSummaryManager.attachWatcher(deps.projectId, deps.projectPath, summaryRoot(), deps.repositoryId);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
console.warn('[code-explorer] watcher startup failed', JSON.stringify({
|
|
641
|
+
projectId: deps.projectId,
|
|
642
|
+
error: err instanceof Error ? err.message : String(err),
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
});
|
|
400
646
|
}
|
|
401
647
|
next();
|
|
402
648
|
});
|
|
403
|
-
router.get('/tree', (req, res) => {
|
|
649
|
+
router.get('/tree', async (req, res) => {
|
|
404
650
|
const filter = req.query.filter ?? 'touched-by-ai';
|
|
651
|
+
if (filter !== 'all' && filter !== 'touched-by-ai') {
|
|
652
|
+
res.status(400).json({ error: 'invalid_tree_filter' });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
405
655
|
const withProvenance = req.query.withProvenance === '1' || req.query.withProvenance === 'true';
|
|
406
|
-
const
|
|
656
|
+
const requestedLimit = parsePositiveInt(req.query.limit);
|
|
657
|
+
const pageLimit = Math.min(requestedLimit ?? MAX_TREE_PAGE, MAX_TREE_PAGE);
|
|
407
658
|
const ticketId = parsePositiveInt(req.query.ticketId);
|
|
408
659
|
const jobId = parseNonEmptyString(req.query.jobId);
|
|
409
|
-
|
|
660
|
+
if ((req.query.ticketId !== undefined && ticketId === null) || (req.query.jobId !== undefined && jobId === null)) {
|
|
661
|
+
res.status(400).json({ error: 'invalid_tree_scope' });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const scope = JSON.stringify([deps.projectId, deps.repositoryId, deps.projectPath, filter, withProvenance,
|
|
665
|
+
filter === 'touched-by-ai' ? ticketId : null, filter === 'touched-by-ai' ? jobId : null]);
|
|
666
|
+
let cursor;
|
|
667
|
+
try {
|
|
668
|
+
cursor = decodeCursor(req.query.cursor);
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
res.status(400).json({ error: 'invalid_tree_cursor' });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
for (const [id, snapshot] of treeSnapshots) {
|
|
675
|
+
if (nowMs() - snapshot.createdAt >= TREE_SNAPSHOT_TTL_MS)
|
|
676
|
+
treeSnapshots.delete(id);
|
|
677
|
+
}
|
|
678
|
+
let snapshot = cursor && cursor !== 'expired' ? treeSnapshots.get(cursor.snapshot) : undefined;
|
|
679
|
+
if (cursor && (cursor === 'expired' || !snapshot)) {
|
|
680
|
+
res.status(409).json({ error: 'tree_snapshot_expired', retryable: true });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (snapshot && snapshot.scope !== scope) {
|
|
684
|
+
res.status(400).json({ error: 'tree_cursor_scope_mismatch' });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const skip = cursor ? cursor.skip : 0;
|
|
688
|
+
if (snapshot && skip >= snapshot.entries.length) {
|
|
689
|
+
res.status(400).json({ error: 'invalid_tree_cursor' });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
410
692
|
const touchedRowsByPath = new Map();
|
|
411
|
-
|
|
412
|
-
|
|
693
|
+
// Initial touched discovery needs the complete filtered file set. Later
|
|
694
|
+
// pages re-enrich the frozen paths without recomputing their positions.
|
|
695
|
+
if ((filter === 'touched-by-ai' && !snapshot) || withProvenance) {
|
|
696
|
+
const rows = listTouchedRows(deps.db, filter === 'touched-by-ai' ? { ticketId, jobId } : {}, repository);
|
|
413
697
|
for (const row of rows) {
|
|
414
698
|
const existing = touchedRowsByPath.get(row.file_path);
|
|
415
699
|
if (existing)
|
|
@@ -417,47 +701,250 @@ function createCodeExplorerRouter(deps) {
|
|
|
417
701
|
else
|
|
418
702
|
touchedRowsByPath.set(row.file_path, [row]);
|
|
419
703
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
entries
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (existing)
|
|
429
|
-
existing.push(row);
|
|
430
|
-
else
|
|
431
|
-
touchedRowsByPath.set(row.file_path, [row]);
|
|
704
|
+
}
|
|
705
|
+
let cache = snapshot ? 'snapshot' : null;
|
|
706
|
+
if (!snapshot) {
|
|
707
|
+
let entries;
|
|
708
|
+
let scanMeta = null;
|
|
709
|
+
if (filter === 'touched-by-ai') {
|
|
710
|
+
try {
|
|
711
|
+
entries = await listTouchedEntries(deps.projectPath, touchedRowsByPath);
|
|
432
712
|
}
|
|
713
|
+
catch {
|
|
714
|
+
res.status(503).json({ error: 'tree_policy_unavailable', retryable: true });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (entries.length > DEFAULT_MAX_TREE_ENTRIES) {
|
|
718
|
+
scanMeta = { truncated: true, reason: 'entry-limit', visited: entries.length, durationMs: 0,
|
|
719
|
+
maxEntries: DEFAULT_MAX_TREE_ENTRIES, maxDurationMs: DEFAULT_TREE_SCAN_MS };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
const result = await getAllEntriesCached();
|
|
724
|
+
entries = result.scan.entries;
|
|
725
|
+
cache = result.cache;
|
|
726
|
+
const { entries: _entries, ...metadata } = result.scan;
|
|
727
|
+
scanMeta = metadata;
|
|
728
|
+
if (entries.length > DEFAULT_MAX_TREE_ENTRIES) {
|
|
729
|
+
scanMeta = { ...metadata, truncated: true, reason: 'entry-limit', maxEntries: DEFAULT_MAX_TREE_ENTRIES };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
snapshot = { id: (0, node_crypto_1.randomUUID)(), scope, createdAt: nowMs(), entries: entries.slice(0, DEFAULT_MAX_TREE_ENTRIES), scanMeta };
|
|
733
|
+
if (snapshot.entries.length > pageLimit) {
|
|
734
|
+
while (treeSnapshots.size >= MAX_TREE_SNAPSHOTS)
|
|
735
|
+
treeSnapshots.delete(treeSnapshots.keys().next().value);
|
|
736
|
+
treeSnapshots.set(snapshot.id, snapshot);
|
|
433
737
|
}
|
|
434
738
|
}
|
|
435
|
-
const
|
|
436
|
-
const nextCursor = skip +
|
|
437
|
-
//
|
|
438
|
-
//
|
|
739
|
+
const candidates = snapshot.entries.slice(skip, skip + pageLimit);
|
|
740
|
+
const nextCursor = skip + candidates.length < snapshot.entries.length ? encodeCursor(snapshot.id, skip + candidates.length) : null;
|
|
741
|
+
// The cursor preserves position, never access: recheck current exclusions
|
|
742
|
+
// and symlink boundaries. Directory badges must not leak excluded child
|
|
743
|
+
// provenance through their aggregate rows either.
|
|
744
|
+
const policyPaths = new Set(candidates.map(entry => entry.rel));
|
|
745
|
+
if (withProvenance && filter === 'touched-by-ai') {
|
|
746
|
+
const directories = new Set(candidates.filter(entry => entry.isDir).map(entry => entry.rel));
|
|
747
|
+
for (const entry of snapshot.entries) {
|
|
748
|
+
if (entry.isDir)
|
|
749
|
+
continue;
|
|
750
|
+
const parts = entry.rel.split('/');
|
|
751
|
+
while (parts.length > 1) {
|
|
752
|
+
parts.pop();
|
|
753
|
+
if (directories.has(parts.join('/'))) {
|
|
754
|
+
policyPaths.add(entry.rel);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const visibility = await (0, exports.filterCodeExplorerActivityPaths)({
|
|
761
|
+
id: deps.repositoryId ?? `primary-${deps.projectId}`, projectId: deps.projectId,
|
|
762
|
+
name: deps.repositoryName ?? path_1.default.basename(deps.projectPath), path: deps.projectPath,
|
|
763
|
+
isPrimary: deps.includeLegacyProvenance !== false, kind: (0, project_repositories_1.legacyRepositoryKind)(deps.projectPath), integrationBranch: null, addedAt: '',
|
|
764
|
+
}, [...policyPaths], DEFAULT_TREE_SCAN_MS);
|
|
765
|
+
if (visibility.unavailable || visibility.incomplete) {
|
|
766
|
+
res.status(503).json({ error: 'tree_policy_unavailable', retryable: true });
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
for (const filePath of touchedRowsByPath.keys()) {
|
|
770
|
+
if (!visibility.allowed.has(filePath))
|
|
771
|
+
touchedRowsByPath.delete(filePath);
|
|
772
|
+
}
|
|
439
773
|
const summaryHashes = getSummaryHashesCached();
|
|
440
|
-
const out =
|
|
774
|
+
const out = candidates.filter(entry => visibility.allowed.has(entry.rel)).map((e) => {
|
|
441
775
|
const isTouchedDir = filter === 'touched-by-ai' && e.isDir;
|
|
442
776
|
const rawRows = withProvenance && !isTouchedDir ? (touchedRowsByPath.get(e.rel) ?? []) : [];
|
|
443
777
|
const provenance = withProvenance && isTouchedDir
|
|
444
778
|
? rollupDirectoryProvenance(touchedRowsByPath, e.rel)
|
|
445
779
|
: rollupProvenance(rawRows);
|
|
446
|
-
return {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
780
|
+
return { path: e.rel, kind: e.isDir ? 'dir' : 'file', sizeBytes: e.size,
|
|
781
|
+
hasSummary: !e.isDir && summaryHashes.has((0, file_summary_manager_1.pathHash)(e.rel)), provenance, lastModifiedAt: e.mtime };
|
|
782
|
+
});
|
|
783
|
+
const scanMeta = snapshot.scanMeta;
|
|
784
|
+
res.json({
|
|
785
|
+
entries: out.map(entry => ({ ...entry, provenance: treeProvenanceToJson(entry.provenance) })), nextCursor,
|
|
786
|
+
...(scanMeta ? {
|
|
787
|
+
truncated: scanMeta.truncated, truncationReason: scanMeta.reason,
|
|
788
|
+
scan: { visited: scanMeta.visited, durationMs: scanMeta.durationMs, maxEntries: scanMeta.maxEntries,
|
|
789
|
+
maxDurationMs: scanMeta.maxDurationMs, retryable: scanMeta.truncated, cache },
|
|
790
|
+
} : {}),
|
|
454
791
|
});
|
|
792
|
+
});
|
|
793
|
+
/**
|
|
794
|
+
* Locate files by name / path-suffix / fragment across the whole scanned tree
|
|
795
|
+
* (same deny-list + .gitignore rules as `/tree?filter=all`, same cached scan).
|
|
796
|
+
* The escape hatch for a caller holding a path that is relative to some
|
|
797
|
+
* subdirectory (a stack trace, an import): `/file` 404s, `/find` says where
|
|
798
|
+
* the file actually lives.
|
|
799
|
+
*/
|
|
800
|
+
router.get('/find', async (req, res) => {
|
|
801
|
+
const q = parseNonEmptyString(req.query.q);
|
|
802
|
+
if (!q) {
|
|
803
|
+
res.status(400).json({ error: 'q is required' });
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (q.length > MAX_FIND_QUERY_LENGTH) {
|
|
807
|
+
res.status(400).json({ error: `q must be at most ${MAX_FIND_QUERY_LENGTH} characters` });
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const limit = Math.min(parsePositiveInt(req.query.limit) ?? DEFAULT_FIND_LIMIT, MAX_FIND_LIMIT);
|
|
811
|
+
const { scan } = await getAllEntriesCached();
|
|
812
|
+
const ranked = rankFindMatches(scan.entries, q);
|
|
813
|
+
res.json({
|
|
814
|
+
query: q,
|
|
815
|
+
matches: ranked.slice(0, limit).map((m) => ({ path: m.rel, sizeBytes: m.size, match: m.match })),
|
|
816
|
+
total: ranked.length,
|
|
817
|
+
truncated: scan.truncated || ranked.length > limit,
|
|
818
|
+
truncationReason: scan.reason ?? (ranked.length > limit ? 'match-limit' : null),
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
router.get('/search', async (req, res) => {
|
|
822
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
823
|
+
if (!query.trim() || query.length > 256 || /[\r\n]/.test(query)) {
|
|
824
|
+
res.status(400).json({ error: 'q must be a non-empty literal single-line query of at most 256 characters' });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const selectedPath = parseNonEmptyString(req.query.path);
|
|
828
|
+
const rawPath = selectedPath === '.' || selectedPath === './' ? null : selectedPath;
|
|
829
|
+
if (rawPath && !resolveSafePath(deps.projectPath, rawPath)) {
|
|
830
|
+
res.status(400).json({ error: 'path traversal not allowed' });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (rawPath && isDeniedRelPath(rawPath)) {
|
|
834
|
+
res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const prefix = rawPath ? normalizeRel(rawPath).replace(/^\.\//, '') : '';
|
|
838
|
+
const caseSensitive = req.query.caseSensitive === 'true' || req.query.caseSensitive === '1';
|
|
839
|
+
const needle = caseSensitive ? query : query.toLowerCase();
|
|
840
|
+
const limit = Math.min(parsePositiveInt(req.query.limit) ?? 30, 100);
|
|
841
|
+
let scan;
|
|
842
|
+
if (prefix) {
|
|
843
|
+
// Narrowing must narrow the walk itself. Filtering an already truncated
|
|
844
|
+
// full-tree cache would make later subfolders impossible to discover.
|
|
845
|
+
try {
|
|
846
|
+
const stat = await fs_1.default.promises.stat(path_1.default.join(deps.projectPath, prefix));
|
|
847
|
+
if (stat.isDirectory())
|
|
848
|
+
scan = (await getAllEntriesCached(prefix)).scan;
|
|
849
|
+
else if (stat.isFile())
|
|
850
|
+
scan = { entries: [{ rel: prefix, isDir: false, size: stat.size, mtime: stat.mtimeMs }], truncated: false, reason: null, visited: 1, durationMs: 0, maxEntries: 1, maxDurationMs: DEFAULT_TREE_SCAN_MS };
|
|
851
|
+
else {
|
|
852
|
+
res.status(400).json({ error: 'search path is not a regular file or directory' });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
res.status(err.code === 'ENOENT' ? 404 : 500).json({ error: 'search path is unavailable' });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
else
|
|
862
|
+
scan = (await getAllEntriesCached()).scan;
|
|
863
|
+
const candidates = scan.entries.filter((entry) => !entry.isDir && (!prefix || entry.rel === prefix || entry.rel.startsWith(`${prefix}/`)));
|
|
864
|
+
const maxFiles = Math.min(positiveEnvInt('SPECRAILS_CODE_SEARCH_MAX_FILES', MAX_SEARCH_FILES), MAX_SEARCH_FILES);
|
|
865
|
+
const maxBytes = Math.min(positiveEnvInt('SPECRAILS_CODE_SEARCH_MAX_BYTES', MAX_SEARCH_BYTES), MAX_SEARCH_BYTES);
|
|
866
|
+
const maxMs = Math.min(positiveEnvInt('SPECRAILS_CODE_SEARCH_MAX_MS', MAX_SEARCH_MS), MAX_SEARCH_MS);
|
|
867
|
+
const started = Date.now();
|
|
868
|
+
const matches = [];
|
|
869
|
+
const reasons = new Set(scan.reason ? [`tree-${scan.reason}`] : []);
|
|
870
|
+
const skipped = { binary: 0, tooLarge: 0, unreadable: 0, excluded: 0 };
|
|
871
|
+
let scannedFiles = 0;
|
|
872
|
+
let bytesRead = 0;
|
|
873
|
+
// The tree is cached. Revalidate ignore rules for this request so adding a
|
|
874
|
+
// .gitignore cannot leak newly excluded content during the cache TTL.
|
|
875
|
+
const ignored = await gitIgnoredSet(deps.projectPath, candidates.slice(0, maxFiles).map((entry) => entry.rel), maxMs);
|
|
876
|
+
if (ignored.incomplete)
|
|
877
|
+
reasons.add('ignore-rules-unavailable');
|
|
878
|
+
search: for (const entry of candidates) {
|
|
879
|
+
if (Date.now() - started >= maxMs) {
|
|
880
|
+
reasons.add('time-limit');
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
if (scannedFiles >= maxFiles) {
|
|
884
|
+
reasons.add('file-limit');
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
if (bytesRead >= maxBytes) {
|
|
888
|
+
reasons.add('byte-limit');
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
scannedFiles++;
|
|
892
|
+
const abs = resolveSafePath(deps.projectPath, entry.rel);
|
|
893
|
+
if (!abs || isDeniedRelPath(entry.rel) || ignored.has(entry.rel)) {
|
|
894
|
+
skipped.excluded++;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
const data = await readBoundedSource(abs, Math.min(MAX_FILE_BYTES, maxBytes - bytesRead));
|
|
899
|
+
if (data === 'too-large') {
|
|
900
|
+
skipped.tooLarge++;
|
|
901
|
+
reasons.add('oversized-or-changing-files');
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (data === 'not-file') {
|
|
905
|
+
skipped.unreadable++;
|
|
906
|
+
reasons.add('unreadable-files');
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
bytesRead += data.length;
|
|
910
|
+
if (data.includes(0)) {
|
|
911
|
+
skipped.binary++;
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const lines = data.toString('utf8').split('\n');
|
|
915
|
+
let hash = null;
|
|
916
|
+
for (let index = 0; index < lines.length; index++) {
|
|
917
|
+
if (index % 128 === 0 && Date.now() - started >= maxMs) {
|
|
918
|
+
reasons.add('time-limit');
|
|
919
|
+
break search;
|
|
920
|
+
}
|
|
921
|
+
const line = lines[index].replace(/\r$/, '');
|
|
922
|
+
const position = (caseSensitive ? line : line.toLowerCase()).indexOf(needle);
|
|
923
|
+
if (position < 0)
|
|
924
|
+
continue;
|
|
925
|
+
if (matches.length >= limit) {
|
|
926
|
+
reasons.add('match-limit');
|
|
927
|
+
break search;
|
|
928
|
+
}
|
|
929
|
+
const from = Math.max(0, position - 80);
|
|
930
|
+
const to = Math.min(line.length, from + 320);
|
|
931
|
+
hash ??= (0, node_crypto_1.createHash)('sha256').update(data).digest('hex');
|
|
932
|
+
matches.push({ path: entry.rel, lineNumber: index + 1, column: position + 1, snippet: line.slice(from, to), snippetTruncated: from > 0 || to < line.length, fileHash: hash });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
skipped.unreadable++;
|
|
937
|
+
reasons.add('unreadable-files');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const truncated = reasons.size > 0;
|
|
455
941
|
res.json({
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
942
|
+
query, path: prefix || null, caseSensitive, matches, truncated,
|
|
943
|
+
truncationReasons: [...reasons],
|
|
944
|
+
scan: { candidateFiles: candidates.length, scannedFiles, bytesRead, durationMs: Date.now() - started, maxFiles, maxBytes, maxDurationMs: maxMs, skipped, treeTruncated: scan.truncated },
|
|
945
|
+
hint: truncated
|
|
946
|
+
? 'Partial search: absence of a match is not proof of absence. Narrow path/query and retry; use read_file at a returned line with its fileHash as expectedHash.'
|
|
947
|
+
: 'Search complete for eligible text files; binary, denied and gitignored paths are excluded. Use read_file at a matching line.',
|
|
461
948
|
});
|
|
462
949
|
});
|
|
463
950
|
router.get('/file', async (req, res) => {
|
|
@@ -477,11 +964,88 @@ function createCodeExplorerRouter(deps) {
|
|
|
477
964
|
}
|
|
478
965
|
// Canonical POSIX form for all summary/provenance/hash lookups.
|
|
479
966
|
const rel = normalizeRel(relRaw);
|
|
480
|
-
if (isGitIgnored(deps.projectPath, rel)) {
|
|
967
|
+
if (await isGitIgnored(deps.projectPath, rel)) {
|
|
481
968
|
res.status(403).json({ error: 'path is gitignored' });
|
|
482
969
|
return;
|
|
483
970
|
}
|
|
484
971
|
const abs = guard;
|
|
972
|
+
// The editor keeps its existing full-preview contract. MCP callers always
|
|
973
|
+
// supply a range, which avoids spending a model context on a 2 MB response.
|
|
974
|
+
const ranged = req.query.startLine !== undefined || req.query.endLine !== undefined || req.query.startColumn !== undefined;
|
|
975
|
+
if (ranged) {
|
|
976
|
+
const startLine = req.query.startLine === undefined ? 1 : parsePositiveInt(req.query.startLine);
|
|
977
|
+
const endLine = req.query.endLine === undefined && startLine !== null ? startLine + 199 : parsePositiveInt(req.query.endLine);
|
|
978
|
+
const startColumn = req.query.startColumn === undefined ? 1 : parsePositiveInt(req.query.startColumn);
|
|
979
|
+
if (startLine === null || endLine === null || startColumn === null || endLine < startLine) {
|
|
980
|
+
res.status(400).json({ error: 'invalid line range; startLine/endLine/startColumn must be positive integers' });
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const expectedHash = parseNonEmptyString(req.query.expectedHash);
|
|
984
|
+
if (expectedHash && !/^[a-f0-9]{64}$/i.test(expectedHash)) {
|
|
985
|
+
res.status(400).json({ error: 'expectedHash must be a SHA-256 file hash' });
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
let bytes;
|
|
989
|
+
try {
|
|
990
|
+
bytes = await readBoundedSource(abs, MAX_FILE_BYTES);
|
|
991
|
+
}
|
|
992
|
+
catch (err) {
|
|
993
|
+
res.status(err.code === 'ENOENT' ? 404 : 500).json({ error: err.code === 'ENOENT' ? 'file not found' : 'failed to read file' });
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (bytes === 'too-large') {
|
|
997
|
+
res.status(413).json({ error: 'file exceeds the 2 MB read limit or changed while reading; use a narrower code search' });
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (bytes === 'not-file') {
|
|
1001
|
+
res.status(400).json({ error: 'path is not a regular file' });
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (bytes.includes(0)) {
|
|
1005
|
+
res.status(415).json({ error: 'binary file is not readable as source text' });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const fileHash = (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
1009
|
+
if (expectedHash && expectedHash.toLowerCase() !== fileHash) {
|
|
1010
|
+
res.status(409).json({ error: 'file_changed', fileHash, detail: 'The file changed since the previous page/search. Restart the read using the current hash.' });
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const lines = bytes.toString('utf8').split('\n');
|
|
1014
|
+
if (startLine > lines.length || startColumn > lines[startLine - 1].length + 1) {
|
|
1015
|
+
res.status(416).json({ error: 'range_out_of_bounds', totalLines: lines.length, fileHash });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const lastLine = Math.min(endLine, startLine + MAX_CODE_PAGE_LINES - 1, lines.length);
|
|
1019
|
+
let content = '';
|
|
1020
|
+
let actualEndLine = startLine;
|
|
1021
|
+
let nextLine = lastLine < lines.length ? lastLine + 1 : null;
|
|
1022
|
+
let nextColumn = nextLine === null ? null : 1;
|
|
1023
|
+
let reason = nextLine === null ? null : 'line-limit';
|
|
1024
|
+
for (let lineNumber = startLine; lineNumber <= lastLine; lineNumber++) {
|
|
1025
|
+
const column = lineNumber === startLine ? startColumn : 1;
|
|
1026
|
+
const line = lines[lineNumber - 1].slice(column - 1);
|
|
1027
|
+
const remaining = MAX_CODE_PAGE_CHARS - content.length;
|
|
1028
|
+
actualEndLine = lineNumber;
|
|
1029
|
+
if (line.length > remaining) {
|
|
1030
|
+
content += line.slice(0, remaining);
|
|
1031
|
+
nextLine = lineNumber;
|
|
1032
|
+
nextColumn = column + remaining;
|
|
1033
|
+
reason = 'character-limit';
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
content += line;
|
|
1037
|
+
if (content.length >= MAX_CODE_PAGE_CHARS && lineNumber < lastLine) {
|
|
1038
|
+
nextLine = lineNumber + 1;
|
|
1039
|
+
nextColumn = 1;
|
|
1040
|
+
reason = 'character-limit';
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
if (lineNumber < lastLine)
|
|
1044
|
+
content += '\n';
|
|
1045
|
+
}
|
|
1046
|
+
res.json({ path: rel, content, encoding: 'utf-8', language: languageForExt(path_1.default.extname(rel)), fileHash, sizeBytes: bytes.length, startLine, startColumn, endLine: actualEndLine, totalLines: lines.length, nextLine, nextColumn, truncated: nextLine !== null, truncationReason: reason });
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
485
1049
|
let stat;
|
|
486
1050
|
try {
|
|
487
1051
|
stat = fs_1.default.statSync(abs);
|
|
@@ -490,7 +1054,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
490
1054
|
// Honour the staleness scenario: even if content is unavailable, return
|
|
491
1055
|
// the existing summary so the client can render a "not found" banner.
|
|
492
1056
|
const summary = (0, file_summary_manager_1.readSummary)(summaryRoot(), rel);
|
|
493
|
-
const provenance = listByPath(deps.db, deps.projectId, rel);
|
|
1057
|
+
const provenance = listByPath(deps.db, deps.projectId, rel, repository);
|
|
494
1058
|
if (summary || provenance.length > 0) {
|
|
495
1059
|
res.json({
|
|
496
1060
|
content: null,
|
|
@@ -512,7 +1076,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
512
1076
|
res.json({
|
|
513
1077
|
tooLarge: true,
|
|
514
1078
|
sizeBytes: stat.size,
|
|
515
|
-
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
|
|
1079
|
+
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel, repository)),
|
|
516
1080
|
summary: (0, file_summary_manager_1.readSummary)(summaryRoot(), rel),
|
|
517
1081
|
absolutePath: abs,
|
|
518
1082
|
});
|
|
@@ -539,7 +1103,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
539
1103
|
binary: true,
|
|
540
1104
|
sizeBytes: stat.size,
|
|
541
1105
|
mime: 'application/octet-stream',
|
|
542
|
-
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
|
|
1106
|
+
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel, repository)),
|
|
543
1107
|
summary: (0, file_summary_manager_1.readSummary)(summaryRoot(), rel),
|
|
544
1108
|
absolutePath: abs,
|
|
545
1109
|
});
|
|
@@ -554,12 +1118,12 @@ function createCodeExplorerRouter(deps) {
|
|
|
554
1118
|
return;
|
|
555
1119
|
}
|
|
556
1120
|
const summary = (0, file_summary_manager_1.readSummary)(summaryRoot(), rel);
|
|
557
|
-
const summaryStale = await computeStaleness(abs, summary);
|
|
1121
|
+
const summaryStale = await computeStaleness(abs, summary, deps.fileSummaryManager.getLanguage?.() ?? 'en');
|
|
558
1122
|
res.json({
|
|
559
1123
|
content,
|
|
560
1124
|
encoding: 'utf-8',
|
|
561
1125
|
language: languageForExt(path_1.default.extname(rel)),
|
|
562
|
-
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
|
|
1126
|
+
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel, repository)),
|
|
563
1127
|
summary,
|
|
564
1128
|
summaryStale,
|
|
565
1129
|
absolutePath: abs,
|
|
@@ -572,7 +1136,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
572
1136
|
// scope here. After a write the existing hash-gated `summaryStale` flag makes
|
|
573
1137
|
// the next GET /file surface the summary as stale (regenerate via the existing
|
|
574
1138
|
// POST /file/regenerate-summary).
|
|
575
|
-
router.put('/file', (req, res) => {
|
|
1139
|
+
router.put('/file', async (req, res) => {
|
|
576
1140
|
const body = (req.body ?? {});
|
|
577
1141
|
const relRaw = typeof body.path === 'string' ? body.path : undefined;
|
|
578
1142
|
const content = typeof body.content === 'string' ? body.content : undefined;
|
|
@@ -590,7 +1154,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
590
1154
|
return;
|
|
591
1155
|
}
|
|
592
1156
|
const rel = normalizeRel(relRaw);
|
|
593
|
-
if (isGitIgnored(deps.projectPath, rel)) {
|
|
1157
|
+
if (await isGitIgnored(deps.projectPath, rel)) {
|
|
594
1158
|
res.status(403).json({ error: 'path is gitignored' });
|
|
595
1159
|
return;
|
|
596
1160
|
}
|
|
@@ -659,7 +1223,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
659
1223
|
return;
|
|
660
1224
|
}
|
|
661
1225
|
const rel = normalizeRel(relRaw);
|
|
662
|
-
if (isGitIgnored(deps.projectPath, rel)) {
|
|
1226
|
+
if (await isGitIgnored(deps.projectPath, rel)) {
|
|
663
1227
|
res.status(403).json({ error: 'path is gitignored' });
|
|
664
1228
|
return;
|
|
665
1229
|
}
|
|
@@ -670,7 +1234,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
670
1234
|
}
|
|
671
1235
|
let summaryStale = false;
|
|
672
1236
|
try {
|
|
673
|
-
summaryStale = await computeStaleness(guard, summary);
|
|
1237
|
+
summaryStale = await computeStaleness(guard, summary, deps.fileSummaryManager.getLanguage?.() ?? 'en');
|
|
674
1238
|
}
|
|
675
1239
|
catch {
|
|
676
1240
|
summaryStale = true;
|
|
@@ -682,7 +1246,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
682
1246
|
// contribution paragraph when generated, and the spec's live title/status.
|
|
683
1247
|
// Same guards as every sibling content endpoint. Works for deleted files too
|
|
684
1248
|
// (their story is still worth telling), so no stat/existence requirement.
|
|
685
|
-
router.get('/file/story', (req, res) => {
|
|
1249
|
+
router.get('/file/story', async (req, res) => {
|
|
686
1250
|
const relRaw = req.query.path;
|
|
687
1251
|
if (!relRaw || typeof relRaw !== 'string') {
|
|
688
1252
|
res.status(400).json({ error: 'path query parameter is required' });
|
|
@@ -698,11 +1262,11 @@ function createCodeExplorerRouter(deps) {
|
|
|
698
1262
|
return;
|
|
699
1263
|
}
|
|
700
1264
|
const rel = normalizeRel(relRaw);
|
|
701
|
-
if (isGitIgnored(deps.projectPath, rel)) {
|
|
1265
|
+
if (await isGitIgnored(deps.projectPath, rel)) {
|
|
702
1266
|
res.status(403).json({ error: 'path is gitignored' });
|
|
703
1267
|
return;
|
|
704
1268
|
}
|
|
705
|
-
res.json({ path: rel, story: (0, file_story_1.getFileStory)(deps.db, rel, deps.getTicketSpec) });
|
|
1269
|
+
res.json({ path: rel, story: (0, file_story_1.getFileStory)(deps.db, rel, deps.getTicketSpec, repository, deps.fileStoryManager?.getLanguage?.()) });
|
|
706
1270
|
});
|
|
707
1271
|
// Construction story: generate (budget-gated) the plain-language "what this
|
|
708
1272
|
// spec contributed" paragraph for ONE intervention. Awaits the single model
|
|
@@ -726,7 +1290,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
726
1290
|
return;
|
|
727
1291
|
}
|
|
728
1292
|
const rel = normalizeRel(relRaw);
|
|
729
|
-
if (isGitIgnored(deps.projectPath, rel)) {
|
|
1293
|
+
if (await isGitIgnored(deps.projectPath, rel)) {
|
|
730
1294
|
res.status(403).json({ error: 'path is gitignored' });
|
|
731
1295
|
return;
|
|
732
1296
|
}
|
|
@@ -745,8 +1309,10 @@ function createCodeExplorerRouter(deps) {
|
|
|
745
1309
|
try {
|
|
746
1310
|
const result = await deps.fileStoryManager.explain({
|
|
747
1311
|
projectId: deps.projectId,
|
|
1312
|
+
...(repository ? { repository } : {}),
|
|
748
1313
|
relPath: rel,
|
|
749
1314
|
provenanceId,
|
|
1315
|
+
force: body.force === true,
|
|
750
1316
|
overrideBudget: body.overrideBudget === true,
|
|
751
1317
|
});
|
|
752
1318
|
if (result === 'generated') {
|
|
@@ -759,6 +1325,14 @@ function createCodeExplorerRouter(deps) {
|
|
|
759
1325
|
res.status(200).json({ skipped: 'budget' });
|
|
760
1326
|
return;
|
|
761
1327
|
}
|
|
1328
|
+
if (result === 'skipped:hash') {
|
|
1329
|
+
res.json({ ok: true, reused: true });
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (result === 'skipped:no-evidence' || result === 'skipped:ttl') {
|
|
1333
|
+
res.json({ skipped: result.slice('skipped:'.length) });
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
762
1336
|
if (result === 'skipped:not-found') {
|
|
763
1337
|
res.status(404).json({ error: 'intervention not found for this file' });
|
|
764
1338
|
return;
|
|
@@ -788,7 +1362,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
788
1362
|
return;
|
|
789
1363
|
}
|
|
790
1364
|
const rel = normalizeRel(relRaw);
|
|
791
|
-
if (isGitIgnored(deps.projectPath, rel)) {
|
|
1365
|
+
if (await isGitIgnored(deps.projectPath, rel)) {
|
|
792
1366
|
res.status(403).json({ error: 'path is gitignored' });
|
|
793
1367
|
return;
|
|
794
1368
|
}
|
|
@@ -835,6 +1409,7 @@ function createCodeExplorerRouter(deps) {
|
|
|
835
1409
|
summaryRoot: summaryRoot(),
|
|
836
1410
|
projectId: deps.projectId,
|
|
837
1411
|
projectSlug: deps.projectId,
|
|
1412
|
+
...(deps.repositoryId ? { repositoryId: deps.repositoryId } : {}),
|
|
838
1413
|
relPath: rel,
|
|
839
1414
|
triggeredBy: { kind: 'user', id: 'manual', ticketId: null },
|
|
840
1415
|
overrideBudget: body.overrideBudget === true,
|
|
@@ -873,7 +1448,21 @@ function createCodeExplorerRouter(deps) {
|
|
|
873
1448
|
res.status(500).json({ error: 'enqueue failed', message: err.message });
|
|
874
1449
|
}
|
|
875
1450
|
});
|
|
876
|
-
router.get('/
|
|
1451
|
+
router.get('/activity', async (req, res) => {
|
|
1452
|
+
const id = deps.repositoryId ?? `primary-${deps.projectId}`;
|
|
1453
|
+
try {
|
|
1454
|
+
res.json(await (0, code_activity_1.listCodeActivity)(deps.db, {
|
|
1455
|
+
id: deps.projectId, path: deps.projectPath,
|
|
1456
|
+
repositories: [{ id, projectId: deps.projectId, name: deps.repositoryName ?? path_1.default.basename(deps.projectPath),
|
|
1457
|
+
path: deps.projectPath, isPrimary: deps.repositoryId ? deps.includeLegacyProvenance === true : true,
|
|
1458
|
+
kind: (0, project_repositories_1.legacyRepositoryKind)(deps.projectPath), integrationBranch: null, addedAt: '' }],
|
|
1459
|
+
}, (0, code_activity_1.parseCodeActivityQuery)(req.query, id), exports.filterCodeExplorerActivityPaths));
|
|
1460
|
+
}
|
|
1461
|
+
catch (err) {
|
|
1462
|
+
res.status(err instanceof code_activity_1.CodeActivityQueryError ? err.status : 500).json({ error: err instanceof code_activity_1.CodeActivityQueryError ? err.message : 'activity_unavailable' });
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
router.get('/provenance', async (req, res) => {
|
|
877
1466
|
const ticketId = parsePositiveInt(req.query.ticketId);
|
|
878
1467
|
const jobId = parseNonEmptyString(req.query.jobId);
|
|
879
1468
|
const relPath = parseNonEmptyString(req.query.path);
|
|
@@ -899,11 +1488,20 @@ function createCodeExplorerRouter(deps) {
|
|
|
899
1488
|
return;
|
|
900
1489
|
}
|
|
901
1490
|
const rows = ticketId != null && !jobId && !relPath
|
|
902
|
-
? listByTicket(deps.db, deps.projectId, ticketId)
|
|
903
|
-
: listTouchedRows(deps.db, { ticketId, jobId, path: relPath ? normalizeRel(relPath) : relPath });
|
|
904
|
-
|
|
1491
|
+
? listByTicket(deps.db, deps.projectId, ticketId, repository)
|
|
1492
|
+
: listTouchedRows(deps.db, { ticketId, jobId, path: relPath ? normalizeRel(relPath) : relPath }, repository);
|
|
1493
|
+
const visibility = await (0, exports.filterCodeExplorerActivityPaths)({
|
|
1494
|
+
id: deps.repositoryId ?? `primary-${deps.projectId}`, projectId: deps.projectId,
|
|
1495
|
+
name: deps.repositoryName ?? path_1.default.basename(deps.projectPath), path: deps.projectPath,
|
|
1496
|
+
isPrimary: deps.includeLegacyProvenance !== false, kind: (0, project_repositories_1.legacyRepositoryKind)(deps.projectPath), integrationBranch: null, addedAt: '',
|
|
1497
|
+
}, [...new Set(rows.map(row => row.file_path))], 2_000);
|
|
1498
|
+
if (visibility.unavailable || visibility.incomplete) {
|
|
1499
|
+
res.status(503).json({ error: 'provenance_policy_unavailable' });
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
res.json(provenanceRowsToJson(rows.filter(row => visibility.allowed.has(row.file_path))));
|
|
905
1503
|
});
|
|
906
|
-
router.get('/diff', (req, res) => {
|
|
1504
|
+
router.get('/diff', async (req, res) => {
|
|
907
1505
|
const jobId = parseNonEmptyString(req.query.jobId);
|
|
908
1506
|
const relPath = parseNonEmptyString(req.query.path);
|
|
909
1507
|
if (!jobId || !relPath) {
|
|
@@ -924,11 +1522,11 @@ function createCodeExplorerRouter(deps) {
|
|
|
924
1522
|
return;
|
|
925
1523
|
}
|
|
926
1524
|
const rel = normalizeRel(relPath);
|
|
927
|
-
if (isGitIgnored(deps.projectPath, rel)) {
|
|
1525
|
+
if (await isGitIgnored(deps.projectPath, rel)) {
|
|
928
1526
|
res.status(403).json({ error: 'path is gitignored' });
|
|
929
1527
|
return;
|
|
930
1528
|
}
|
|
931
|
-
const diff = (0, file_provenance_1.getProvenanceDiff)(deps.db, deps.projectId, jobId, rel);
|
|
1529
|
+
const diff = (0, file_provenance_1.getProvenanceDiff)(deps.db, deps.projectId, jobId, rel, repository);
|
|
932
1530
|
if (!diff) {
|
|
933
1531
|
res.status(404).json({ error: 'diff not available' });
|
|
934
1532
|
return;
|
|
@@ -988,9 +1586,11 @@ function resolveSafePath(projectPath, relPath) {
|
|
|
988
1586
|
}
|
|
989
1587
|
}
|
|
990
1588
|
}
|
|
991
|
-
async function computeStaleness(abs, summary) {
|
|
1589
|
+
async function computeStaleness(abs, summary, language) {
|
|
992
1590
|
if (!summary)
|
|
993
1591
|
return false;
|
|
1592
|
+
if ((0, file_summary_manager_1.isSummaryMetadataStale)(summary, language))
|
|
1593
|
+
return true;
|
|
994
1594
|
try {
|
|
995
1595
|
const currentHash = await (0, file_summary_manager_1.computeFileHash)(abs);
|
|
996
1596
|
return currentHash !== summary.fileHash;
|