@hienlh/ppm 0.22.8 → 0.22.10
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/CHANGELOG.md +10 -0
- package/CLAUDE.md +3 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +11 -1
- package/bun.lock +10 -10
- package/dist/web/assets/accounts-settings-section-BYz8j7Fq.js +1 -0
- package/dist/web/assets/accounts-settings-section-BYz8j7Fq.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-BYz8j7Fq.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-CtvWAxMW.js → ai-resource-editor-K7Q5d1jo.js} +1 -1
- package/dist/web/assets/ai-resource-editor-K7Q5d1jo.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-K7Q5d1jo.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-ChEBg9Be.js +1 -0
- package/dist/web/assets/ai-settings-section-CyWSTidP.js +1 -0
- package/dist/web/assets/ai-settings-section-CyWSTidP.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CyWSTidP.js.gz +0 -0
- package/dist/web/assets/{appearance-settings-section-DRWpbQqj.js → appearance-settings-section-DH0DxaKv.js} +1 -1
- package/dist/web/assets/appearance-settings-section-DH0DxaKv.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-DH0DxaKv.js.gz +0 -0
- package/dist/web/assets/{branch-review-tab-B3BMVCdK.js → branch-review-tab-BjPYFJ7n.js} +1 -1
- package/dist/web/assets/branch-review-tab-BjPYFJ7n.js.br +0 -0
- package/dist/web/assets/branch-review-tab-BjPYFJ7n.js.gz +0 -0
- package/dist/web/assets/chat-tab-BFrvTu8k.js +15 -0
- package/dist/web/assets/chat-tab-BFrvTu8k.js.br +0 -0
- package/dist/web/assets/chat-tab-BFrvTu8k.js.gz +0 -0
- package/dist/web/assets/code-editor-CTTLNDyE.js +7 -0
- package/dist/web/assets/code-editor-CTTLNDyE.js.br +0 -0
- package/dist/web/assets/code-editor-CTTLNDyE.js.gz +0 -0
- package/dist/web/assets/{codex-daily-guard-DjLeZ_hE.js → codex-daily-guard-PsB0g8B0.js} +1 -1
- package/dist/web/assets/codex-daily-guard-PsB0g8B0.js.br +0 -0
- package/dist/web/assets/codex-daily-guard-PsB0g8B0.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-Dx8MnO6j.js → conflict-editor-BoLsr5fL.js} +1 -1
- package/dist/web/assets/conflict-editor-BoLsr5fL.js.br +0 -0
- package/dist/web/assets/conflict-editor-BoLsr5fL.js.gz +0 -0
- package/dist/web/assets/{csv-preview-U-Fy9KPT.js → csv-preview-C-7PUd6W.js} +1 -1
- package/dist/web/assets/csv-preview-C-7PUd6W.js.br +0 -0
- package/dist/web/assets/csv-preview-C-7PUd6W.js.gz +0 -0
- package/dist/web/assets/database-viewer-BJgZ-0s2.js +1 -0
- package/dist/web/assets/database-viewer-BJgZ-0s2.js.br +0 -0
- package/dist/web/assets/database-viewer-BJgZ-0s2.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-Bv2J64t8.js → diff-viewer-i0_wCJL2.js} +1 -1
- package/dist/web/assets/diff-viewer-i0_wCJL2.js.br +0 -0
- package/dist/web/assets/diff-viewer-i0_wCJL2.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-C1J6iorq.js → editor-language-service-DDhZjZqB.js} +1 -1
- package/dist/web/assets/editor-language-service-DDhZjZqB.js.br +0 -0
- package/dist/web/assets/editor-language-service-DDhZjZqB.js.gz +0 -0
- package/dist/web/assets/explorer-body-DeIIj3uF.js +2 -0
- package/dist/web/assets/explorer-body-DeIIj3uF.js.br +0 -0
- package/dist/web/assets/explorer-body-DeIIj3uF.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-CZIwX6cE.js → explorer-window-content-D0i22zgM.js} +1 -1
- package/dist/web/assets/explorer-window-content-D0i22zgM.js.br +0 -0
- package/dist/web/assets/explorer-window-content-D0i22zgM.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-DGSKz5iJ.js → extension-manager-section-D8TSwRG8.js} +1 -1
- package/dist/web/assets/extension-manager-section-D8TSwRG8.js.br +0 -0
- package/dist/web/assets/extension-manager-section-D8TSwRG8.js.gz +0 -0
- package/dist/web/assets/{extension-webview-FuuXP0Aw.js → extension-webview-CfzJpLQR.js} +1 -1
- package/dist/web/assets/extension-webview-CfzJpLQR.js.br +0 -0
- package/dist/web/assets/extension-webview-CfzJpLQR.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-CGd2yfzl.js → files-settings-section-Ne10pnQU.js} +1 -1
- package/dist/web/assets/files-settings-section-Ne10pnQU.js.br +0 -0
- package/dist/web/assets/files-settings-section-Ne10pnQU.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-CcG10nJq.js → general-settings-section-fbSqpiKY.js} +1 -1
- package/dist/web/assets/general-settings-section-fbSqpiKY.js.br +0 -0
- package/dist/web/assets/general-settings-section-fbSqpiKY.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-B-aSlSlu.js → git-log-panel-9mRsulRQ.js} +1 -1
- package/dist/web/assets/git-log-panel-9mRsulRQ.js.br +0 -0
- package/dist/web/assets/git-log-panel-9mRsulRQ.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-usB_4gTg.js → glide-data-grid-OhxZFlYg.js} +3 -3
- package/dist/web/assets/glide-data-grid-OhxZFlYg.js.br +0 -0
- package/dist/web/assets/glide-data-grid-OhxZFlYg.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-4DdMvOkG.js +1 -0
- package/dist/web/assets/group-chat-tab-4DdMvOkG.js.br +0 -0
- package/dist/web/assets/group-chat-tab-4DdMvOkG.js.gz +0 -0
- package/dist/web/assets/index-C3v3QRVR.js +52 -0
- package/dist/web/assets/index-C3v3QRVR.js.br +0 -0
- package/dist/web/assets/index-C3v3QRVR.js.gz +0 -0
- package/dist/web/assets/index-aXJy_SvC.css +1 -0
- package/dist/web/assets/index-aXJy_SvC.css.br +0 -0
- package/dist/web/assets/index-aXJy_SvC.css.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-cDMBzIR7.js → jira-watcher-section-PYPh3xGU.js} +1 -1
- package/dist/web/assets/jira-watcher-section-PYPh3xGU.js.br +0 -0
- package/dist/web/assets/jira-watcher-section-PYPh3xGU.js.gz +0 -0
- package/dist/web/assets/keybindings-store-DhAtn4Pd.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-CIcdYToP.js → keyboard-shortcuts-section-CM3pNiBR.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-CM3pNiBR.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-CM3pNiBR.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-BzX5WysW.js → markdown-renderer-Df1vynCr.js} +1 -1
- package/dist/web/assets/{markdown-renderer-KTNg80_A.js → markdown-renderer-DqNZUt1S.js} +3 -3
- package/dist/web/assets/markdown-renderer-DqNZUt1S.js.br +0 -0
- package/dist/web/assets/{markdown-renderer-KTNg80_A.js.gz → markdown-renderer-DqNZUt1S.js.gz} +0 -0
- package/dist/web/assets/{mobile-explorer-sheet-cnwXe-Uz.js → mobile-explorer-sheet-BSuzC_08.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BSuzC_08.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BSuzC_08.js.gz +0 -0
- package/dist/web/assets/notification-store-DbKwLWwl.js +1 -0
- package/dist/web/assets/{ppmbot-settings-section-CDp69sHV.js → ppmbot-settings-section-CY_yAGFM.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-CY_yAGFM.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-CY_yAGFM.js.gz +0 -0
- package/dist/web/assets/{problems-panel-BiE3c-31.js → problems-panel-CJNasbnt.js} +1 -1
- package/dist/web/assets/problems-panel-CJNasbnt.js.br +0 -0
- package/dist/web/assets/problems-panel-CJNasbnt.js.gz +0 -0
- package/dist/web/assets/{process-table-DcEq9HCl.js → process-table-1CN3rBVp.js} +1 -1
- package/dist/web/assets/process-table-1CN3rBVp.js.br +0 -0
- package/dist/web/assets/process-table-1CN3rBVp.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-P_ULFRpZ.js → proxy-settings-section-CqcvneFB.js} +1 -1
- package/dist/web/assets/proxy-settings-section-CqcvneFB.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-CqcvneFB.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-clipboard-notice-COjdmADR.js → remote-desktop-clipboard-notice-BysFGa_4.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-BysFGa_4.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-BysFGa_4.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-sheet-lCU5l2w4.js → remote-desktop-mobile-sheet-DLW_0HDz.js} +2 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-DLW_0HDz.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-DLW_0HDz.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-view-Bo-wgZPC.js → remote-desktop-mobile-view-Df6SfRtI.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-view-Df6SfRtI.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-Df6SfRtI.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-Cqz8kqWG.js → remote-desktop-readiness-gate-LugXhLJ3.js} +1 -1
- package/dist/web/assets/remote-desktop-readiness-gate-LugXhLJ3.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-LugXhLJ3.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-k-pIAIDq.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-k-pIAIDq.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-k-pIAIDq.js.gz +0 -0
- package/dist/web/assets/{schedules-settings-section-DHs84DvG.js → schedules-settings-section-ByCm7IOg.js} +1 -1
- package/dist/web/assets/schedules-settings-section-ByCm7IOg.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-ByCm7IOg.js.gz +0 -0
- package/dist/web/assets/{settings-body-CLt6iutX.js → settings-body-Dwt_rUSa.js} +2 -2
- package/dist/web/assets/settings-body-Dwt_rUSa.js.br +0 -0
- package/dist/web/assets/settings-body-Dwt_rUSa.js.gz +0 -0
- package/dist/web/assets/settings-store-m6qOXoy4.js +2 -0
- package/dist/web/assets/settings-store-m6qOXoy4.js.br +0 -0
- package/dist/web/assets/settings-store-m6qOXoy4.js.gz +0 -0
- package/dist/web/assets/{settings-tab-DjOxh-vW.js → settings-tab-Dvj9UfzL.js} +1 -1
- package/dist/web/assets/{settings-window-content-BxzSerga.js → settings-window-content-Di-eONmj.js} +1 -1
- package/dist/web/assets/{sql-query-editor-4Okd-7ss.js → sql-query-editor-DgwwJxwJ.js} +1 -1
- package/dist/web/assets/sql-query-editor-DgwwJxwJ.js.br +0 -0
- package/dist/web/assets/sql-query-editor-DgwwJxwJ.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-BqDUckIJ.js → sqlite-viewer-gxiJZ3kk.js} +1 -1
- package/dist/web/assets/sqlite-viewer-gxiJZ3kk.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-gxiJZ3kk.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-Ssmt-r5H.js → system-monitor-body-BJrVntEe.js} +2 -2
- package/dist/web/assets/system-monitor-body-BJrVntEe.js.br +0 -0
- package/dist/web/assets/system-monitor-body-BJrVntEe.js.gz +0 -0
- package/dist/web/assets/{system-monitor-tab-DNe62hWL.js → system-monitor-tab-Uz9A5I07.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-C6vnW_pQ.js → system-monitor-window-content-9cKlCVN0.js} +1 -1
- package/dist/web/assets/{tab-host-window-content--BVyPR1f.js → tab-host-window-content-D3M-lRyv.js} +1 -1
- package/dist/web/assets/tab-host-window-content-D3M-lRyv.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-D3M-lRyv.js.gz +0 -0
- package/dist/web/assets/{team-member-sheet-CE2eHuFX.js → team-member-sheet-D66xVR6G.js} +2 -2
- package/dist/web/assets/team-member-sheet-D66xVR6G.js.br +0 -0
- package/dist/web/assets/team-member-sheet-D66xVR6G.js.gz +0 -0
- package/dist/web/assets/{team-member-window-content-B1sg0R4H.js → team-member-window-content-D6SxpWVr.js} +1 -1
- package/dist/web/assets/team-member-window-content-D6SxpWVr.js.br +0 -0
- package/dist/web/assets/team-member-window-content-D6SxpWVr.js.gz +0 -0
- package/dist/web/assets/{terminal-tab-BrdS4bAy.js → terminal-tab-DUfOyHxp.js} +3 -3
- package/dist/web/assets/terminal-tab-DUfOyHxp.js.br +0 -0
- package/dist/web/assets/terminal-tab-DUfOyHxp.js.gz +0 -0
- package/dist/web/assets/{tool-cards-DzYJVhjl.js → tool-cards-O9N-oJuB.js} +3 -3
- package/dist/web/assets/tool-cards-O9N-oJuB.js.br +0 -0
- package/dist/web/assets/tool-cards-O9N-oJuB.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-CKu-Sd-l.js → use-inline-blame-B40AsaN2.js} +1 -1
- package/dist/web/assets/use-inline-blame-B40AsaN2.js.br +0 -0
- package/dist/web/assets/use-inline-blame-B40AsaN2.js.gz +0 -0
- package/dist/web/assets/{use-lsp-BaG0NUXY.js → use-lsp-B30TcKvt.js} +1 -1
- package/dist/web/assets/use-lsp-B30TcKvt.js.br +0 -0
- package/dist/web/assets/use-lsp-B30TcKvt.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-DvWcXiCH.js → use-monaco-theme-DGi9_j5M.js} +1 -1
- package/dist/web/assets/{use-websocket-Dj0e6N6t.js → use-websocket-DdGdvJc5.js} +1 -1
- package/dist/web/assets/video-preview-RS5jKlrC.js +1 -0
- package/dist/web/assets/video-preview-RS5jKlrC.js.br +0 -0
- package/dist/web/assets/video-preview-RS5jKlrC.js.gz +0 -0
- package/dist/web/index.html +3 -3
- package/dist/web/index.html.br +0 -0
- package/dist/web/index.html.gz +0 -0
- package/dist/web/sw.js +1 -1
- package/dist/web/sw.js.br +0 -0
- package/dist/web/sw.js.gz +0 -0
- package/docs/codebase-summary.md +1 -1
- package/docs/project-roadmap.md +1 -1
- package/package.json +2 -2
- package/src/providers/claude-agent-sdk.ts +87 -8
- package/src/server/index.ts +9 -0
- package/src/server/routes/mcp-auth.ts +117 -0
- package/src/server/routes/settings.ts +2 -0
- package/src/server/ws/chat-mcp-sign-in-sync.ts +58 -0
- package/src/server/ws/chat.ts +35 -1
- package/src/services/mcp-oauth/mcp-control-query.ts +58 -0
- package/src/services/mcp-oauth/mcp-oauth-flows.ts +271 -0
- package/src/services/mcp-oauth/mcp-oauth-redirect.ts +68 -0
- package/src/services/mcp-oauth/mcp-status-probe.ts +76 -0
- package/src/web/components/ai-resources/ai-resources-panel.tsx +4 -0
- package/src/web/components/chat/chat-tab.tsx +5 -0
- package/src/web/components/mcp-auth/claude-ai-connector.ts +11 -0
- package/src/web/components/mcp-auth/mcp-server-name.ts +10 -0
- package/src/web/components/mcp-auth/mcp-sign-in-bar.tsx +74 -0
- package/src/web/components/mcp-auth/mcp-sign-in-dialog.tsx +196 -0
- package/src/web/components/mcp-auth/mcp-sign-in-list.tsx +110 -0
- package/src/web/components/mcp-auth/use-mcp-sign-in-flow.ts +120 -0
- package/src/web/components/settings/ai-settings-section.tsx +11 -0
- package/src/web/hooks/use-chat.ts +14 -0
- package/src/web/lib/api-mcp-auth.ts +54 -0
- package/src/web/stores/settings-store.ts +34 -0
- package/dist/web/assets/accounts-settings-section-Dk9qWWTy.js +0 -1
- package/dist/web/assets/accounts-settings-section-Dk9qWWTy.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-Dk9qWWTy.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-CtvWAxMW.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-CtvWAxMW.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-BxN-zFfm.js +0 -1
- package/dist/web/assets/ai-settings-section-CfI9nNSa.js +0 -1
- package/dist/web/assets/ai-settings-section-CfI9nNSa.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CfI9nNSa.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-DRWpbQqj.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-DRWpbQqj.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-B3BMVCdK.js.br +0 -0
- package/dist/web/assets/branch-review-tab-B3BMVCdK.js.gz +0 -0
- package/dist/web/assets/chat-tab-B0g-TzDa.js +0 -15
- package/dist/web/assets/chat-tab-B0g-TzDa.js.br +0 -0
- package/dist/web/assets/chat-tab-B0g-TzDa.js.gz +0 -0
- package/dist/web/assets/code-editor-DXra_Rnt.js +0 -7
- package/dist/web/assets/code-editor-DXra_Rnt.js.br +0 -0
- package/dist/web/assets/code-editor-DXra_Rnt.js.gz +0 -0
- package/dist/web/assets/codex-daily-guard-DjLeZ_hE.js.br +0 -0
- package/dist/web/assets/codex-daily-guard-DjLeZ_hE.js.gz +0 -0
- package/dist/web/assets/conflict-editor-Dx8MnO6j.js.br +0 -0
- package/dist/web/assets/conflict-editor-Dx8MnO6j.js.gz +0 -0
- package/dist/web/assets/csv-preview-U-Fy9KPT.js.br +0 -0
- package/dist/web/assets/csv-preview-U-Fy9KPT.js.gz +0 -0
- package/dist/web/assets/database-viewer-v5S-zzB6.js +0 -1
- package/dist/web/assets/database-viewer-v5S-zzB6.js.br +0 -0
- package/dist/web/assets/database-viewer-v5S-zzB6.js.gz +0 -0
- package/dist/web/assets/diff-viewer-Bv2J64t8.js.br +0 -0
- package/dist/web/assets/diff-viewer-Bv2J64t8.js.gz +0 -0
- package/dist/web/assets/editor-language-service-C1J6iorq.js.br +0 -0
- package/dist/web/assets/editor-language-service-C1J6iorq.js.gz +0 -0
- package/dist/web/assets/explorer-body-BpTOY2kH.js +0 -2
- package/dist/web/assets/explorer-body-BpTOY2kH.js.br +0 -0
- package/dist/web/assets/explorer-body-BpTOY2kH.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-CZIwX6cE.js.br +0 -0
- package/dist/web/assets/explorer-window-content-CZIwX6cE.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-DGSKz5iJ.js.br +0 -0
- package/dist/web/assets/extension-manager-section-DGSKz5iJ.js.gz +0 -0
- package/dist/web/assets/extension-webview-FuuXP0Aw.js.br +0 -0
- package/dist/web/assets/extension-webview-FuuXP0Aw.js.gz +0 -0
- package/dist/web/assets/files-settings-section-CGd2yfzl.js.br +0 -0
- package/dist/web/assets/files-settings-section-CGd2yfzl.js.gz +0 -0
- package/dist/web/assets/general-settings-section-CcG10nJq.js.br +0 -0
- package/dist/web/assets/general-settings-section-CcG10nJq.js.gz +0 -0
- package/dist/web/assets/git-log-panel-B-aSlSlu.js.br +0 -0
- package/dist/web/assets/git-log-panel-B-aSlSlu.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-usB_4gTg.js.br +0 -0
- package/dist/web/assets/glide-data-grid-usB_4gTg.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-BAMi_dhb.js +0 -1
- package/dist/web/assets/group-chat-tab-BAMi_dhb.js.br +0 -0
- package/dist/web/assets/group-chat-tab-BAMi_dhb.js.gz +0 -0
- package/dist/web/assets/index-CoTvom0K.js +0 -52
- package/dist/web/assets/index-CoTvom0K.js.br +0 -0
- package/dist/web/assets/index-CoTvom0K.js.gz +0 -0
- package/dist/web/assets/index-DLn9-Lrb.css +0 -1
- package/dist/web/assets/index-DLn9-Lrb.css.br +0 -0
- package/dist/web/assets/index-DLn9-Lrb.css.gz +0 -0
- package/dist/web/assets/jira-watcher-section-cDMBzIR7.js.br +0 -3
- package/dist/web/assets/jira-watcher-section-cDMBzIR7.js.gz +0 -0
- package/dist/web/assets/keybindings-store-D7nwnInl.js +0 -1
- package/dist/web/assets/keyboard-shortcuts-section-CIcdYToP.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-CIcdYToP.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-KTNg80_A.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-cnwXe-Uz.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-cnwXe-Uz.js.gz +0 -0
- package/dist/web/assets/notification-store-Bayjh6Js.js +0 -1
- package/dist/web/assets/ppmbot-settings-section-CDp69sHV.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-CDp69sHV.js.gz +0 -0
- package/dist/web/assets/problems-panel-BiE3c-31.js.br +0 -0
- package/dist/web/assets/problems-panel-BiE3c-31.js.gz +0 -0
- package/dist/web/assets/process-table-DcEq9HCl.js.br +0 -0
- package/dist/web/assets/process-table-DcEq9HCl.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-P_ULFRpZ.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-P_ULFRpZ.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-COjdmADR.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-COjdmADR.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-lCU5l2w4.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-lCU5l2w4.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-Bo-wgZPC.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-Bo-wgZPC.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-Cqz8kqWG.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-Cqz8kqWG.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-R-aRW07B.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-R-aRW07B.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-R-aRW07B.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-DHs84DvG.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-DHs84DvG.js.gz +0 -0
- package/dist/web/assets/settings-body-CLt6iutX.js.br +0 -0
- package/dist/web/assets/settings-body-CLt6iutX.js.gz +0 -0
- package/dist/web/assets/settings-store-tWrnlyEW.js +0 -2
- package/dist/web/assets/settings-store-tWrnlyEW.js.br +0 -0
- package/dist/web/assets/settings-store-tWrnlyEW.js.gz +0 -0
- package/dist/web/assets/sql-query-editor-4Okd-7ss.js.br +0 -0
- package/dist/web/assets/sql-query-editor-4Okd-7ss.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-BqDUckIJ.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-BqDUckIJ.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-Ssmt-r5H.js.br +0 -0
- package/dist/web/assets/system-monitor-body-Ssmt-r5H.js.gz +0 -0
- package/dist/web/assets/tab-host-window-content--BVyPR1f.js.br +0 -0
- package/dist/web/assets/tab-host-window-content--BVyPR1f.js.gz +0 -0
- package/dist/web/assets/team-member-sheet-CE2eHuFX.js.br +0 -0
- package/dist/web/assets/team-member-sheet-CE2eHuFX.js.gz +0 -0
- package/dist/web/assets/team-member-window-content-B1sg0R4H.js.br +0 -0
- package/dist/web/assets/team-member-window-content-B1sg0R4H.js.gz +0 -0
- package/dist/web/assets/terminal-tab-BrdS4bAy.js.br +0 -0
- package/dist/web/assets/terminal-tab-BrdS4bAy.js.gz +0 -0
- package/dist/web/assets/tool-cards-DzYJVhjl.js.br +0 -0
- package/dist/web/assets/tool-cards-DzYJVhjl.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-CKu-Sd-l.js.br +0 -0
- package/dist/web/assets/use-inline-blame-CKu-Sd-l.js.gz +0 -0
- package/dist/web/assets/use-lsp-BaG0NUXY.js.br +0 -0
- package/dist/web/assets/use-lsp-BaG0NUXY.js.gz +0 -0
- package/dist/web/assets/video-preview-ZyRR49Lr.js +0 -1
- package/dist/web/assets/video-preview-ZyRR49Lr.js.br +0 -0
- package/dist/web/assets/video-preview-ZyRR49Lr.js.gz +0 -0
|
@@ -1072,14 +1072,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1072
1072
|
}
|
|
1073
1073
|
console.log(`[sdk] query: session=${sessionId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${effectiveCwd} platform=${process.platform} accountMode=${!!account} permissionMode=${permissionMode} isBypass=${isBypass}`);
|
|
1074
1074
|
|
|
1075
|
-
|
|
1076
|
-
// merged with servers inherited from Claude Code's ~/.claude.json for this project.
|
|
1077
|
-
// PPM DB entries override inherited ones on name conflict.
|
|
1078
|
-
const ownServers = mcpConfigService.list();
|
|
1079
|
-
const inheritedServers = providerConfig.inherit_claude_mcp !== false
|
|
1080
|
-
? listInheritedClaudeMcpServers(effectiveCwd)
|
|
1081
|
-
: {};
|
|
1082
|
-
const mcpServers = { ...inheritedServers, ...ownServers };
|
|
1075
|
+
const mcpServers = this.resolveMcpServers(effectiveCwd);
|
|
1083
1076
|
const hasMcp = Object.keys(mcpServers).length > 0;
|
|
1084
1077
|
|
|
1085
1078
|
// Buffer subprocess stderr for crash diagnostics + log in real-time
|
|
@@ -1334,6 +1327,15 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1334
1327
|
} else {
|
|
1335
1328
|
console.log(`[sdk] session=${sessionId} init: sdk_session_id=${sdkSid}`);
|
|
1336
1329
|
}
|
|
1330
|
+
// Carry each MCP server's connection state so the host can offer a sign-in
|
|
1331
|
+
// for the ones reporting `needs-auth`.
|
|
1332
|
+
const initServers = (msg as any).mcp_servers;
|
|
1333
|
+
yield {
|
|
1334
|
+
type: "system" as any,
|
|
1335
|
+
subtype,
|
|
1336
|
+
...(Array.isArray(initServers) && { mcpServers: initServers }),
|
|
1337
|
+
} as any;
|
|
1338
|
+
continue;
|
|
1337
1339
|
}
|
|
1338
1340
|
|
|
1339
1341
|
// Detect compacting status
|
|
@@ -2109,6 +2111,83 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
2109
2111
|
}
|
|
2110
2112
|
|
|
2111
2113
|
|
|
2114
|
+
/**
|
|
2115
|
+
* MCP servers for a query in `cwd`: PPM's own (read fresh — the user may add or remove
|
|
2116
|
+
* one between chats) over those inherited from Claude Code's ~/.claude.json for this
|
|
2117
|
+
* project. PPM entries win on a name conflict.
|
|
2118
|
+
*/
|
|
2119
|
+
private resolveMcpServers(cwd: string): Record<string, unknown> {
|
|
2120
|
+
const inherited = this.getProviderConfig().inherit_claude_mcp !== false
|
|
2121
|
+
? listInheritedClaudeMcpServers(cwd)
|
|
2122
|
+
: {};
|
|
2123
|
+
return { ...inherited, ...mcpConfigService.list() };
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
/**
|
|
2127
|
+
* A subprocess that never receives a prompt, for control requests only — MCP status
|
|
2128
|
+
* and MCP sign-in. It is configured like a chat query in `cwd` (same CLI, settings
|
|
2129
|
+
* sources and MCP servers), so it sees exactly the servers a chat there would, and it
|
|
2130
|
+
* never calls the model. The caller owns it and must `close()` it: an OAuth flow's
|
|
2131
|
+
* localhost callback listener lives inside this process.
|
|
2132
|
+
*/
|
|
2133
|
+
openMcpControlQuery(cwd: string): { query: any; close: () => void } {
|
|
2134
|
+
const providerConfig = this.getProviderConfig();
|
|
2135
|
+
const cliExecutablePath = resolveCliExecutablePath(
|
|
2136
|
+
(providerConfig as { cli_command?: string }).cli_command,
|
|
2137
|
+
);
|
|
2138
|
+
const mcpServers = this.resolveMcpServers(cwd);
|
|
2139
|
+
const { generator, controller } = createMessageChannel();
|
|
2140
|
+
const q = query({
|
|
2141
|
+
prompt: generator,
|
|
2142
|
+
options: {
|
|
2143
|
+
...(needsNodeInterpreter(process.platform, cliExecutablePath) && { executable: "node" }),
|
|
2144
|
+
...(cliExecutablePath && { pathToClaudeCodeExecutable: cliExecutablePath }),
|
|
2145
|
+
cwd,
|
|
2146
|
+
settingSources: ["user", "project"],
|
|
2147
|
+
// No account: MCP OAuth tokens belong to the CLI's own credential store, the same
|
|
2148
|
+
// one an interactive `claude` writes, and no model call is ever made here.
|
|
2149
|
+
env: this.buildQueryEnv(cwd, null),
|
|
2150
|
+
settings: { permissions: { allow: [], deny: [] } },
|
|
2151
|
+
...(Object.keys(mcpServers).length > 0 && { mcpServers }),
|
|
2152
|
+
stderr: (chunk: string) => {
|
|
2153
|
+
const line = chunk.trim();
|
|
2154
|
+
if (line) console.log(`[mcp-control] stderr: ${line.slice(0, 300)}`);
|
|
2155
|
+
},
|
|
2156
|
+
} as any,
|
|
2157
|
+
});
|
|
2158
|
+
let closed = false;
|
|
2159
|
+
return {
|
|
2160
|
+
query: q,
|
|
2161
|
+
close: () => {
|
|
2162
|
+
if (closed) return;
|
|
2163
|
+
closed = true;
|
|
2164
|
+
controller.done();
|
|
2165
|
+
q.close();
|
|
2166
|
+
},
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* Reconnect one MCP server inside a live chat subprocess, so a sign-in finished
|
|
2172
|
+
* elsewhere shows up without restarting the conversation. Returns the server's status
|
|
2173
|
+
* afterwards, or null when the session has no live subprocess.
|
|
2174
|
+
*/
|
|
2175
|
+
async reconnectMcpServer(sessionId: string, serverName: string): Promise<string | null> {
|
|
2176
|
+
const ss = this.streamingSessions.get(sessionId);
|
|
2177
|
+
if (!ss) return null;
|
|
2178
|
+
try {
|
|
2179
|
+
await ss.query.reconnectMcpServer(serverName);
|
|
2180
|
+
} catch (e) {
|
|
2181
|
+
console.warn(`[sdk] session=${sessionId} reconnectMcpServer(${serverName}) failed: ${(e as Error).message}`);
|
|
2182
|
+
}
|
|
2183
|
+
try {
|
|
2184
|
+
const statuses: Array<{ name: string; status: string }> = await ss.query.mcpServerStatus();
|
|
2185
|
+
return statuses.find((s) => s.name === serverName)?.status ?? null;
|
|
2186
|
+
} catch {
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2112
2191
|
/** Abort and fully teardown the streaming session — user must resume to continue */
|
|
2113
2192
|
abortQuery(sessionId: string, source = "unknown"): void {
|
|
2114
2193
|
// Capture stack to identify caller during debugging intermittent abort bugs
|
package/src/server/index.ts
CHANGED
|
@@ -180,6 +180,12 @@ app.route("/proxy", proxyRoutes);
|
|
|
180
180
|
// HTML assets authenticate with bounded preview capabilities, never the PPM session token.
|
|
181
181
|
app.route("/api/html-preview/content", htmlPreviewRoutes.content);
|
|
182
182
|
|
|
183
|
+
// MCP sign-in redirect: reached by a browser navigation from the authorization server,
|
|
184
|
+
// which carries no PPM token. It only completes a flow an authenticated user started.
|
|
185
|
+
import { mcpAuthRoutes, mcpAuthCallbackHandler } from "./routes/mcp-auth.ts";
|
|
186
|
+
import { mcpOAuthFlows } from "../services/mcp-oauth/mcp-oauth-flows.ts";
|
|
187
|
+
app.get("/api/mcp-auth/callback", mcpAuthCallbackHandler);
|
|
188
|
+
|
|
183
189
|
// Auth check endpoint (behind auth middleware)
|
|
184
190
|
app.use("/api/*", authMiddleware);
|
|
185
191
|
app.use("/api/*", gzipJson);
|
|
@@ -227,6 +233,7 @@ app.route("/api/loopback", loopbackRoutes);
|
|
|
227
233
|
// API routes
|
|
228
234
|
app.route("/api/settings", settingsRoutes);
|
|
229
235
|
app.route("/api/settings/mcp", mcpRoutes);
|
|
236
|
+
app.route("/api/mcp-auth", mcpAuthRoutes);
|
|
230
237
|
app.route("/api/settings/themes", settingsThemesRoutes);
|
|
231
238
|
app.route("/api/tunnel", tunnelRoutes);
|
|
232
239
|
import { namedTunnelRoutes } from "./routes/named-tunnel.ts";
|
|
@@ -1054,6 +1061,8 @@ if (process.argv.includes("__serve__")) {
|
|
|
1054
1061
|
// resident rust-analyzer holding a crate graph is too expensive to leave
|
|
1055
1062
|
// to chance. Synchronous because process.exit follows immediately.
|
|
1056
1063
|
try { lspManager.killAllSync(); } catch {}
|
|
1064
|
+
// An MCP sign-in holds a Claude subprocess until it settles.
|
|
1065
|
+
try { mcpOAuthFlows.disposeAll(); } catch {}
|
|
1057
1066
|
try { server.stop(true); } catch {}
|
|
1058
1067
|
process.exit(0);
|
|
1059
1068
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP sign-in (OAuth) for servers that report `needs-auth`.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is behind PPM's auth except `mcpAuthCallbackHandler`: the authorization
|
|
5
|
+
* server redirects the *browser* there by a top-level navigation, which carries no PPM
|
|
6
|
+
* token. It is safe to leave open because it can only complete a flow an authenticated
|
|
7
|
+
* user started — it matches the redirect's single-use OAuth `state` to a waiting flow, and
|
|
8
|
+
* the CLI checks that state again before exchanging the code.
|
|
9
|
+
*/
|
|
10
|
+
import { Hono, type Context } from "hono";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { ok, err } from "../../types/api.ts";
|
|
13
|
+
import { resolveProjectPath } from "../helpers/resolve-project.ts";
|
|
14
|
+
import { mcpOAuthFlows, McpAuthFlowNotFoundError, type McpOAuthFlows } from "../../services/mcp-oauth/mcp-oauth-flows.ts";
|
|
15
|
+
import { mcpStatusProbe } from "../../services/mcp-oauth/mcp-status-probe.ts";
|
|
16
|
+
import { callbackUrlFor, customRedirectUri } from "../../services/mcp-oauth/mcp-oauth-redirect.ts";
|
|
17
|
+
|
|
18
|
+
// A cached status would still say `needs-auth` right after a sign-in.
|
|
19
|
+
mcpOAuthFlows.onAuthorized(() => mcpStatusProbe.invalidate());
|
|
20
|
+
|
|
21
|
+
export const mcpAuthRoutes = new Hono();
|
|
22
|
+
|
|
23
|
+
/** Directory whose MCP configuration applies: the project's, or the home directory. */
|
|
24
|
+
function cwdFor(project: string | undefined): string {
|
|
25
|
+
return project ? resolveProjectPath(project) : homedir();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// GET /status?project=<name>&fresh=1 — every server's connection state
|
|
29
|
+
mcpAuthRoutes.get("/status", async (c) => {
|
|
30
|
+
let cwd: string;
|
|
31
|
+
try { cwd = cwdFor(c.req.query("project")); } catch (e) { return c.json(err((e as Error).message), 404); }
|
|
32
|
+
try {
|
|
33
|
+
const servers = await mcpStatusProbe.status(cwd, { fresh: c.req.query("fresh") === "1" });
|
|
34
|
+
return c.json(ok(servers));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return c.json(err(`Could not read MCP status: ${(e as Error).message}`), 500);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// POST /start { server, project?, origin? } — begin a sign-in, returns the flow
|
|
41
|
+
mcpAuthRoutes.post("/start", async (c) => {
|
|
42
|
+
const body = await c.req.json<{ server?: string; project?: string; origin?: string }>().catch(() => ({} as Record<string, undefined>));
|
|
43
|
+
if (!body.server || typeof body.server !== "string") return c.json(err("Missing required field: server"), 400);
|
|
44
|
+
let cwd: string;
|
|
45
|
+
try { cwd = cwdFor(body.project); } catch (e) { return c.json(err((e as Error).message), 404); }
|
|
46
|
+
const redirectUri = customRedirectUri(body.origin, [c.req.header("x-forwarded-host"), c.req.header("host")]);
|
|
47
|
+
try {
|
|
48
|
+
return c.json(ok(await mcpOAuthFlows.start(body.server, cwd, redirectUri)));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return c.json(err(`Could not start the sign-in: ${(e as Error).message}`), 500);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
mcpAuthRoutes.get("/flows/:id", (c) => {
|
|
55
|
+
const flow = mcpOAuthFlows.get(c.req.param("id"));
|
|
56
|
+
return flow ? c.json(ok(flow)) : c.json(err("This sign-in is no longer active. Start again."), 404);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// POST /flows/:id/callback { url } — the redirect URL the user pasted
|
|
60
|
+
mcpAuthRoutes.post("/flows/:id/callback", async (c) => {
|
|
61
|
+
const body = await c.req.json<{ url?: string }>().catch(() => ({} as { url?: string }));
|
|
62
|
+
if (!body.url || typeof body.url !== "string" || body.url.length > 16_384) {
|
|
63
|
+
return c.json(err("Paste the full address from the browser after signing in."), 400);
|
|
64
|
+
}
|
|
65
|
+
return flowAction(c, () => mcpOAuthFlows.submitCallback(c.req.param("id"), body.url!.trim()));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// POST /flows/:id/confirm — "I've granted access" for a claude.ai connector
|
|
69
|
+
mcpAuthRoutes.post("/flows/:id/confirm", (c) => flowAction(c, () => mcpOAuthFlows.confirm(c.req.param("id"))));
|
|
70
|
+
|
|
71
|
+
mcpAuthRoutes.delete("/flows/:id", (c) => {
|
|
72
|
+
const flow = mcpOAuthFlows.cancel(c.req.param("id"));
|
|
73
|
+
return flow ? c.json(ok(flow)) : c.json(err("Sign-in not found"), 404);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function flowAction(c: Context, run: () => Promise<unknown>) {
|
|
77
|
+
try {
|
|
78
|
+
return c.json(ok(await run()));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e instanceof McpAuthFlowNotFoundError) return c.json(err(e.message), 404);
|
|
81
|
+
return c.json(err((e as Error).message), 500);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** GET /api/mcp-auth/callback — PUBLIC. Where the authorization server sends the browser back. */
|
|
86
|
+
export function createMcpAuthCallbackHandler(flows: McpOAuthFlows) {
|
|
87
|
+
return async (c: Context) => {
|
|
88
|
+
const url = new URL(c.req.url);
|
|
89
|
+
const flow = flows.findByState(url.searchParams.get("state"));
|
|
90
|
+
// A flow without a redirect URI uses the CLI's own localhost listener; its state
|
|
91
|
+
// arriving here means somebody is replaying it, not completing it.
|
|
92
|
+
if (!flow || !flow.redirectUri) {
|
|
93
|
+
return c.html(page("Sign-in not found", "This sign-in link is no longer active. Start the sign-in again from PPM."), 404);
|
|
94
|
+
}
|
|
95
|
+
const serverName = flow.serverName;
|
|
96
|
+
const result = await flows.submitCallback(flow.id, callbackUrlFor(flow.redirectUri, url.search));
|
|
97
|
+
if (result.status === "done") {
|
|
98
|
+
return c.html(page("Signed in", `${serverName} is connected. You can close this tab and return to PPM.`, true));
|
|
99
|
+
}
|
|
100
|
+
return c.html(page("Sign-in failed", result.error ?? `Could not finish signing in to ${serverName}.`), 400);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const mcpAuthCallbackHandler = createMcpAuthCallbackHandler(mcpOAuthFlows);
|
|
105
|
+
|
|
106
|
+
function page(title: string, message: string, closeSoon = false): string {
|
|
107
|
+
// Best effort: after the cross-site redirects most browsers refuse to let a tab close
|
|
108
|
+
// itself, and then the message simply stays.
|
|
109
|
+
const script = closeSoon ? "<script>setTimeout(function(){window.close()},1500)</script>" : "";
|
|
110
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">`
|
|
111
|
+
+ `<title>${escapeHtml(title)}</title><style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1.5rem;line-height:1.5;color-scheme:light dark}</style>`
|
|
112
|
+
+ `</head><body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p>${script}</body></html>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function escapeHtml(s: string): string {
|
|
116
|
+
return s.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]!);
|
|
117
|
+
}
|
|
@@ -118,6 +118,8 @@ const UI_PREF_VALIDATORS: Record<string, (v: unknown) => boolean> = {
|
|
|
118
118
|
sidebarCollapsed: (v) => typeof v === "boolean",
|
|
119
119
|
remoteDesktopStatsVisible: (v) => typeof v === "boolean",
|
|
120
120
|
remoteDesktopWarningDismissed: (v) => typeof v === "boolean",
|
|
121
|
+
// MCP servers the user hid from the chat's sign-in bar (by name)
|
|
122
|
+
mcpSignInDismissed: (v) => Array.isArray(v) && v.length <= 200 && v.every((s) => typeof s === "string" && s.length <= 200),
|
|
121
123
|
keepScreenAwake: (v) => typeof v === "boolean",
|
|
122
124
|
sidebarWidth: (v) => typeof v === "number" && v >= 200 && v <= 600,
|
|
123
125
|
gitStatusViewMode: (v) => v === "flat" || v === "tree",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps each chat's "needs sign-in" list in step with MCP sign-ins.
|
|
3
|
+
*
|
|
4
|
+
* The list comes from the SDK's `init` message, so it is only as fresh as the subprocess.
|
|
5
|
+
* A sign-in finished from any screen stores tokens every Claude process can read, but a
|
|
6
|
+
* running chat subprocess keeps the server in `needs-auth` until told to reconnect it — so
|
|
7
|
+
* on each sign-in the server is reconnected in every chat still listing it.
|
|
8
|
+
*
|
|
9
|
+
* If that chat's subprocess still reports `needs-auth` afterwards, the only way to make it
|
|
10
|
+
* read the tokens is a fresh subprocess. That is done only when nothing would be lost: an
|
|
11
|
+
* idle turn is not enough, because background agents and shells keep running inside the
|
|
12
|
+
* subprocess after the turn that started them has ended. Otherwise the server goes back on
|
|
13
|
+
* the list, so the bar keeps saying what is still true.
|
|
14
|
+
*/
|
|
15
|
+
import { mcpOAuthFlows } from "../../services/mcp-oauth/mcp-oauth-flows.ts";
|
|
16
|
+
|
|
17
|
+
export interface McpSignInSession {
|
|
18
|
+
providerId: string;
|
|
19
|
+
phase: string;
|
|
20
|
+
mcpNeedsAuth?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface McpSignInSyncDeps {
|
|
24
|
+
sessions: () => Iterable<[string, McpSignInSession]>;
|
|
25
|
+
broadcast: (sessionId: string, event: unknown) => void;
|
|
26
|
+
reconnect: (providerId: string, sessionId: string, serverName: string) => Promise<string | null>;
|
|
27
|
+
/** Whether the subprocess can be replaced without killing work still running in it. */
|
|
28
|
+
canDrop: (sessionId: string) => boolean;
|
|
29
|
+
dropIdle: (sessionId: string, serverName: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The event carrying a session's list to its clients. */
|
|
33
|
+
export function mcpStatusEvent(needsAuth: string[] | undefined) {
|
|
34
|
+
return { type: "mcp_status", needsAuth: needsAuth ?? [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** React to one successful sign-in. Exported for tests; production wires it via `register`. */
|
|
38
|
+
export async function handleMcpAuthorized(serverName: string, deps: McpSignInSyncDeps): Promise<void> {
|
|
39
|
+
const affected = [...deps.sessions()].filter(([, s]) => s.mcpNeedsAuth?.includes(serverName));
|
|
40
|
+
await Promise.all(affected.map(async ([sessionId, s]) => {
|
|
41
|
+
s.mcpNeedsAuth = s.mcpNeedsAuth!.filter((n) => n !== serverName);
|
|
42
|
+
deps.broadcast(sessionId, mcpStatusEvent(s.mcpNeedsAuth));
|
|
43
|
+
const status = await deps.reconnect(s.providerId, sessionId, serverName).catch(() => null);
|
|
44
|
+
// Anything but a definite `needs-auth` (connected, pending, a transient failure, or no
|
|
45
|
+
// live subprocess at all) is left alone: the next `init` reports the truth.
|
|
46
|
+
if (status !== "needs-auth") return;
|
|
47
|
+
if (s.phase === "idle" && deps.canDrop(sessionId)) {
|
|
48
|
+
deps.dropIdle(sessionId, serverName);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!s.mcpNeedsAuth!.includes(serverName)) s.mcpNeedsAuth = [...s.mcpNeedsAuth!, serverName];
|
|
52
|
+
deps.broadcast(sessionId, mcpStatusEvent(s.mcpNeedsAuth));
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerMcpSignInSync(deps: McpSignInSyncDeps): () => void {
|
|
57
|
+
return mcpOAuthFlows.onAuthorized((serverName) => { void handleMcpAuthorized(serverName, deps); });
|
|
58
|
+
}
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { configService } from "../../services/config.service.ts";
|
|
|
18
18
|
import { formatTurnUsageLog } from "../../shared/turn-usage.ts";
|
|
19
19
|
import { isAsyncAgentLaunchAck, isTerminalAgentStatus } from "../../shared/background-agent-status.ts";
|
|
20
20
|
import { cacheReleaseDelayMs, selectWarmIdleEvictions } from "../../services/subprocess-retention.ts";
|
|
21
|
+
import { needsAuthServerNames } from "../../services/mcp-oauth/mcp-oauth-redirect.ts";
|
|
22
|
+
import { mcpStatusEvent, registerMcpSignInSync } from "./chat-mcp-sign-in-sync.ts";
|
|
21
23
|
|
|
22
24
|
/** Resolve the SESSION's provider config — not the global default provider's.
|
|
23
25
|
* Otherwise a non-default provider's chat (e.g. codex) would inherit claude's values. */
|
|
@@ -116,6 +118,8 @@ interface SessionEntry {
|
|
|
116
118
|
lastImplicitTeamProbe?: number;
|
|
117
119
|
/** Compact indicator state — sticky until turn ends or boundary received, synced on reconnect */
|
|
118
120
|
compactStatus?: "compacting" | null;
|
|
121
|
+
/** MCP servers the subprocess reported as `needs-auth` at init — drives the chat's sign-in bar */
|
|
122
|
+
mcpNeedsAuth?: string[];
|
|
119
123
|
/** toolUseIds of Bash/Agent calls launched with run_in_background — their spy outlives the tool_result */
|
|
120
124
|
backgroundToolUseIds?: Set<string>;
|
|
121
125
|
/** Nested-agent children buffered into turnEvents this turn (see MAX_NESTED_TURN_EVENTS) */
|
|
@@ -249,6 +253,28 @@ function broadcastBackgroundRegistry(sessionId: string): void {
|
|
|
249
253
|
/** Tracks active sessions — persists even when FE disconnects */
|
|
250
254
|
const activeSessions = new Map<string, SessionEntry>();
|
|
251
255
|
|
|
256
|
+
registerMcpSignInSync({
|
|
257
|
+
sessions: () => activeSessions.entries(),
|
|
258
|
+
broadcast: (sessionId, event) => broadcast(sessionId, event),
|
|
259
|
+
reconnect: async (providerId, sessionId, serverName) => {
|
|
260
|
+
const provider = providerRegistry.get(providerId) as { reconnectMcpServer?: (s: string, n: string) => Promise<string | null> } | undefined;
|
|
261
|
+
return provider?.reconnectMcpServer?.(sessionId, serverName) ?? null;
|
|
262
|
+
},
|
|
263
|
+
// Background agents and shells outlive the turn inside the subprocess; a sign-in elsewhere
|
|
264
|
+
// must never be what kills them.
|
|
265
|
+
canDrop: (sessionId) => {
|
|
266
|
+
const entry = activeSessions.get(sessionId);
|
|
267
|
+
if (!entry || entry.isStreamingActive) return false;
|
|
268
|
+
if ((entry.backgroundToolUseIds?.size ?? 0) > 0) return false;
|
|
269
|
+
return !backgroundShellRegistry.list(sessionId).some((sh) => sh.status !== "stopped");
|
|
270
|
+
},
|
|
271
|
+
dropIdle: (sessionId, serverName) => dropIdleSubprocess(
|
|
272
|
+
sessionId,
|
|
273
|
+
"mcp_sign_in",
|
|
274
|
+
`Subprocess released: it could not see the new ${serverName} sign-in, the next turn starts a fresh one`,
|
|
275
|
+
),
|
|
276
|
+
});
|
|
277
|
+
|
|
252
278
|
/** Check if any frontend client is currently connected via WebSocket */
|
|
253
279
|
export function hasActiveClient(): boolean {
|
|
254
280
|
for (const entry of activeSessions.values()) {
|
|
@@ -609,7 +635,10 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
609
635
|
// System events → transition connecting → thinking, forward compact events
|
|
610
636
|
if (evType === "system") {
|
|
611
637
|
const sub = (ev as any).subtype;
|
|
612
|
-
if (sub === "
|
|
638
|
+
if (sub === "init" && Array.isArray(ev.mcpServers)) {
|
|
639
|
+
entry.mcpNeedsAuth = needsAuthServerNames(ev.mcpServers);
|
|
640
|
+
broadcast(sessionId, mcpStatusEvent(entry.mcpNeedsAuth));
|
|
641
|
+
} else if (sub === "compacting") {
|
|
613
642
|
entry.compactStatus = "compacting";
|
|
614
643
|
console.log(`[chat] session=${sessionId} compact_status=compacting (persisted on entry)`);
|
|
615
644
|
broadcast(sessionId, { type: "compact_status", status: "compacting" });
|
|
@@ -1001,6 +1030,7 @@ export const chatWebSocket = {
|
|
|
1001
1030
|
pendingApproval: existing.pendingApprovalEvent ?? null,
|
|
1002
1031
|
sessionTitle: session?.title || null,
|
|
1003
1032
|
compactStatus: existing.compactStatus ?? null,
|
|
1033
|
+
mcpNeedsAuth: existing.mcpNeedsAuth ?? [],
|
|
1004
1034
|
model: resolveSessionModel(sessionId),
|
|
1005
1035
|
effort: resolveSessionEffort(sessionId),
|
|
1006
1036
|
thinking: resolveSessionThinkingEnabled(sessionId),
|
|
@@ -1134,6 +1164,7 @@ export const chatWebSocket = {
|
|
|
1134
1164
|
pendingApproval: entry.pendingApprovalEvent ?? null,
|
|
1135
1165
|
sessionTitle: chatService.getSession(sessionId)?.title || null,
|
|
1136
1166
|
compactStatus: entry.compactStatus ?? null,
|
|
1167
|
+
mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
|
|
1137
1168
|
model: resolveSessionModel(sessionId),
|
|
1138
1169
|
effort: resolveSessionEffort(sessionId),
|
|
1139
1170
|
thinking: resolveSessionThinkingEnabled(sessionId),
|
|
@@ -1342,6 +1373,7 @@ export const chatWebSocket = {
|
|
|
1342
1373
|
pendingApproval: entry.pendingApprovalEvent ?? null,
|
|
1343
1374
|
sessionTitle: chatService.getSession(sessionId)?.title || null,
|
|
1344
1375
|
compactStatus: entry.compactStatus ?? null,
|
|
1376
|
+
mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
|
|
1345
1377
|
model: resolveSessionModel(sessionId),
|
|
1346
1378
|
effort: resolveSessionEffort(sessionId),
|
|
1347
1379
|
thinking: resolveSessionThinkingEnabled(sessionId),
|
|
@@ -1368,6 +1400,7 @@ export const chatWebSocket = {
|
|
|
1368
1400
|
pendingApproval: entry.pendingApprovalEvent ?? null,
|
|
1369
1401
|
sessionTitle: chatService.getSession(sessionId)?.title || null,
|
|
1370
1402
|
compactStatus: entry.compactStatus ?? null,
|
|
1403
|
+
mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
|
|
1371
1404
|
model: resolveSessionModel(sessionId),
|
|
1372
1405
|
effort: resolveSessionEffort(sessionId),
|
|
1373
1406
|
thinking: resolveSessionThinkingEnabled(sessionId),
|
|
@@ -1388,6 +1421,7 @@ export const chatWebSocket = {
|
|
|
1388
1421
|
pendingApproval: entry.pendingApprovalEvent ?? null,
|
|
1389
1422
|
sessionTitle: chatService.getSession(sessionId)?.title || null,
|
|
1390
1423
|
compactStatus: entry.compactStatus ?? null,
|
|
1424
|
+
mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
|
|
1391
1425
|
model: resolveSessionModel(sessionId),
|
|
1392
1426
|
effort: resolveSessionEffort(sessionId),
|
|
1393
1427
|
thinking: resolveSessionThinkingEnabled(sessionId),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { McpServerState } from "./mcp-oauth-redirect.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The control requests PPM sends to a prompt-less Claude subprocess.
|
|
5
|
+
*
|
|
6
|
+
* `mcpAuthenticate` and `mcpSubmitOAuthCallbackUrl` exist on the SDK's `Query` object
|
|
7
|
+
* (they are what Claude's own apps use for "Sign in") but are left out of `sdk.d.ts`,
|
|
8
|
+
* which is why they are typed here rather than imported. A test asserts they still exist
|
|
9
|
+
* so an SDK upgrade that renames them fails loudly instead of the button doing nothing.
|
|
10
|
+
*/
|
|
11
|
+
export interface McpControlQuery {
|
|
12
|
+
mcpServerStatus(): Promise<McpServerState[]>;
|
|
13
|
+
mcpAuthenticate(serverName: string, redirectUri?: string): Promise<McpAuthenticateResult>;
|
|
14
|
+
mcpSubmitOAuthCallbackUrl(serverName: string, callbackUrl: string): Promise<unknown>;
|
|
15
|
+
reconnectMcpServer(serverName: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What the CLI answers to `mcp_authenticate`. */
|
|
19
|
+
export interface McpAuthenticateResult {
|
|
20
|
+
authUrl?: string;
|
|
21
|
+
/** False when the server is already signed in — nothing for the user to do. */
|
|
22
|
+
requiresUserAction: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* False for claude.ai connectors: the grant happens on claude.ai and nothing is ever
|
|
25
|
+
* redirected back, so completion can only be detected by reconnecting.
|
|
26
|
+
*/
|
|
27
|
+
callbackExpected: boolean;
|
|
28
|
+
/** "localhost": the CLI's own listener on `callbackPort`. "custom": the redirect URI PPM passed. */
|
|
29
|
+
redirectScheme?: "localhost" | "custom";
|
|
30
|
+
callbackPort?: number;
|
|
31
|
+
state?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface McpControlHandle {
|
|
35
|
+
query: McpControlQuery;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type OpenMcpControlQuery = (cwd: string) => Promise<McpControlHandle>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A control request that never answers must not hold its subprocess (or everyone waiting
|
|
43
|
+
* on it) forever; this turns silence into an error the caller can settle on.
|
|
44
|
+
*/
|
|
45
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
46
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
const timeout = new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
|
|
48
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Opens control queries through the Claude provider, configured exactly like its chats. */
|
|
52
|
+
export const openClaudeMcpControlQuery: OpenMcpControlQuery = async (cwd) => {
|
|
53
|
+
// Lazy: the registry pulls in every provider, which a unit test of the flow logic must not.
|
|
54
|
+
const { providerRegistry } = await import("../../providers/registry.ts");
|
|
55
|
+
const provider = providerRegistry.get("claude") as { openMcpControlQuery?: (cwd: string) => McpControlHandle } | undefined;
|
|
56
|
+
if (!provider?.openMcpControlQuery) throw new Error("The Claude provider is not available");
|
|
57
|
+
return provider.openMcpControlQuery(cwd);
|
|
58
|
+
};
|