@vantran-se/openchamber-web 2.1.1 → 2.2.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 +32 -24
- package/bin/cli.js +1 -3
- package/bin/cli.test.js +40 -16
- package/bin/lib/DOCUMENTATION.md +6 -5
- package/bin/lib/cli-args.js +6 -1
- package/bin/lib/cli-args.test.js +34 -0
- package/bin/lib/cli-lifecycle.js +0 -1
- package/bin/lib/cli-network.js +8 -2
- package/bin/lib/cli-process.js +1 -1
- package/bin/lib/cli-settings-accessors.test.js +21 -13
- package/bin/lib/cli-startup.js +17 -11
- package/bin/lib/cli-startup.test.js +92 -2
- package/bin/lib/commands-connect-url.js +8 -1
- package/bin/lib/commands-schedule.js +8 -2
- package/bin/lib/commands-serve.js +15 -21
- package/bin/lib/commands-update.js +12 -24
- package/bin/lib/commands-update.test.js +14 -46
- package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
- package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
- package/dist/assets/FilesView-BVoD8I2x.js +14 -0
- package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
- package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
- package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
- package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
- package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
- package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
- package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
- package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
- package/dist/assets/PlanView-RCBadcY7.js +5 -0
- package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
- package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
- package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
- package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
- package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
- package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
- package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
- package/dist/assets/de-YKfk5MmC.js +14 -0
- package/dist/assets/es-_gwT0kr-.js +20 -0
- package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
- package/dist/assets/fr-DqzvXmIm.js +18 -0
- package/dist/assets/index-BLaHKdtx.js +1 -0
- package/dist/assets/index-BkMXRrVQ.css +1 -0
- package/dist/assets/index-DxNa7xYB.js +65 -0
- package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
- package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
- package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
- package/dist/assets/ja-B-GFYhQE.js +20 -0
- package/dist/assets/ko-mzksfqDC.js +20 -0
- package/dist/assets/main-C5Xx8dn_.js +2 -0
- package/dist/assets/main-CfBSQM1H.js +990 -0
- package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
- package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
- package/dist/assets/mobile-DeEQd0wZ.js +2 -0
- package/dist/assets/nl-CWQr6NSq.js +20 -0
- package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
- package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
- package/dist/assets/pl-UmRcDUX2.js +20 -0
- package/dist/assets/pt-BR-BSLybvVF.js +20 -0
- package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
- package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
- package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
- package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
- package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
- package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
- package/dist/assets/space-apply-DM_2pRW7.js +1 -0
- package/dist/assets/space-close-Bpomt8z0.js +1 -0
- package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
- package/dist/assets/tr-gaTrX_HK.js +11 -0
- package/dist/assets/uk-CnJF6ykl.js +20 -0
- package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
- package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
- package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
- package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
- package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
- package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
- package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
- package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
- package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
- package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
- package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
- package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
- package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
- package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
- package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
- package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
- package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
- package/dist/assets/zh-CN-t2vigSB1.js +20 -0
- package/dist/assets/zh-TW-jAgqBokB.js +20 -0
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +8 -5
- package/server/index.js +80 -36
- package/server/lib/agent-memory/actions.js +1 -6
- package/server/lib/agent-memory/routes.js +1 -2
- package/server/lib/agent-memory/runtime.js +1 -8
- package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
- package/server/lib/agent-tool/runtime.js +3 -6
- package/server/lib/agent-tool/runtime.test.js +0 -2
- package/server/lib/browser-control/DOCUMENTATION.md +6 -0
- package/server/lib/changelog/update-notes.js +1 -1
- package/server/lib/context-obligatory/runtime.js +1 -2
- package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
- package/server/lib/dev-tunnel/client.js +21 -10
- package/server/lib/dev-tunnel/tunnel.test.js +34 -0
- package/server/lib/dispatch-results/runtime.js +1 -1
- package/server/lib/enterprise-mode.d.ts +1 -0
- package/server/lib/enterprise-mode.js +13 -2
- package/server/lib/enterprise-mode.test.js +21 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/index.js +1 -3
- package/server/lib/event-stream/translate-v2.js +1 -9
- package/server/lib/event-stream/translate-v2.test.js +0 -11
- package/server/lib/fs/DOCUMENTATION.md +5 -2
- package/server/lib/fs/routes.js +62 -15
- package/server/lib/fs/routes.test.js +278 -48
- package/server/lib/fs/workspace-file-names.js +104 -0
- package/server/lib/fs/workspace-file-names.test.js +62 -0
- package/server/lib/git/DOCUMENTATION.md +17 -5
- package/server/lib/git/contributor-provenance-storage.js +9 -6
- package/server/lib/git/contributor-provenance-storage.test.js +29 -0
- package/server/lib/git/credential-broker.js +1 -1
- package/server/lib/git/credential-resolver.js +1 -4
- package/server/lib/git/credentials.js +1 -0
- package/server/lib/git/discovery-endpoint.js +1 -1
- package/server/lib/git/identity-provisioning.js +2 -1
- package/server/lib/git/identity-storage.js +3 -4
- package/server/lib/git/lfs-discovery.js +14 -14
- package/server/lib/git/lfs-discovery.test.js +16 -6
- package/server/lib/git/network-operation-plan.js +50 -62
- package/server/lib/git/network-operation-plan.test.js +103 -1
- package/server/lib/git/network-operation-registry.js +4 -6
- package/server/lib/git/network-operation-storage.js +4 -6
- package/server/lib/git/network-operations.js +12 -4
- package/server/lib/git/network-operations.test.js +19 -6
- package/server/lib/git/redaction.js +2 -1
- package/server/lib/git/repository-credential-runtime.js +5 -3
- package/server/lib/git/repository-credential-runtime.test.js +22 -0
- package/server/lib/git/routes.js +31 -137
- package/server/lib/git/routes.test.js +2 -2
- package/server/lib/git/service.js +143 -27
- package/server/lib/git/service.test.js +215 -1
- package/server/lib/git/ssh-credential-storage.js +1 -4
- package/server/lib/git/submodule-discovery.js +1 -1
- package/server/lib/git/worktree-bootstrap-storage.js +3 -4
- package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
- package/server/lib/git/worktree-creation.js +194 -0
- package/server/lib/git/worktree-creation.test.js +155 -0
- package/server/lib/github/DOCUMENTATION.md +11 -6
- package/server/lib/github/auth.js +2 -4
- package/server/lib/github/device-flow.js +2 -2
- package/server/lib/github/gh-cli-credential.js +1 -0
- package/server/lib/github/octokit.js +31 -0
- package/server/lib/github/pr-status.js +35 -18
- package/server/lib/github/pr-status.test.js +271 -3
- package/server/lib/github/reference-search.js +102 -13
- package/server/lib/github/reference-search.test.js +48 -7
- package/server/lib/github/routes.auth.test.js +18 -0
- package/server/lib/github/routes.js +360 -6
- package/server/lib/github/routes.mutations.test.js +239 -0
- package/server/lib/github/routes.test.js +134 -2
- package/server/lib/gitlab/DOCUMENTATION.md +5 -1
- package/server/lib/gitlab/auth-storage.test.js +1 -0
- package/server/lib/gitlab/mappers.js +30 -1
- package/server/lib/gitlab/resources.js +230 -6
- package/server/lib/gitlab/resources.test.js +179 -0
- package/server/lib/gitlab/routes.js +200 -6
- package/server/lib/gitlab/routes.test.js +186 -2
- package/server/lib/gitlab/validation.js +1 -5
- package/server/lib/guests/DOCUMENTATION.md +1 -1
- package/server/lib/guests/oauth.js +2 -2
- package/server/lib/guests/service.js +1 -1
- package/server/lib/guests/sockets.js +1 -1
- package/server/lib/injected-env.d.ts +8 -0
- package/server/lib/injected-env.js +50 -0
- package/server/lib/injected-env.test.js +56 -0
- package/server/lib/linear/DOCUMENTATION.md +5 -6
- package/server/lib/linear/issues.js +62 -4
- package/server/lib/linear/issues.test.js +68 -0
- package/server/lib/linear/oauth.js +5 -5
- package/server/lib/linear/parse.js +2 -9
- package/server/lib/message-queue/DOCUMENTATION.md +23 -1
- package/server/lib/message-queue/runtime.js +85 -12
- package/server/lib/message-queue/runtime.test.js +172 -2
- package/server/lib/notifications/apns-runtime.js +5 -6
- package/server/lib/notifications/apns-runtime.test.js +20 -4
- package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
- package/server/lib/openchamber-control/actions.js +2 -1
- package/server/lib/openchamber-control/service.js +119 -20
- package/server/lib/openchamber-control/service.test.js +166 -0
- package/server/lib/openchamber-sessions/archive-store.js +1 -6
- package/server/lib/openchamber-sessions/routes.js +47 -12
- package/server/lib/openchamber-sessions/routes.test.js +65 -2
- package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
- package/server/lib/opencode/DOCUMENTATION.md +22 -8
- package/server/lib/opencode/agents.js +62 -0
- package/server/lib/opencode/auth-state-runtime.js +3 -1
- package/server/lib/opencode/auth.d.ts +1 -1
- package/server/lib/opencode/auth.js +8 -0
- package/server/lib/opencode/auth.test.js +31 -0
- package/server/lib/opencode/config-entity-routes.js +16 -0
- package/server/lib/opencode/config-entity-v2.test.js +16 -1
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.js +2 -4
- package/server/lib/opencode/feature-routes-runtime.js +89 -49
- package/server/lib/opencode/lifecycle.js +41 -21
- package/server/lib/opencode/lifecycle.test.js +50 -2
- package/server/lib/opencode/model-discovery.js +1 -1
- package/server/lib/opencode/model-discovery.test.js +17 -0
- package/server/lib/opencode/models-metadata.js +0 -2
- package/server/lib/opencode/openchamber-routes.test.js +3 -3
- package/server/lib/opencode/plugins.js +6 -9
- package/server/lib/opencode/provider-source-route.test.js +101 -3
- package/server/lib/opencode/providers-disabled.test.js +59 -0
- package/server/lib/opencode/providers.js +34 -0
- package/server/lib/opencode/proxy.js +11 -3
- package/server/lib/opencode/routes.js +40 -2
- package/server/lib/opencode/settings-helpers.js +27 -1
- package/server/lib/opencode/settings-helpers.test.js +19 -7
- package/server/lib/opencode/settings-registry.json +27 -16
- package/server/lib/opencode/settings-runtime.test.js +18 -0
- package/server/lib/opencode/shared-service-runtime.js +23 -104
- package/server/lib/opencode/shared.js +77 -46
- package/server/lib/opencode/shared.test.js +12 -12
- package/server/lib/opencode/shutdown-runtime.test.js +2 -3
- package/server/lib/opencode/skill-routes.js +123 -38
- package/server/lib/opencode/skill-routes.test.js +201 -0
- package/server/lib/opencode/skills.js +81 -34
- package/server/lib/opencode/skills.test.js +176 -21
- package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
- package/server/lib/opencode/watcher.test.js +0 -54
- package/server/lib/outbound-proxy.js +52 -0
- package/server/lib/outbound-proxy.test.js +55 -0
- package/server/lib/package-manager.js +23 -17
- package/server/lib/package-manager.test.js +5 -18
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
- package/server/lib/permission-auto-accept/runtime.js +19 -47
- package/server/lib/permission-auto-accept/runtime.test.js +2 -2
- package/server/lib/project-context/DOCUMENTATION.md +7 -1
- package/server/lib/project-context/routes.js +1 -2
- package/server/lib/project-context/runtime.js +12 -14
- package/server/lib/project-context/runtime.test.js +78 -2
- package/server/lib/projects/project-config.js +30 -15
- package/server/lib/projects/project-config.test.js +30 -0
- package/server/lib/projects/project-setup.js +1 -2
- package/server/lib/projects/routes.js +1 -2
- package/server/lib/quota/DOCUMENTATION.md +35 -7
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/claude/index.js +1 -1
- package/server/lib/quota/providers/cline-pass.js +1 -1
- package/server/lib/quota/providers/exe-dev.js +1 -1
- package/server/lib/quota/providers/google/index.js +1 -1
- package/server/lib/quota/providers/hyper.js +1 -1
- package/server/lib/quota/providers/kimi.js +116 -36
- package/server/lib/quota/providers/kimi.test.js +128 -3
- package/server/lib/quota/providers/opencode-go.js +144 -21
- package/server/lib/quota/providers/opencode-go.test.js +229 -6
- package/server/lib/quota/providers/openrouter.js +1 -1
- package/server/lib/quota/providers/zenmux.js +1 -1
- package/server/lib/quota/utils/auth.js +1 -0
- package/server/lib/quota/utils/transformers.js +2 -6
- package/server/lib/relay/DOCUMENTATION.md +18 -3
- package/server/lib/relay/e2ee.js +7 -7
- package/server/lib/relay/host-client.js +84 -13
- package/server/lib/relay/host-client.test.js +201 -1
- package/server/lib/relay/identity.js +6 -29
- package/server/lib/relay/identity.test.js +18 -28
- package/server/lib/relay/key-store.js +141 -0
- package/server/lib/relay/key-store.test.js +138 -0
- package/server/lib/relay/service.js +8 -8
- package/server/lib/relay/service.test.js +10 -5
- package/server/lib/relay/signing-key.js +13 -36
- package/server/lib/relay/tunnel-codec.js +10 -10
- package/server/lib/route-module-exports.test.js +22 -0
- package/server/lib/routing/DOCUMENTATION.md +23 -3
- package/server/lib/routing/cache.js +87 -0
- package/server/lib/routing/defaults.js +11 -0
- package/server/lib/routing/history.js +12 -4
- package/server/lib/routing/runtime.js +39 -3
- package/server/lib/routing/runtime.test.js +81 -2
- package/server/lib/routing/store.js +12 -1
- package/server/lib/routing/store.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
- package/server/lib/scheduled-tasks/existing-session.js +119 -0
- package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
- package/server/lib/scheduled-tasks/loops.js +1 -8
- package/server/lib/scheduled-tasks/routes.js +8 -8
- package/server/lib/scheduled-tasks/runtime.js +103 -13
- package/server/lib/scheduled-tasks/runtime.test.js +48 -0
- package/server/lib/scheduled-tasks/service.js +33 -11
- package/server/lib/scheduled-tasks/service.test.js +128 -2
- package/server/lib/scheduled-tasks/session-defaults.js +27 -0
- package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
- package/server/lib/session-assist/context.js +1 -0
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-goal/objectives.js +1 -1
- package/server/lib/session-knowledge/routes.js +2 -2
- package/server/lib/session-knowledge/runtime.js +1 -2
- package/server/lib/session-work/DOCUMENTATION.md +39 -14
- package/server/lib/session-work/questions.js +31 -2
- package/server/lib/session-work/runtime.js +22 -13
- package/server/lib/session-work/runtime.test.js +43 -26
- package/server/lib/session-work/state.js +10 -4
- package/server/lib/session-work/state.test.js +8 -1
- package/server/lib/shared/DOCUMENTATION.md +89 -0
- package/server/lib/shared/guards.js +53 -0
- package/server/lib/shared/guards.test.js +112 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
- package/server/lib/skills-catalog/cache.js +3 -2
- package/server/lib/skills-catalog/curated-sources.js +5 -1
- package/server/lib/skills-catalog/git.js +107 -20
- package/server/lib/skills-catalog/git.test.js +78 -0
- package/server/lib/skills-catalog/install.js +21 -12
- package/server/lib/skills-catalog/scan.js +25 -15
- package/server/lib/skills-catalog/source.js +26 -1
- package/server/lib/skills-catalog/source.test.js +27 -0
- package/server/lib/source-control/DOCUMENTATION.md +2 -1
- package/server/lib/source-control/audit-storage.js +7 -6
- package/server/lib/source-control/audit-storage.test.js +11 -0
- package/server/lib/source-control/binding-service.js +2 -5
- package/server/lib/source-control/change-request-worktree.js +90 -0
- package/server/lib/source-control/change-request-worktree.test.js +121 -0
- package/server/lib/source-control/mutation-executor.js +1 -1
- package/server/lib/source-control/mutation-storage.js +11 -5
- package/server/lib/source-control/provider-instance.js +1 -2
- package/server/lib/source-control/repository-identity.js +1 -1
- package/server/lib/source-control/routes.js +14 -2
- package/server/lib/spaces/DOCUMENTATION.md +42 -17
- package/server/lib/spaces/command-stream.test.js +15 -0
- package/server/lib/spaces/dispatcher.js +7 -0
- package/server/lib/spaces/dispatcher.test.js +17 -0
- package/server/lib/spaces/hardening.js +2 -2
- package/server/lib/spaces/hardening.test.js +1 -1
- package/server/lib/spaces/host.js +2 -0
- package/server/lib/spaces/journey.js +66 -5
- package/server/lib/spaces/journey.test.js +95 -4
- package/server/lib/spaces/layout.js +1 -1
- package/server/lib/spaces/places/docker-disk.js +38 -14
- package/server/lib/spaces/places/docker-disk.test.js +80 -1
- package/server/lib/spaces/places/docker.js +35 -11
- package/server/lib/spaces/places/fake-docker.js +22 -3
- package/server/lib/spaces/places/memory-place.js +3 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
- package/server/lib/spaces/routes.js +5 -0
- package/server/lib/spaces/routes.test.js +4 -2
- package/server/lib/spaces/run-command.js +9 -2
- package/server/lib/spaces/tools.js +4 -5
- package/server/lib/spaces/tools.test.js +2 -2
- package/server/lib/terminal/runtime.js +18 -6
- package/server/lib/terminal/runtime.test.js +66 -0
- package/server/lib/tts/index.js +3 -0
- package/server/lib/tts/service.js +4 -2
- package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
- package/server/lib/walkthrough/index.js +2 -3
- package/server/lib/walkthrough/pr-source.test.js +22 -0
- package/server/lib/walkthrough/sources.js +6 -1
- package/server/opencode-proxy.test.js +127 -0
- package/bin/lib/commands-startup.test.js +0 -15
- package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
- package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
- package/dist/assets/FilesView-DT0uEYgT.js +0 -17
- package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
- package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
- package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
- package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
- package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
- package/dist/assets/PlanView-DsRWnsOI.js +0 -5
- package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
- package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
- package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
- package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
- package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
- package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
- package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
- package/dist/assets/de-CBla4hjd.js +0 -14
- package/dist/assets/es-BmnZtxTD.js +0 -20
- package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
- package/dist/assets/fr-OVKIBUZO.js +0 -18
- package/dist/assets/index-CKCFKmjF.js +0 -1
- package/dist/assets/index-Dbx4V4GN.css +0 -1
- package/dist/assets/index-vQ1Pg1cr.js +0 -61
- package/dist/assets/ja-6v-yr-gE.js +0 -20
- package/dist/assets/ko-I_ZLvgQ9.js +0 -20
- package/dist/assets/main-CmO4dRtR.js +0 -2
- package/dist/assets/main-b9vqhlYM.js +0 -999
- package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
- package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
- package/dist/assets/mobile-CVtShif9.js +0 -2
- package/dist/assets/nl-DcvIJf46.js +0 -20
- package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
- package/dist/assets/pl-JxCUl0sd.js +0 -20
- package/dist/assets/pt-BR-L89mjja2.js +0 -20
- package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
- package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
- package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
- package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
- package/dist/assets/space-apply-1trGVIl8.js +0 -1
- package/dist/assets/space-close-y7j-NuBv.js +0 -1
- package/dist/assets/tr-ka5J1flt.js +0 -11
- package/dist/assets/uk-PyGe6Q_u.js +0 -20
- package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
- package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
- package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
- package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
- package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
- package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
- package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
- package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* "In work": Jev moves a session into work when real work starts in it, and
|
|
3
|
-
* says when a turn looks like the end of that work
|
|
4
|
-
* runtime never does. See
|
|
3
|
+
* says when a turn looks like the end of that work, or handed over changes
|
|
4
|
+
* worth a look. The user closes; this runtime never does. See
|
|
5
|
+
* DOCUMENTATION.md.
|
|
5
6
|
*
|
|
6
7
|
* Two moments ask Jev:
|
|
7
8
|
* - a user message was sent (`message.updated`, role user): only while the
|
|
@@ -26,9 +27,10 @@ import {
|
|
|
26
27
|
buildTurnEndRequest,
|
|
27
28
|
decideAssist,
|
|
28
29
|
decideOpen,
|
|
30
|
+
decideReviewReady,
|
|
29
31
|
decideWrapUp,
|
|
30
32
|
} from './questions.js';
|
|
31
|
-
import { clearSuggestionPatch, openByJevPatch, readWork, suggestDonePatch } from './state.js';
|
|
33
|
+
import { clearSuggestionPatch, offerReviewPatch, openByJevPatch, readWork, suggestDonePatch } from './state.js';
|
|
32
34
|
|
|
33
35
|
const OPENCHAMBER_SETTINGS_FILE = path.join(
|
|
34
36
|
process.env.OPENCHAMBER_DATA_DIR
|
|
@@ -37,17 +39,20 @@ const OPENCHAMBER_SETTINGS_FILE = path.join(
|
|
|
37
39
|
'settings.json',
|
|
38
40
|
);
|
|
39
41
|
|
|
40
|
-
/**
|
|
42
|
+
/** In work and auto-open default on, the review offer off; read at every use so a change applies without a restart. */
|
|
41
43
|
const readSessionWorkSettings = () => {
|
|
42
44
|
const settings = readMergedSettingsSync({ fs, path, settingsFilePath: OPENCHAMBER_SETTINGS_FILE });
|
|
43
45
|
return {
|
|
44
46
|
enabled: settings.sessionWorkEnabled !== false,
|
|
45
47
|
autoOpen: settings.sessionWorkAutoOpen !== false,
|
|
48
|
+
reviewOffer: settings.sessionReviewOfferEnabled === true,
|
|
46
49
|
};
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
const READ_TIMEOUT_MS = 5_000;
|
|
50
53
|
const REQUEST_CHARS = 1_500;
|
|
54
|
+
/** Head and tail of the turn's answer: what was changed sits early, the outcome late. */
|
|
55
|
+
const ANSWER_EDGE_CHARS = 500;
|
|
51
56
|
const SEEN_MESSAGES_LIMIT = 500;
|
|
52
57
|
|
|
53
58
|
const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
|
|
@@ -75,7 +80,7 @@ const reviewSessionSchema = z.object({ openchamber: z.object({ kind: z.literal('
|
|
|
75
80
|
export function createSessionWorkRuntime({
|
|
76
81
|
buildOpenCodeUrl,
|
|
77
82
|
getOpenCodeAuthHeaders,
|
|
78
|
-
/** `{ enabled, autoOpen }` as currently saved. */
|
|
83
|
+
/** `{ enabled, autoOpen, reviewOffer }` as currently saved. */
|
|
79
84
|
getSettings = readSessionWorkSettings,
|
|
80
85
|
/** The classification provider's endpoint, or null when there is no Jev. */
|
|
81
86
|
classifierEndpoint,
|
|
@@ -98,12 +103,12 @@ export function createSessionWorkRuntime({
|
|
|
98
103
|
};
|
|
99
104
|
let stopped = false;
|
|
100
105
|
const seenMessages = new Set();
|
|
101
|
-
// Sessions where this process wrote a
|
|
106
|
+
// Sessions where this process wrote a hint, so a new turn can retire it
|
|
102
107
|
// without reading every session's metadata at every turn start.
|
|
103
108
|
const suggested = new Map();
|
|
104
109
|
// A turn counter per session, moved by every turn start and every new user
|
|
105
110
|
// message. A turn-end check captures it before asking Jev and writes the
|
|
106
|
-
//
|
|
111
|
+
// hint only if no newer turn began in the meantime: a late answer about
|
|
107
112
|
// the previous turn must not land on the next one.
|
|
108
113
|
// Values come from one process-wide counter, so they never repeat: a session
|
|
109
114
|
// evicted from the bounded map reads 0, which no pending check captured
|
|
@@ -139,7 +144,6 @@ export function createSessionWorkRuntime({
|
|
|
139
144
|
const session = await client.session.get({ sessionID: sessionId }, { signal });
|
|
140
145
|
if (session?.id === sessionId) lineage?.remember(sessionId, session.parentID ?? null);
|
|
141
146
|
if (session?.id !== sessionId || session.parentID) return null;
|
|
142
|
-
if (session.metadata?.openchamber?.adopted !== true) return null;
|
|
143
147
|
if (isChatDirectory(session.location?.directory ?? session.directory)) return null;
|
|
144
148
|
if (reviewSessionSchema.safeParse(session.metadata).success) return null;
|
|
145
149
|
if (await isSessionArchived(sessionId)) return null;
|
|
@@ -181,7 +185,7 @@ export function createSessionWorkRuntime({
|
|
|
181
185
|
const directory = suggested.get(sessionId);
|
|
182
186
|
suggested.delete(sessionId);
|
|
183
187
|
Promise.resolve(updateMetadata(sessionId, clearSuggestionPatch, { directory }))
|
|
184
|
-
.catch((error) => console.warn('[session-work] could not retire a
|
|
188
|
+
.catch((error) => console.warn('[session-work] could not retire a hint:', errorMessage(error)));
|
|
185
189
|
};
|
|
186
190
|
|
|
187
191
|
const processPayload = (payload, directoryHint = '') => {
|
|
@@ -217,7 +221,7 @@ export function createSessionWorkRuntime({
|
|
|
217
221
|
const evaluateTurnEnd = async ({ sessionId, directory, assist }) => {
|
|
218
222
|
if (stopped || lineage?.isChild(sessionId) === true) return null;
|
|
219
223
|
const settings = getSettings();
|
|
220
|
-
if (!settings.enabled && !assist.recap && !assist.suggestion) return null;
|
|
224
|
+
if (!settings.enabled && !settings.reviewOffer && !assist.recap && !assist.suggestion) return null;
|
|
221
225
|
// Never capture 0: an evicted session also reads 0.
|
|
222
226
|
if (!turnGenerations.has(sessionId)) advanceTurn(sessionId);
|
|
223
227
|
const generation = turnGeneration(sessionId);
|
|
@@ -232,6 +236,7 @@ export function createSessionWorkRuntime({
|
|
|
232
236
|
const ask = {
|
|
233
237
|
open: settings.enabled && settings.autoOpen && work?.state !== 'open',
|
|
234
238
|
wrapUp: settings.enabled && work?.state === 'open',
|
|
239
|
+
reviewReady: settings.reviewOffer,
|
|
235
240
|
recap: assist.recap,
|
|
236
241
|
nextStep: assist.suggestion,
|
|
237
242
|
};
|
|
@@ -241,7 +246,7 @@ export function createSessionWorkRuntime({
|
|
|
241
246
|
const request = buildTurnEndRequest({
|
|
242
247
|
history: turnsToHistory(context.turns.slice(0, -1)),
|
|
243
248
|
request: excerptHead(turn.user.text, REQUEST_CHARS),
|
|
244
|
-
answer: excerptHeadTail(turn.assistant.text,
|
|
249
|
+
answer: excerptHeadTail(turn.assistant.text, ANSWER_EDGE_CHARS, ANSWER_EDGE_CHARS),
|
|
245
250
|
ask,
|
|
246
251
|
});
|
|
247
252
|
if (!request) return null;
|
|
@@ -251,12 +256,16 @@ export function createSessionWorkRuntime({
|
|
|
251
256
|
const requestAt = turn.user.created ?? now();
|
|
252
257
|
await updateMetadata(sessionId, (metadata) => openByJevPatch(metadata, { requestAt, now: now() }), { directory });
|
|
253
258
|
}
|
|
254
|
-
|
|
259
|
+
// Looks done wins over a review offer from the same turn.
|
|
260
|
+
const suggest = ask.wrapUp && decideWrapUp(answers)
|
|
261
|
+
? suggestDonePatch
|
|
262
|
+
: ask.reviewReady && decideReviewReady(answers) ? offerReviewPatch : null;
|
|
263
|
+
if (suggest) {
|
|
255
264
|
// Decided at write time: a turn that started while Jev was answering
|
|
256
265
|
// makes this answer about an older turn.
|
|
257
266
|
const { changed } = await updateMetadata(
|
|
258
267
|
sessionId,
|
|
259
|
-
(metadata) => (turnGeneration(sessionId) === generation ?
|
|
268
|
+
(metadata) => (turnGeneration(sessionId) === generation ? suggest(metadata, { now: now() }) : null),
|
|
260
269
|
{ directory },
|
|
261
270
|
);
|
|
262
271
|
if (changed) markSuggested(sessionId, directory);
|
|
@@ -18,12 +18,11 @@ const answeredTurn = [
|
|
|
18
18
|
...settledPair,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
const stubOpenCode = ({ session, records = answeredTurn } = {}) => {
|
|
22
|
-
const sessionRecord = session ?? { metadata: { openchamber: { adopted: true } } };
|
|
21
|
+
const stubOpenCode = ({ session = {}, records = answeredTurn } = {}) => {
|
|
23
22
|
const fetchMock = vi.fn(async (input) => {
|
|
24
23
|
const url = new URL(String(input));
|
|
25
24
|
let body;
|
|
26
|
-
if (url.pathname === '/api/session/ses_1') body = { data: { id: 'ses_1', ...
|
|
25
|
+
if (url.pathname === '/api/session/ses_1') body = { data: { id: 'ses_1', ...session } };
|
|
27
26
|
else if (url.pathname === '/api/session/ses_1/message') body = { data: records, cursor: {} };
|
|
28
27
|
else throw new Error(`unexpected ${url.pathname}`);
|
|
29
28
|
return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
@@ -32,7 +31,7 @@ const stubOpenCode = ({ session, records = answeredTurn } = {}) => {
|
|
|
32
31
|
return fetchMock;
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
const makeRuntime = ({ metadata = {
|
|
34
|
+
const makeRuntime = ({ metadata = {}, answers = {}, askError = null, settings = {}, endpoint = ENDPOINT, lineage = null } = {}) => {
|
|
36
35
|
const records = new Map([['ses_1', metadata]]);
|
|
37
36
|
const updateMetadata = vi.fn(async (id, decide) => {
|
|
38
37
|
const current = records.get(id) ?? {};
|
|
@@ -47,7 +46,7 @@ const makeRuntime = ({ metadata = { openchamber: { adopted: true } }, answers =
|
|
|
47
46
|
const runtime = createSessionWorkRuntime({
|
|
48
47
|
buildOpenCodeUrl: (fetchPath) => `http://opencode.test${fetchPath}`,
|
|
49
48
|
getOpenCodeAuthHeaders: () => ({}),
|
|
50
|
-
getSettings: () => ({ enabled: true, autoOpen: true, ...settings }),
|
|
49
|
+
getSettings: () => ({ enabled: true, autoOpen: true, reviewOffer: true, ...settings }),
|
|
51
50
|
classifierEndpoint: async () => endpoint,
|
|
52
51
|
jev,
|
|
53
52
|
readMetadata: async (id) => records.get(id) ?? {},
|
|
@@ -89,24 +88,13 @@ describe('session work decisions', () => {
|
|
|
89
88
|
});
|
|
90
89
|
|
|
91
90
|
describe('session work runtime: a message was sent', () => {
|
|
92
|
-
it('does not send a TUI-only session to Jev', async () => {
|
|
93
|
-
stubOpenCode({ session: {}, records: [{ id: 'msg_new', type: 'user', text: 'Keep this local' }, ...settledPair] });
|
|
94
|
-
const { runtime, jev } = makeRuntime({ metadata: {} });
|
|
95
|
-
|
|
96
|
-
runtime.processPayload(sent('msg_new', 'Keep this local'));
|
|
97
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
98
|
-
|
|
99
|
-
expect(jev.ask).not.toHaveBeenCalled();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
91
|
it('asks Jev on the new message with the settled turns as history, and opens the session', async () => {
|
|
103
92
|
stubOpenCode({ records: [{ id: 'msg_new', type: 'user', text: 'Now fix the header' }, ...settledPair] });
|
|
104
93
|
const { runtime, jev, records } = makeRuntime({ answers: { change: { noul: 0.95 }, toward_change: { noul: 0.9 } } });
|
|
105
94
|
|
|
106
95
|
runtime.processPayload(sent());
|
|
107
|
-
await settle(() => records.get('ses_1').openchamber
|
|
96
|
+
await settle(() => records.get('ses_1').openchamber);
|
|
108
97
|
|
|
109
|
-
expect(jev.ask).toHaveBeenCalledOnce();
|
|
110
98
|
const [request] = jev.ask.mock.calls[0];
|
|
111
99
|
expect(request.state.request).toBe('Now fix the header');
|
|
112
100
|
expect(request.state.history.map((entry) => entry.role)).toEqual(['user', 'assistant']);
|
|
@@ -116,7 +104,7 @@ describe('session work runtime: a message was sent', () => {
|
|
|
116
104
|
|
|
117
105
|
it('does not ask again for a session already in work, or for the same message twice', async () => {
|
|
118
106
|
stubOpenCode();
|
|
119
|
-
const open = makeRuntime({ metadata: { openchamber: {
|
|
107
|
+
const open = makeRuntime({ metadata: { openchamber: { work: { state: 'open' } } } });
|
|
120
108
|
open.runtime.processPayload(sent());
|
|
121
109
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
122
110
|
expect(open.jev.ask).not.toHaveBeenCalled();
|
|
@@ -177,22 +165,22 @@ describe('session work runtime: a turn ended', () => {
|
|
|
177
165
|
const gate = await runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: true, suggestion: true } });
|
|
178
166
|
|
|
179
167
|
expect(jev.ask).toHaveBeenCalledTimes(1);
|
|
180
|
-
expect(Object.keys(jev.ask.mock.calls[0][0].questions).sort()).toEqual(['change', 'next_step', 'recap', 'toward_change']);
|
|
168
|
+
expect(Object.keys(jev.ask.mock.calls[0][0].questions).sort()).toEqual(['change', 'next_step', 'recap', 'review_ready', 'toward_change']);
|
|
181
169
|
expect(jev.ask.mock.calls[0][0].state).toMatchObject({ request: 'Implement it', answer: 'Done, try it.' });
|
|
182
170
|
expect(gate).toEqual({ recap: true, suggestion: false });
|
|
183
171
|
expect(records.get('ses_1')).toMatchObject({ openchamber: { work: { state: 'open' } } });
|
|
184
172
|
});
|
|
185
173
|
|
|
186
|
-
it('asks only
|
|
174
|
+
it('asks only about the end of the work and a review in a session already in work, and a new turn retires the hint', async () => {
|
|
187
175
|
stubOpenCode();
|
|
188
176
|
const { runtime, jev, records, updateMetadata } = makeRuntime({
|
|
189
|
-
metadata: { openchamber: {
|
|
177
|
+
metadata: { openchamber: { work: { state: 'open', openedAt: 1 } } },
|
|
190
178
|
answers: { wrap_up: { noul: 0.93 } },
|
|
191
179
|
});
|
|
192
180
|
|
|
193
181
|
const gate = await runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: false, suggestion: false } });
|
|
194
182
|
|
|
195
|
-
expect(Object.keys(jev.ask.mock.calls[0][0].questions)).toEqual(['wrap_up']);
|
|
183
|
+
expect(Object.keys(jev.ask.mock.calls[0][0].questions)).toEqual(['wrap_up', 'review_ready']);
|
|
196
184
|
expect(gate).toBeNull();
|
|
197
185
|
expect(records.get('ses_1').openchamber.work.suggestDoneAt).toBeTypeOf('number');
|
|
198
186
|
|
|
@@ -205,10 +193,39 @@ describe('session work runtime: a turn ended', () => {
|
|
|
205
193
|
expect(updateMetadata).toHaveBeenCalledTimes(2);
|
|
206
194
|
});
|
|
207
195
|
|
|
196
|
+
it('offers a review after a turn that changed the project, and looks done wins over it', async () => {
|
|
197
|
+
stubOpenCode();
|
|
198
|
+
const open = { openchamber: { work: { state: 'open', openedAt: 1 } } };
|
|
199
|
+
const review = makeRuntime({ metadata: open, answers: { wrap_up: { noul: 0.2 }, review_ready: { noul: 0.75 } } });
|
|
200
|
+
await review.runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: false, suggestion: false } });
|
|
201
|
+
expect(review.records.get('ses_1').openchamber.reviewOffer.at).toBeTypeOf('number');
|
|
202
|
+
expect(review.records.get('ses_1').openchamber.work).not.toHaveProperty('suggestDoneAt');
|
|
203
|
+
|
|
204
|
+
const both = makeRuntime({ metadata: open, answers: { wrap_up: { noul: 0.9 }, review_ready: { noul: 0.9 } } });
|
|
205
|
+
await both.runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: false, suggestion: false } });
|
|
206
|
+
expect(both.records.get('ses_1').openchamber.work.suggestDoneAt).toBeTypeOf('number');
|
|
207
|
+
expect(both.records.get('ses_1').openchamber).not.toHaveProperty('reviewOffer');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('offers a review without In work, asks nothing about it when it is off, and a new turn retires it', async () => {
|
|
211
|
+
stubOpenCode();
|
|
212
|
+
const solo = makeRuntime({ settings: { enabled: false }, answers: { review_ready: { noul: 0.8 } } });
|
|
213
|
+
await solo.runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: false, suggestion: false } });
|
|
214
|
+
expect(Object.keys(solo.jev.ask.mock.calls[0][0].questions)).toEqual(['review_ready']);
|
|
215
|
+
expect(solo.records.get('ses_1')).toEqual({ openchamber: { reviewOffer: { at: expect.any(Number) } } });
|
|
216
|
+
solo.runtime.processPayload({ type: 'session.status', properties: { sessionID: 'ses_1', status: { type: 'busy' } } });
|
|
217
|
+
await settle(() => !solo.records.get('ses_1').openchamber?.reviewOffer);
|
|
218
|
+
expect(solo.records.get('ses_1').openchamber).not.toHaveProperty('reviewOffer');
|
|
219
|
+
|
|
220
|
+
const off = makeRuntime({ settings: { reviewOffer: false }, answers: { change: { noul: 0.1 } } });
|
|
221
|
+
await off.runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: false, suggestion: false } });
|
|
222
|
+
expect(Object.keys(off.jev.ask.mock.calls[0][0].questions)).not.toContain('review_ready');
|
|
223
|
+
});
|
|
224
|
+
|
|
208
225
|
it('drops a late done answer once the next turn started', async () => {
|
|
209
226
|
stubOpenCode();
|
|
210
227
|
const { runtime, jev, records } = makeRuntime({
|
|
211
|
-
metadata: { openchamber: {
|
|
228
|
+
metadata: { openchamber: { work: { state: 'open', openedAt: 1 } } },
|
|
212
229
|
});
|
|
213
230
|
let answer;
|
|
214
231
|
jev.ask.mockImplementation(() => new Promise((resolve) => { answer = resolve; }));
|
|
@@ -239,7 +256,7 @@ describe('session work runtime: a turn ended', () => {
|
|
|
239
256
|
it('never closes: a closing turn in a session in work only hints', async () => {
|
|
240
257
|
stubOpenCode();
|
|
241
258
|
const { runtime, records } = makeRuntime({
|
|
242
|
-
metadata: { openchamber: {
|
|
259
|
+
metadata: { openchamber: { work: { state: 'open', openedAt: 1 } } },
|
|
243
260
|
answers: { wrap_up: { noul: 0.99 }, change: { noul: 0 } },
|
|
244
261
|
});
|
|
245
262
|
await runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: false, suggestion: false } });
|
|
@@ -257,9 +274,9 @@ describe('session work runtime: a turn ended', () => {
|
|
|
257
274
|
expect(updateMetadata).not.toHaveBeenCalled();
|
|
258
275
|
});
|
|
259
276
|
|
|
260
|
-
it('makes no call when the
|
|
277
|
+
it('makes no call when In work, the review offer and both assist fields are off', async () => {
|
|
261
278
|
const fetchMock = stubOpenCode();
|
|
262
|
-
const { runtime, jev } = makeRuntime({ settings: { enabled: false } });
|
|
279
|
+
const { runtime, jev } = makeRuntime({ settings: { enabled: false, reviewOffer: false } });
|
|
263
280
|
expect(await runtime.evaluateTurnEnd({ sessionId: 'ses_1', directory: '/repo', assist: { recap: false, suggestion: false } })).toBeNull();
|
|
264
281
|
expect(jev.ask).not.toHaveBeenCalled();
|
|
265
282
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `metadata.openchamber.work`: whether a session is in work and what Jev
|
|
3
|
-
* currently suggests about it.
|
|
3
|
+
* currently suggests about it; `metadata.openchamber.reviewOffer`: Jev's offer
|
|
4
|
+
* to look over the last turn's changes, which does not depend on work. Every function returns a JSON merge patch for
|
|
4
5
|
* the session metadata store, or null when nothing changes, so the store can
|
|
5
6
|
* decide against the record as it is at write time.
|
|
6
7
|
*
|
|
@@ -19,6 +20,7 @@ const workSchema = z.object({
|
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
const metadataSchema = z.object({ openchamber: z.object({ work: workSchema }) });
|
|
23
|
+
const reviewOfferSchema = z.object({ openchamber: z.object({ reviewOffer: z.object({ at: z.number() }) }) });
|
|
22
24
|
|
|
23
25
|
/** The session's work record, or null when it has none (or a malformed one). */
|
|
24
26
|
export const readWork = (metadata) => metadataSchema.safeParse(metadata).data?.openchamber.work ?? null;
|
|
@@ -40,9 +42,13 @@ export const suggestDonePatch = (metadata, { now }) => {
|
|
|
40
42
|
return patchOf({ suggestDoneAt: now });
|
|
41
43
|
};
|
|
42
44
|
|
|
45
|
+
/** The turn that just ended handed over changes worth a look; any session. */
|
|
46
|
+
export const offerReviewPatch = (_metadata, { now }) => ({ openchamber: { reviewOffer: { at: now } } });
|
|
47
|
+
|
|
43
48
|
/** A new turn started: whatever Jev concluded about the last one no longer holds. */
|
|
44
49
|
export const clearSuggestionPatch = (metadata) => {
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
50
|
+
const patch = {};
|
|
51
|
+
if (readWork(metadata)?.suggestDoneAt !== undefined) patch.work = { suggestDoneAt: null };
|
|
52
|
+
if (reviewOfferSchema.safeParse(metadata).success) patch.reviewOffer = null;
|
|
53
|
+
return Object.keys(patch).length > 0 ? { openchamber: patch } : null;
|
|
48
54
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { clearSuggestionPatch, openByJevPatch, readWork, suggestDonePatch } from './state.js';
|
|
2
|
+
import { clearSuggestionPatch, openByJevPatch, readWork, offerReviewPatch, suggestDonePatch } from './state.js';
|
|
3
3
|
|
|
4
4
|
const withWork = (work) => ({ openchamber: { goal: { id: 'g' }, work } });
|
|
5
5
|
|
|
@@ -31,4 +31,11 @@ describe('session work state', () => {
|
|
|
31
31
|
expect(clearSuggestionPatch(withWork({ state: 'open', suggestDoneAt: 3 }))).toEqual({ openchamber: { work: { suggestDoneAt: null } } });
|
|
32
32
|
expect(clearSuggestionPatch(withWork({ state: 'open' }))).toBeNull();
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it('offers a review on any session, and a new turn retires both hints', () => {
|
|
36
|
+
expect(offerReviewPatch({}, { now: 3 })).toEqual({ openchamber: { reviewOffer: { at: 3 } } });
|
|
37
|
+
expect(clearSuggestionPatch({ openchamber: { reviewOffer: { at: 3 } } })).toEqual({ openchamber: { reviewOffer: null } });
|
|
38
|
+
expect(clearSuggestionPatch({ openchamber: { work: { state: 'open', suggestDoneAt: 3 }, reviewOffer: { at: 3 } } }))
|
|
39
|
+
.toEqual({ openchamber: { work: { suggestDoneAt: null }, reviewOffer: null } });
|
|
40
|
+
});
|
|
34
41
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Shared Guards Documentation
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`packages/web/server/lib/shared/guards.js` holds the small value predicates and
|
|
6
|
+
coercions that several feature modules used to define privately. It is neutral:
|
|
7
|
+
it knows nothing about git, OpenCode, projects, or any other domain, and it
|
|
8
|
+
imports nothing. Consolidating the copies means a value can no longer be a
|
|
9
|
+
string on one route and not a string on another because two files spelled the
|
|
10
|
+
check differently.
|
|
11
|
+
|
|
12
|
+
Only the dominant, behavior-defining spelling of each guard belongs here.
|
|
13
|
+
Variants that answer differently for real inputs stay with their owner.
|
|
14
|
+
|
|
15
|
+
## Public exports
|
|
16
|
+
|
|
17
|
+
- `isString(value)`: `Object.prototype.toString.call(value) === '[object String]'`.
|
|
18
|
+
True for primitive strings and boxed `String` objects. This is wider than
|
|
19
|
+
`typeof value === 'string'`, which is why callers that only accept primitives
|
|
20
|
+
keep their own `typeof` check.
|
|
21
|
+
- `isPlainObject(value)`: true only for a plain `{}`-shaped object. Rejects
|
|
22
|
+
`null`, arrays, boxed primitives, `Date`, `Map`, class instances, functions,
|
|
23
|
+
and null-prototype objects (`Object.create(null)`).
|
|
24
|
+
- `isRecord(value)`: true for any non-null, non-array object. Accepts plain
|
|
25
|
+
objects, class instances, `Date`, `Map`, boxed primitives, and null-prototype
|
|
26
|
+
objects.
|
|
27
|
+
- Modules that call the predicate `isObjectRecord` import `isRecord` under that
|
|
28
|
+
local name; the shared module exposes one canonical export.
|
|
29
|
+
- `asNonEmptyString(value)`: trims a string and returns it, or returns `null`
|
|
30
|
+
when the input is not a string or is empty/whitespace after trimming. The
|
|
31
|
+
`null` answer (not `''`) keeps "absent" distinguishable from "present".
|
|
32
|
+
|
|
33
|
+
## Behavior matrix
|
|
34
|
+
|
|
35
|
+
| input | `isString` | `isPlainObject` | `isRecord` / `isObjectRecord` | `asNonEmptyString` |
|
|
36
|
+
|---|---|---|---|---|
|
|
37
|
+
| `'s'` | true | false | false | `'s'` |
|
|
38
|
+
| `''` | true | false | false | `null` |
|
|
39
|
+
| `' s '` | true | false | false | `'s'` |
|
|
40
|
+
| `' '` | true | false | false | `null` |
|
|
41
|
+
| `new String('s')` | true | false | true | `null` (not a primitive) |
|
|
42
|
+
| `42` / `NaN` | false | false | false | `null` |
|
|
43
|
+
| `null` / `undefined` | false | false | false | `null` |
|
|
44
|
+
| `{}` | false | true | true | `null` |
|
|
45
|
+
| `[]` | false | false | false | `null` |
|
|
46
|
+
| `new Foo()` (class) | false | false | true | `null` |
|
|
47
|
+
| `new Date()` | false | false | true | `null` |
|
|
48
|
+
| `new Map()` | false | false | true | `null` |
|
|
49
|
+
| `Object.create(null)` | false | false | true | `null` |
|
|
50
|
+
|
|
51
|
+
## What stays out
|
|
52
|
+
|
|
53
|
+
These look adjacent but are not the same predicate, so each keeps its private
|
|
54
|
+
copy:
|
|
55
|
+
|
|
56
|
+
- `terminal/runtime.js` and `terminal/shells.js` use `String(value) === value`,
|
|
57
|
+
which rejects boxed strings and throws on null-prototype objects.
|
|
58
|
+
- `guests/oauth.js`, `opencode/settings-files.js`, and `opencode/shared.js`
|
|
59
|
+
expose a loose `isPlainObject` that uses `typeof value === 'object'` and so
|
|
60
|
+
accepts values the strict predicate rejects. They keep their own name and
|
|
61
|
+
shape; only modules asking for `isRecord` were migrated.
|
|
62
|
+
- `source-control/mutation-executor.js` accepts null-prototype objects under an
|
|
63
|
+
`isPlainObject` name, and `github/routes.js` /
|
|
64
|
+
`github/repo/fork-detection.js` classify by `[object Object]`; all three are
|
|
65
|
+
divergent and stay put.
|
|
66
|
+
- `quota/utils/transformers.js` exports an array-accepting `asObject`; it is not
|
|
67
|
+
a guard for a record and is out of scope.
|
|
68
|
+
- `projects/project-config.js` owns its config normalization pipeline and keeps
|
|
69
|
+
it local.
|
|
70
|
+
|
|
71
|
+
## Re-exported public APIs
|
|
72
|
+
|
|
73
|
+
The modules below keep their exported names and signatures and now re-export
|
|
74
|
+
the shared functions, so existing importers are unaffected:
|
|
75
|
+
|
|
76
|
+
- `opencode/config-v2.js`: `isRecord`
|
|
77
|
+
- `linear/parse.js`: `isString`, `isPlainObject`
|
|
78
|
+
- `gitlab/validation.js`: `isString`, `isPlainObject`
|
|
79
|
+
- `quota/utils/transformers.js`: `asNonEmptyString`
|
|
80
|
+
|
|
81
|
+
## Notes for contributors
|
|
82
|
+
|
|
83
|
+
- Add a predicate here only when two or more feature modules need the exact
|
|
84
|
+
same behavior. If a caller needs a stricter or looser answer, keep the
|
|
85
|
+
variant local and name the difference in a comment.
|
|
86
|
+
- Do not add feature imports to this module. It must stay free of domain
|
|
87
|
+
dependencies so every runtime can safely depend on it.
|
|
88
|
+
- When a guard's answer changes, update the behavior matrix test beside this
|
|
89
|
+
file; the test is the contract.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutral value guards shared across the web server.
|
|
3
|
+
*
|
|
4
|
+
* These are intentionally dependency-free and feature-agnostic: they describe
|
|
5
|
+
* what a value *is*, never what it means. Feature modules that need one of
|
|
6
|
+
* these predicates should import it from here rather than defining a private
|
|
7
|
+
* copy, so the same input can never classify differently on two surfaces.
|
|
8
|
+
*
|
|
9
|
+
* Only the dominant spelling of each guard lives here. Variants that look
|
|
10
|
+
* similar but answer differently for real inputs stay with their owner:
|
|
11
|
+
* `terminal/runtime.js` uses `String(value) === value`, which rejects boxed
|
|
12
|
+
* strings and throws on null-prototype objects; `guests/oauth.js`,
|
|
13
|
+
* `opencode/settings-files.js`, and `opencode/shared.js` use a loose
|
|
14
|
+
* `typeof value === 'object'` object check that classifies differently from
|
|
15
|
+
* `isPlainObject`; `quota/utils/transformers.js` exports an array-accepting
|
|
16
|
+
* `asObject`. See DOCUMENTATION.md for the full matrix.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* True for primitive strings and `String` objects (`[object String]`).
|
|
21
|
+
* This is broader than `typeof value === 'string'`; callers that only want
|
|
22
|
+
* primitives must check `typeof` themselves.
|
|
23
|
+
*/
|
|
24
|
+
export const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* True only for a plain `{}`-shaped object: not null, not an array, not a
|
|
28
|
+
* boxed primitive, `Date`, `Map`, class instance, or null-prototype object.
|
|
29
|
+
* Use this when the value is decoded from JSON or written back to JSON and a
|
|
30
|
+
* non-plain object would be corrupted by spreading or key enumeration.
|
|
31
|
+
*/
|
|
32
|
+
export const isPlainObject = (value) => typeof value === 'object'
|
|
33
|
+
&& value !== null
|
|
34
|
+
&& !Array.isArray(value)
|
|
35
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* True for any non-null, non-array object: plain objects, class instances,
|
|
39
|
+
* `Date`, `Map`, boxed primitives, and null-prototype objects. Use this when a
|
|
40
|
+
* value only needs to be traversable as an object, not spread as JSON.
|
|
41
|
+
*/
|
|
42
|
+
export const isRecord = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Trim a string, returning `null` when the input is not a string or is empty
|
|
46
|
+
* after trimming. The `null` (not `''`) answer lets callers distinguish
|
|
47
|
+
* "absent" from "present".
|
|
48
|
+
*/
|
|
49
|
+
export const asNonEmptyString = (value) => {
|
|
50
|
+
if (typeof value !== 'string') return null;
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
return trimmed ? trimmed : null;
|
|
53
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
asNonEmptyString,
|
|
5
|
+
isPlainObject,
|
|
6
|
+
isRecord,
|
|
7
|
+
isString,
|
|
8
|
+
} from './guards.js';
|
|
9
|
+
|
|
10
|
+
class Example {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.value = 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const nullPrototype = Object.assign(Object.create(null), { value: 1 });
|
|
17
|
+
const boxedString = Object('s');
|
|
18
|
+
const label = (value) => {
|
|
19
|
+
try {
|
|
20
|
+
return String(value);
|
|
21
|
+
} catch {
|
|
22
|
+
return Object.prototype.toString.call(value);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('isString', () => {
|
|
27
|
+
it('accepts primitive and boxed strings, including the empty string', () => {
|
|
28
|
+
expect(isString('s')).toBe(true);
|
|
29
|
+
expect(isString('')).toBe(true);
|
|
30
|
+
expect(isString(' ')).toBe(true);
|
|
31
|
+
expect(isString(boxedString)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('rejects every non-string value', () => {
|
|
35
|
+
for (const value of [42, 0, NaN, true, null, undefined, [], {}, nullPrototype, new Example(), new Date(), new Map()]) {
|
|
36
|
+
expect(isString(value), `isString(${label(value)})`).toBe(false);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('isPlainObject', () => {
|
|
42
|
+
it('accepts only a plain object literal', () => {
|
|
43
|
+
expect(isPlainObject({})).toBe(true);
|
|
44
|
+
expect(isPlainObject({ a: 1 })).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('rejects arrays, primitives, and absent values', () => {
|
|
48
|
+
for (const value of [[], [1], 42, 0, 's', '', true, null, undefined]) {
|
|
49
|
+
expect(isPlainObject(value), `isPlainObject(${label(value)})`).toBe(false);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('rejects values that merely look object-shaped', () => {
|
|
54
|
+
expect(isPlainObject(boxedString)).toBe(false);
|
|
55
|
+
expect(isPlainObject(new Example())).toBe(false);
|
|
56
|
+
expect(isPlainObject(new Date())).toBe(false);
|
|
57
|
+
expect(isPlainObject(new Map())).toBe(false);
|
|
58
|
+
expect(isPlainObject(nullPrototype)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('isRecord', () => {
|
|
63
|
+
it('accept every non-null, non-array object', () => {
|
|
64
|
+
for (const value of [{}, { a: 1 }, boxedString, new Example(), new Date(), new Map(), nullPrototype]) {
|
|
65
|
+
expect(isRecord(value), `isRecord(${label(value)})`).toBe(true);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reject arrays, primitives, and absent values', () => {
|
|
70
|
+
for (const value of [[], [1], 42, 0, NaN, 's', '', true, null, undefined]) {
|
|
71
|
+
expect(isRecord(value), `isRecord(${label(value)})`).toBe(false);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('asNonEmptyString', () => {
|
|
77
|
+
it('trims a string and returns the trimmed value', () => {
|
|
78
|
+
expect(asNonEmptyString('s')).toBe('s');
|
|
79
|
+
expect(asNonEmptyString(' s ')).toBe('s');
|
|
80
|
+
expect(asNonEmptyString('\n\ttab\t\n')).toBe('tab');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns null for empty, whitespace-only, and non-string values', () => {
|
|
84
|
+
for (const value of ['', ' ', '\n\t', 42, 0, NaN, true, null, undefined, {}, [], boxedString]) {
|
|
85
|
+
expect(asNonEmptyString(value), `asNonEmptyString(${label(value)})`).toBeNull();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('re-exported public APIs', () => {
|
|
91
|
+
it('linear/parse.js re-exports the shared predicates', async () => {
|
|
92
|
+
const parse = await import('../linear/parse.js');
|
|
93
|
+
expect(parse.isString).toBe(isString);
|
|
94
|
+
expect(parse.isPlainObject).toBe(isPlainObject);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('gitlab/validation.js re-exports the shared predicates', async () => {
|
|
98
|
+
const validation = await import('../gitlab/validation.js');
|
|
99
|
+
expect(validation.isString).toBe(isString);
|
|
100
|
+
expect(validation.isPlainObject).toBe(isPlainObject);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('opencode/config-v2.js re-exports the shared isRecord', async () => {
|
|
104
|
+
const configV2 = await import('../opencode/config-v2.js');
|
|
105
|
+
expect(configV2.isRecord).toBe(isRecord);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('quota/utils/transformers.js re-exports the shared asNonEmptyString', async () => {
|
|
109
|
+
const transformers = await import('../quota/utils/transformers.js');
|
|
110
|
+
expect(transformers.asNonEmptyString).toBe(asNonEmptyString);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -38,17 +38,17 @@ The following functions are exported and used by the web server:
|
|
|
38
38
|
- `parseSkillRepoSource(source, { subpath })`: Parse git repository source string into structured object with SSH/HTTPS clone URLs, normalized repo, and effective subpath. Supports SSH URLs, HTTPS URLs, and shorthand `owner/repo[/subpath]` format.
|
|
39
39
|
|
|
40
40
|
### Git Repository Scanning (`scan.js`)
|
|
41
|
-
- `scanSkillsRepository({ source, subpath, defaultSubpath, identity })`: Scan
|
|
41
|
+
- `scanSkillsRepository({ source, subpath, defaultSubpath, identity, credentialResolver })`: Scan a Git repository for skills by cloning it and reading its `SKILL.md` files. Account identities use their connected HTTPS credential, SSH identities use their managed key, anonymous identities use credential-free HTTPS, and no identity (labelled "This machine" in the catalog dialog) uses the server's own Git configuration and credential helpers.
|
|
42
42
|
|
|
43
43
|
### Git Repository Installation (`install.js`)
|
|
44
|
-
- `installSkillsFromRepository({ source, subpath, defaultSubpath, identity, scope, targetSource, workingDirectory, userSkillDir, selections, conflictPolicy, conflictDecisions })`: Install skills from
|
|
44
|
+
- `installSkillsFromRepository({ source, subpath, defaultSubpath, identity, credentialResolver, scope, targetSource, workingDirectory, userSkillDir, selections, conflictPolicy, conflictDecisions })`: Install selected skills from a Git repository. It uses the same identity transports as scanning and supports user/project scopes, opencode/agents targets, conflict resolution (prompt/skipAll/overwriteAll), and sparse checkout.
|
|
45
45
|
|
|
46
46
|
## Internal Helpers
|
|
47
47
|
|
|
48
48
|
The following functions are internal helpers used by exported functions:
|
|
49
49
|
|
|
50
50
|
### Git Helpers (`git.js`)
|
|
51
|
-
- `runGit(args, options)`: Execute
|
|
51
|
+
- `runGit(args, options)`: Execute a Git command with optional managed identity, credential resolver, timeout, and max buffer. HTTPS tokens go through the one-use local credential broker and never enter Git's arguments. Returns `{ ok, stdout, stderr, message, code, signal }`.
|
|
52
52
|
- `looksLikeAuthError(message)`: Detect if error message indicates authentication failure (permission denied, publickey, etc.).
|
|
53
53
|
- `assertGitAvailable()`: Check if git is available in PATH.
|
|
54
54
|
|
|
@@ -62,7 +62,7 @@ The following functions are internal helpers used by exported functions:
|
|
|
62
62
|
- `normalizeUserSkillDir(userSkillDir)`: Normalize the user skill directory path (handles the legacy `skill` directory in the XDG config location, or `~/.config/opencode/skill` when XDG is unset, by selecting the plural `skills` directory when appropriate).
|
|
63
63
|
|
|
64
64
|
### Git Clone Helpers (`install.js`, `scan.js`)
|
|
65
|
-
- `cloneRepo({ cloneUrl, identity, tempDir })`: Clone git repository with preferred partial clone (`--filter=blob:none`) and fallback. Uses non-interactive mode.
|
|
65
|
+
- `cloneRepo({ cloneUrl, identity, credentialResolver, tempDir, ref })`: Clone git repository with preferred partial clone (`--filter=blob:none`) and fallback, on `ref` when the source names one. Uses non-interactive mode.
|
|
66
66
|
|
|
67
67
|
### SKILL.md Parsing (`scan.js`)
|
|
68
68
|
- `parseSkillMd(content)`: Parse YAML frontmatter from SKILL.md content. Returns `{ ok, frontmatter, warnings }`.
|
|
@@ -111,11 +111,14 @@ The following functions are internal helpers used by exported functions:
|
|
|
111
111
|
- Skill names are derived from directory basenames for git repos.
|
|
112
112
|
- Invalid names result in non-installable skills with appropriate warnings.
|
|
113
113
|
|
|
114
|
-
### Git
|
|
115
|
-
- Use sparse checkout to minimize
|
|
114
|
+
### Git cloning strategy
|
|
115
|
+
- Use sparse checkout to minimize checkout size: `sparse-checkout init`, `sparse-checkout set`, `checkout HEAD`.
|
|
116
|
+
- Keep managed credentials scoped to the selected provider endpoint. Do not fall back to System Git after a selected identity fails, and refuse a selected identity that names no credential.
|
|
117
|
+
- Only commands that reach the remote resolve the identity: the clone, the `checkout`, and the `show` fallback in `scan.js`, which fetch blobs lazily from the partial clone. Local commands (`sparse-checkout`, `ls-files`, `ls-tree`) run without it.
|
|
118
|
+
- Managed HTTPS and anonymous runs point `HOME`, `USERPROFILE`, and `XDG_CONFIG_HOME` at the null device, so an ambient `~/.netrc` cannot sign in as another account. SSH runs keep `HOME` for `known_hosts`.
|
|
116
119
|
- Preferred clone uses `--depth=1 --filter=blob:none` for partial clone with fallback to `--depth=1`.
|
|
117
120
|
- Always use non-interactive mode (`GIT_TERMINAL_PROMPT=0`) to avoid hangs.
|
|
118
|
-
- SSH keys are injected
|
|
121
|
+
- Managed SSH keys are injected as `-c core.sshCommand=<wrapper>` through the shared SSH wrapper; the operation's key copy is removed after the run.
|
|
119
122
|
|
|
120
123
|
### Conflict Resolution
|
|
121
124
|
- Installation checks for existing skills before downloading/cloning.
|
|
@@ -133,7 +136,6 @@ The following functions are internal helpers used by exported functions:
|
|
|
133
136
|
### Security Considerations
|
|
134
137
|
- Path traversal protection in `copyDirectoryNoSymlinks`: resolves real paths and checks containment.
|
|
135
138
|
- Symlinks are explicitly rejected to prevent escape from skill directory.
|
|
136
|
-
- SSH key paths are trimmed but not escaped in `git.js` (assumes safe input from profiles).
|
|
137
139
|
- Temporary directories are cleaned up in `finally` blocks.
|
|
138
140
|
|
|
139
141
|
### Error Handling
|