@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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP sign-in flows.
|
|
3
|
+
*
|
|
4
|
+
* Each flow owns one prompt-less Claude subprocess for its whole life, and that is not a
|
|
5
|
+
* detail: when the CLI redirects to its own `http://localhost:<port>/callback`, the
|
|
6
|
+
* listener runs inside that process, so closing it early strands the browser on a dead
|
|
7
|
+
* page. The subprocess is closed as soon as the flow settles, or after `FLOW_TTL_MS`.
|
|
8
|
+
*
|
|
9
|
+
* The tokens the CLI stores are shared with every other Claude process on the machine
|
|
10
|
+
* (same credential store as an interactive `claude`), which is why a sign-in finished here
|
|
11
|
+
* is visible to chats — they only need to reconnect the server (see `onAuthorized`).
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import type { McpControlHandle, OpenMcpControlQuery } from "./mcp-control-query.ts";
|
|
15
|
+
import { openClaudeMcpControlQuery, withTimeout } from "./mcp-control-query.ts";
|
|
16
|
+
|
|
17
|
+
export type McpAuthFlowStatus = "waiting" | "completing" | "done" | "failed" | "expired" | "cancelled";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What the browser is told about a flow. The `state` is not a field of its own, though it
|
|
21
|
+
* is inside `authUrl` — which only the authenticated user who started the flow can read.
|
|
22
|
+
*/
|
|
23
|
+
export interface McpAuthFlowView {
|
|
24
|
+
id: string;
|
|
25
|
+
serverName: string;
|
|
26
|
+
status: McpAuthFlowStatus;
|
|
27
|
+
authUrl?: string;
|
|
28
|
+
callbackExpected: boolean;
|
|
29
|
+
redirectScheme?: "localhost" | "custom";
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Flow extends McpAuthFlowView {
|
|
34
|
+
control: McpControlHandle | null;
|
|
35
|
+
state?: string;
|
|
36
|
+
/** Only set for a "custom" flow: the redirect URI the authorization server was given. */
|
|
37
|
+
redirectUri?: string;
|
|
38
|
+
expiryTimer?: ReturnType<typeof setTimeout>;
|
|
39
|
+
pollTimer?: ReturnType<typeof setInterval>;
|
|
40
|
+
polling?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Long enough to finish a real sign-in (2FA, SSO); short enough not to hoard a subprocess. */
|
|
44
|
+
const FLOW_TTL_MS = 10 * 60_000;
|
|
45
|
+
/** How often a localhost flow checks whether the CLI has finished exchanging the code. */
|
|
46
|
+
const POLL_MS = 2_000;
|
|
47
|
+
/** A settled flow stays readable this long, so a polling dialog sees how it ended. */
|
|
48
|
+
const SETTLED_RETENTION_MS = 5 * 60_000;
|
|
49
|
+
/** How long the CLI gets to produce a sign-in link (it registers a client first, over the network). */
|
|
50
|
+
const START_TIMEOUT_MS = 60_000;
|
|
51
|
+
/** Any other control request: status reads, reconnects, and exchanging the code. */
|
|
52
|
+
const REQUEST_TIMEOUT_MS = 45_000;
|
|
53
|
+
|
|
54
|
+
type AuthorizedListener = (serverName: string) => void;
|
|
55
|
+
|
|
56
|
+
export class McpOAuthFlows {
|
|
57
|
+
private flows = new Map<string, Flow>();
|
|
58
|
+
private listeners = new Set<AuthorizedListener>();
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly open: OpenMcpControlQuery = openClaudeMcpControlQuery,
|
|
62
|
+
private readonly timing = {
|
|
63
|
+
ttlMs: FLOW_TTL_MS, pollMs: POLL_MS, retentionMs: SETTLED_RETENTION_MS,
|
|
64
|
+
startMs: START_TIMEOUT_MS, requestMs: REQUEST_TIMEOUT_MS,
|
|
65
|
+
},
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
/** Called with a server's name each time a sign-in for it succeeds. Returns an unsubscribe. */
|
|
69
|
+
onAuthorized(listener: AuthorizedListener): () => void {
|
|
70
|
+
this.listeners.add(listener);
|
|
71
|
+
return () => this.listeners.delete(listener);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get(id: string): McpAuthFlowView | null {
|
|
75
|
+
const flow = this.flows.get(id);
|
|
76
|
+
return flow ? view(flow) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Start signing in to `serverName` as a chat in `cwd` would see it. A flow already open
|
|
81
|
+
* for the same server is cancelled: only the newest sign-in link can still succeed.
|
|
82
|
+
*/
|
|
83
|
+
async start(serverName: string, cwd: string, redirectUri?: string): Promise<McpAuthFlowView> {
|
|
84
|
+
for (const f of this.flows.values()) {
|
|
85
|
+
if (f.serverName === serverName && isOpen(f.status)) this.settle(f, "cancelled");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const t0 = Date.now();
|
|
89
|
+
console.log(`[mcp-oauth] ${serverName}: starting sign-in (cwd=${cwd}, redirect=${redirectUri ? "custom" : "localhost"})`);
|
|
90
|
+
const control = await this.open(cwd);
|
|
91
|
+
const flow: Flow = { id: randomUUID(), serverName, status: "waiting", callbackExpected: false, control };
|
|
92
|
+
this.flows.set(flow.id, flow);
|
|
93
|
+
try {
|
|
94
|
+
// Bounded: the expiry timer only starts once there is a link, so a CLI that never
|
|
95
|
+
// answers would otherwise hold its subprocess open for as long as PPM runs.
|
|
96
|
+
const res = await withTimeout(
|
|
97
|
+
control.query.mcpAuthenticate(serverName, redirectUri),
|
|
98
|
+
this.timing.startMs,
|
|
99
|
+
`${serverName} did not return a sign-in link. Try again.`,
|
|
100
|
+
);
|
|
101
|
+
console.log(`[mcp-oauth] ${serverName}: sign-in link ready in ${Date.now() - t0}ms (redirect=${res.redirectScheme ?? "none"}, flow=${flow.id})`);
|
|
102
|
+
// A newer sign-in for this server may have cancelled this one while the CLI answered.
|
|
103
|
+
if (!isOpen(flow.status)) return view(flow);
|
|
104
|
+
if (!res.requiresUserAction) {
|
|
105
|
+
this.settle(flow, "done");
|
|
106
|
+
return view(flow);
|
|
107
|
+
}
|
|
108
|
+
flow.authUrl = res.authUrl;
|
|
109
|
+
flow.callbackExpected = res.callbackExpected;
|
|
110
|
+
flow.redirectScheme = res.redirectScheme;
|
|
111
|
+
flow.state = res.state;
|
|
112
|
+
if (res.redirectScheme === "custom") flow.redirectUri = redirectUri;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
this.settle(flow, "failed", errorMessage(e));
|
|
115
|
+
return view(flow);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
flow.expiryTimer = setTimeout(() => this.settle(flow, "expired", "The sign-in link expired. Start again."), this.timing.ttlMs);
|
|
119
|
+
// The CLI's own listener gets the code without telling anyone, so the only way to see
|
|
120
|
+
// it finish is to watch the server's status change.
|
|
121
|
+
if (flow.callbackExpected && flow.redirectScheme === "localhost") {
|
|
122
|
+
flow.pollTimer = setInterval(() => { void this.pollStatus(flow); }, this.timing.pollMs);
|
|
123
|
+
}
|
|
124
|
+
return view(flow);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Hand the redirect URL to the CLI — pasted by the user, or delivered by the public
|
|
129
|
+
* callback route. The CLI validates it against this flow's `state` itself.
|
|
130
|
+
*/
|
|
131
|
+
async submitCallback(id: string, callbackUrl: string): Promise<McpAuthFlowView> {
|
|
132
|
+
const flow = this.requireFlow(id);
|
|
133
|
+
if (flow.status !== "waiting") return view(flow);
|
|
134
|
+
if (!flow.control) return view(flow);
|
|
135
|
+
flow.status = "completing";
|
|
136
|
+
flow.error = undefined;
|
|
137
|
+
try {
|
|
138
|
+
// The public callback page waits on this; a proxy in front (Cloudflare gives up at
|
|
139
|
+
// 100 s) would otherwise show its own error page while the sign-in carries on.
|
|
140
|
+
await withTimeout(
|
|
141
|
+
flow.control.query.mcpSubmitOAuthCallbackUrl(flow.serverName, callbackUrl),
|
|
142
|
+
this.timing.requestMs,
|
|
143
|
+
`${flow.serverName} did not confirm the sign-in in time. Check the server's status, or start again.`,
|
|
144
|
+
);
|
|
145
|
+
this.settle(flow, "done");
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// A mistyped or foreign URL leaves the CLI's flow open, so let the user try again.
|
|
148
|
+
if (isOpen(flow.status)) {
|
|
149
|
+
flow.status = "waiting";
|
|
150
|
+
flow.error = errorMessage(e);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return view(flow);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The flow a redirect's `state` parameter belongs to, if it is still waiting for one. */
|
|
157
|
+
findByState(state: string | null): Flow | null {
|
|
158
|
+
if (!state) return null;
|
|
159
|
+
for (const f of this.flows.values()) {
|
|
160
|
+
if (f.state === state && f.status === "waiting") return f;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* For a claude.ai connector, whose grant never redirects anywhere: the user says they
|
|
167
|
+
* are done, and a reconnect tells whether that is true.
|
|
168
|
+
*/
|
|
169
|
+
async confirm(id: string): Promise<McpAuthFlowView> {
|
|
170
|
+
const flow = this.requireFlow(id);
|
|
171
|
+
if (flow.status !== "waiting" || !flow.control) return view(flow);
|
|
172
|
+
try {
|
|
173
|
+
await withTimeout(flow.control.query.reconnectMcpServer(flow.serverName), this.timing.requestMs, "reconnect timed out");
|
|
174
|
+
} catch { /* the status read below is what decides */ }
|
|
175
|
+
const status = await this.serverStatus(flow);
|
|
176
|
+
if (status === "connected") this.settle(flow, "done");
|
|
177
|
+
else flow.error = "The server still needs a sign-in. Finish granting access, then try again.";
|
|
178
|
+
return view(flow);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
cancel(id: string): McpAuthFlowView | null {
|
|
182
|
+
const flow = this.flows.get(id);
|
|
183
|
+
if (!flow) return null;
|
|
184
|
+
if (isOpen(flow.status)) this.settle(flow, "cancelled");
|
|
185
|
+
return view(flow);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Close every subprocess — for server shutdown and tests. */
|
|
189
|
+
disposeAll(): void {
|
|
190
|
+
for (const f of this.flows.values()) {
|
|
191
|
+
if (isOpen(f.status)) this.settle(f, "cancelled");
|
|
192
|
+
}
|
|
193
|
+
this.flows.clear();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async pollStatus(flow: Flow): Promise<void> {
|
|
197
|
+
// One read at a time: a CLI that stops answering must not collect a request per tick.
|
|
198
|
+
if (flow.status !== "waiting" || flow.polling) return;
|
|
199
|
+
flow.polling = true;
|
|
200
|
+
try {
|
|
201
|
+
const status = await this.serverStatus(flow);
|
|
202
|
+
if (flow.status === "waiting" && status === "connected") this.settle(flow, "done");
|
|
203
|
+
} finally {
|
|
204
|
+
flow.polling = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async serverStatus(flow: Flow): Promise<string | null> {
|
|
209
|
+
if (!flow.control) return null;
|
|
210
|
+
try {
|
|
211
|
+
const all = await withTimeout(flow.control.query.mcpServerStatus(), this.timing.requestMs, "status timed out");
|
|
212
|
+
return all.find((s) => s.name === flow.serverName)?.status ?? null;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private settle(flow: Flow, status: McpAuthFlowStatus, error?: string): void {
|
|
219
|
+
if (!isOpen(flow.status)) return;
|
|
220
|
+
console.log(`[mcp-oauth] ${flow.serverName}: sign-in ${status}${error ? ` — ${error}` : ""} (flow=${flow.id})`);
|
|
221
|
+
flow.status = status;
|
|
222
|
+
flow.error = error;
|
|
223
|
+
if (flow.expiryTimer) clearTimeout(flow.expiryTimer);
|
|
224
|
+
if (flow.pollTimer) clearInterval(flow.pollTimer);
|
|
225
|
+
flow.expiryTimer = undefined;
|
|
226
|
+
flow.pollTimer = undefined;
|
|
227
|
+
try { flow.control?.close(); } catch { /* already gone */ }
|
|
228
|
+
flow.control = null;
|
|
229
|
+
// The state is single-use; forgetting it keeps a replayed redirect from matching.
|
|
230
|
+
flow.state = undefined;
|
|
231
|
+
if (status === "done") {
|
|
232
|
+
for (const l of this.listeners) {
|
|
233
|
+
try { l(flow.serverName); } catch (e) { console.warn(`[mcp-oauth] listener failed: ${errorMessage(e)}`); }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const timer = setTimeout(() => this.flows.delete(flow.id), this.timing.retentionMs);
|
|
237
|
+
(timer as { unref?: () => void }).unref?.();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private requireFlow(id: string): Flow {
|
|
241
|
+
const flow = this.flows.get(id);
|
|
242
|
+
if (!flow) throw new McpAuthFlowNotFoundError();
|
|
243
|
+
return flow;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export class McpAuthFlowNotFoundError extends Error {
|
|
248
|
+
constructor() { super("This sign-in is no longer active. Start again."); }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isOpen(status: McpAuthFlowStatus): boolean {
|
|
252
|
+
return status === "waiting" || status === "completing";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function view(f: Flow): McpAuthFlowView {
|
|
256
|
+
return {
|
|
257
|
+
id: f.id,
|
|
258
|
+
serverName: f.serverName,
|
|
259
|
+
status: f.status,
|
|
260
|
+
callbackExpected: f.callbackExpected,
|
|
261
|
+
...(f.authUrl && isOpen(f.status) && { authUrl: f.authUrl }),
|
|
262
|
+
...(f.redirectScheme && { redirectScheme: f.redirectScheme }),
|
|
263
|
+
...(f.error && { error: f.error }),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function errorMessage(e: unknown): string {
|
|
268
|
+
return e instanceof Error ? e.message : String(e);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const mcpOAuthFlows = new McpOAuthFlows();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decisions for MCP sign-in: where the authorization server sends the browser back
|
|
3
|
+
* to, and which servers need a sign-in at all. Kept free of imports so they can be unit
|
|
4
|
+
* tested without a CLI or a database.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Public route that receives the authorization server's redirect and hands it to the CLI. */
|
|
8
|
+
export const MCP_OAUTH_CALLBACK_PATH = "/api/mcp-auth/callback";
|
|
9
|
+
|
|
10
|
+
/** The subset of the SDK's `McpServerStatus` PPM passes around. */
|
|
11
|
+
export interface McpServerState {
|
|
12
|
+
name: string;
|
|
13
|
+
status: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
scope?: string;
|
|
16
|
+
source?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The redirect URI to ask the CLI for, or undefined to let it use its own
|
|
21
|
+
* `http://localhost:<port>/callback` listener.
|
|
22
|
+
*
|
|
23
|
+
* A redirect back to PPM is what makes a sign-in finish when the browser is not on the
|
|
24
|
+
* host machine (a phone over a tunnel): the CLI's localhost listener is only reachable
|
|
25
|
+
* from the host itself. It is offered only for an https origin, because authorization
|
|
26
|
+
* servers refuse plain-http redirects to anything but loopback, and only when the origin
|
|
27
|
+
* the browser reports is the host this request actually arrived on — otherwise a crafted
|
|
28
|
+
* origin could have the authorization code delivered to somebody else's server.
|
|
29
|
+
*/
|
|
30
|
+
export function customRedirectUri(
|
|
31
|
+
origin: string | undefined,
|
|
32
|
+
requestHosts: Array<string | undefined>,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
if (!origin) return undefined;
|
|
35
|
+
let url: URL;
|
|
36
|
+
try { url = new URL(origin); } catch { return undefined; }
|
|
37
|
+
if (url.protocol !== "https:" || url.username || url.password) return undefined;
|
|
38
|
+
const hosts = requestHosts
|
|
39
|
+
.flatMap((h) => (h ?? "").split(","))
|
|
40
|
+
.map((h) => h.trim().toLowerCase())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
if (!hosts.includes(url.host.toLowerCase())) return undefined;
|
|
43
|
+
return `${url.origin}${MCP_OAUTH_CALLBACK_PATH}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Names of the servers waiting for a sign-in, in the order the CLI reported them. */
|
|
47
|
+
export function needsAuthServerNames(servers: unknown): string[] {
|
|
48
|
+
if (!Array.isArray(servers)) return [];
|
|
49
|
+
const names: string[] = [];
|
|
50
|
+
for (const s of servers) {
|
|
51
|
+
if (s && typeof s === "object" && (s as { status?: unknown }).status === "needs-auth") {
|
|
52
|
+
const name = (s as { name?: unknown }).name;
|
|
53
|
+
if (typeof name === "string" && name && !names.includes(name)) names.push(name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return names;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Rebuild the redirect the CLI is waiting for from the query string that reached PPM.
|
|
61
|
+
* The request URL itself cannot be forwarded: behind a tunnel it names the local port,
|
|
62
|
+
* not the redirect URI the sign-in was started with.
|
|
63
|
+
*/
|
|
64
|
+
export function callbackUrlFor(redirectUri: string, search: string): string {
|
|
65
|
+
const url = new URL(redirectUri);
|
|
66
|
+
url.search = search.startsWith("?") ? search.slice(1) : search;
|
|
67
|
+
return url.href;
|
|
68
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server status as a chat in a given directory would see it, for screens that have no
|
|
3
|
+
* chat to ask (the AI Resources list).
|
|
4
|
+
*
|
|
5
|
+
* Every probe starts a Claude subprocess, which in turn connects every configured MCP
|
|
6
|
+
* server — stdio ones included — so answers are cached per directory and concurrent
|
|
7
|
+
* callers share one probe.
|
|
8
|
+
*/
|
|
9
|
+
import type { OpenMcpControlQuery } from "./mcp-control-query.ts";
|
|
10
|
+
import { openClaudeMcpControlQuery, withTimeout } from "./mcp-control-query.ts";
|
|
11
|
+
import type { McpServerState } from "./mcp-oauth-redirect.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Long, because a probe is not free: it starts a full CLI, which loads every plugin and
|
|
15
|
+
* spawns every stdio MCP server. A sign-in invalidates it, and the list's refresh button
|
|
16
|
+
* asks for a fresh answer, so nothing that changes it here goes unseen.
|
|
17
|
+
*/
|
|
18
|
+
const CACHE_MS = 5 * 60_000;
|
|
19
|
+
/** Servers start "pending"; wait this long for them to settle before answering anyway. */
|
|
20
|
+
const SETTLE_MS = 5_000;
|
|
21
|
+
const SETTLE_POLL_MS = 500;
|
|
22
|
+
/** The first read includes the subprocess starting up and loading plugins. */
|
|
23
|
+
const READ_TIMEOUT_MS = 30_000;
|
|
24
|
+
|
|
25
|
+
export class McpStatusProbe {
|
|
26
|
+
private cache = new Map<string, { at: number; servers: McpServerState[] }>();
|
|
27
|
+
private inFlight = new Map<string, Promise<McpServerState[]>>();
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly open: OpenMcpControlQuery = openClaudeMcpControlQuery,
|
|
31
|
+
private readonly timing = { cacheMs: CACHE_MS, settleMs: SETTLE_MS, pollMs: SETTLE_POLL_MS, readMs: READ_TIMEOUT_MS },
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
async status(cwd: string, opts: { fresh?: boolean } = {}): Promise<McpServerState[]> {
|
|
35
|
+
const cached = this.cache.get(cwd);
|
|
36
|
+
if (!opts.fresh && cached && Date.now() - cached.at < this.timing.cacheMs) return cached.servers;
|
|
37
|
+
const running = this.inFlight.get(cwd);
|
|
38
|
+
if (running) return running;
|
|
39
|
+
|
|
40
|
+
const probe = this.probe(cwd).finally(() => this.inFlight.delete(cwd));
|
|
41
|
+
this.inFlight.set(cwd, probe);
|
|
42
|
+
return probe;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Forget every cached answer — a sign-in just changed what they would say. */
|
|
46
|
+
invalidate(): void {
|
|
47
|
+
this.cache.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async probe(cwd: string): Promise<McpServerState[]> {
|
|
51
|
+
const control = await this.open(cwd);
|
|
52
|
+
// Every caller for this directory shares this probe, so it has to end even if the CLI
|
|
53
|
+
// stops answering.
|
|
54
|
+
const read = () => withTimeout(control.query.mcpServerStatus(), this.timing.readMs, "Claude did not report MCP status");
|
|
55
|
+
try {
|
|
56
|
+
const deadline = Date.now() + this.timing.settleMs;
|
|
57
|
+
let servers = await read();
|
|
58
|
+
while (servers.some((s) => s.status === "pending") && Date.now() < deadline) {
|
|
59
|
+
await new Promise((r) => setTimeout(r, this.timing.pollMs));
|
|
60
|
+
servers = await read();
|
|
61
|
+
}
|
|
62
|
+
const slim = servers.map(({ name, status, error, scope, source }) => ({
|
|
63
|
+
name, status,
|
|
64
|
+
...(error && { error }),
|
|
65
|
+
...(scope && { scope }),
|
|
66
|
+
...(source && { source }),
|
|
67
|
+
}));
|
|
68
|
+
this.cache.set(cwd, { at: Date.now(), servers: slim });
|
|
69
|
+
return slim;
|
|
70
|
+
} finally {
|
|
71
|
+
control.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const mcpStatusProbe = new McpStatusProbe();
|
|
@@ -18,6 +18,7 @@ import { openResourceTab } from "./open-resource-tab";
|
|
|
18
18
|
import { useMcpServers } from "./use-mcp-servers";
|
|
19
19
|
import { McpGroup, serverPreview } from "./mcp-group";
|
|
20
20
|
import { NewResourceMenu } from "./new-resource-menu";
|
|
21
|
+
import { McpSignInList } from "@/components/mcp-auth/mcp-sign-in-list";
|
|
21
22
|
|
|
22
23
|
type TypeFilter = "all" | AiResourceType | "mcp";
|
|
23
24
|
const FILTERS: { id: TypeFilter; label: string }[] = [
|
|
@@ -181,6 +182,9 @@ export function AiResourcesPanel() {
|
|
|
181
182
|
|
|
182
183
|
{/* List */}
|
|
183
184
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
185
|
+
{/* First, and outside the states below: it is something to act on, it has its own
|
|
186
|
+
data source, and under "All" the MCP group sits below hundreds of skills. */}
|
|
187
|
+
{showMcp && <McpSignInList project={activeProject?.name} search={search} />}
|
|
184
188
|
{error ? (
|
|
185
189
|
<div className="p-4 text-center text-xs text-destructive">{error}</div>
|
|
186
190
|
) : loading && !result ? (
|
|
@@ -16,6 +16,7 @@ import { getAISettings, pickAccountForTab } from "@/lib/api-settings";
|
|
|
16
16
|
import { MessageList } from "./message-list";
|
|
17
17
|
import { BackgroundCommandBar } from "./background-command-bar";
|
|
18
18
|
import { TeamWorkingBar } from "./team-working-bar";
|
|
19
|
+
import { McpSignInBar } from "@/components/mcp-auth/mcp-sign-in-bar";
|
|
19
20
|
import { useTeamActivityFeed } from "@/hooks/use-team-activity-feed";
|
|
20
21
|
import { MessageInput, type ChatAttachment, type MessagePriority } from "./message-input";
|
|
21
22
|
import { SlashCommandPicker, type SlashItem } from "./slash-command-picker";
|
|
@@ -283,6 +284,7 @@ function ChatTabContent({ metadata, tabId }: ChatTabProps) {
|
|
|
283
284
|
pendingApproval,
|
|
284
285
|
contextWindowPct,
|
|
285
286
|
compactStatus,
|
|
287
|
+
mcpNeedsAuth,
|
|
286
288
|
statusMessage,
|
|
287
289
|
sessionTitle,
|
|
288
290
|
liveAccount,
|
|
@@ -960,6 +962,9 @@ function ChatTabContent({ metadata, tabId }: ChatTabProps) {
|
|
|
960
962
|
{/* Teammates still working — pinned here so it is the last thing under the conversation */}
|
|
961
963
|
<TeamWorkingBar teamName={primaryTeam} members={teamMembers} projectName={projectName} />
|
|
962
964
|
|
|
965
|
+
{/* MCP servers this session cannot use until someone signs in */}
|
|
966
|
+
<McpSignInBar key={sessionId ?? "draft"} needsAuth={mcpNeedsAuth} projectName={projectName || undefined} />
|
|
967
|
+
|
|
963
968
|
{/* Bottom toolbar */}
|
|
964
969
|
<div className="border-t border-border bg-panel shrink-0">
|
|
965
970
|
{/* Unified toolbar: History, Config, Usage, Bug report, Connection */}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude.ai connectors (Slack, Gmail, Drive… added on claude.ai) reach Claude through the
|
|
3
|
+
* account, not through local config, and the CLI names them `claude.ai <display name>`.
|
|
4
|
+
* Its `mcp_authenticate` answers "Server not found" for them in SDK mode, so they are
|
|
5
|
+
* connected where they were added — on claude.ai — rather than through a local flow.
|
|
6
|
+
*/
|
|
7
|
+
export const CLAUDE_AI_CONNECTORS_URL = "https://claude.ai/settings/connectors";
|
|
8
|
+
|
|
9
|
+
export function isClaudeAiConnector(name: string, scope?: string): boolean {
|
|
10
|
+
return scope === "claudeai" || name.startsWith("claude.ai ");
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's MCP server is named `plugin:<plugin>:<server>` by the CLI. The prefix is what
|
|
3
|
+
* keeps names unique, but in a one-line bar it pushes every server after the first out of
|
|
4
|
+
* view, so the bar shows the last segment and keeps the full name as the tooltip.
|
|
5
|
+
*/
|
|
6
|
+
export function shortMcpServerName(name: string): string {
|
|
7
|
+
if (!name.startsWith("plugin:")) return name;
|
|
8
|
+
const last = name.slice(name.lastIndexOf(":") + 1);
|
|
9
|
+
return last || name;
|
|
10
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { KeyRound, X } from "@/lib/icons";
|
|
4
|
+
import { useSettingsStore } from "@/stores/settings-store";
|
|
5
|
+
import { McpSignInDialog } from "./mcp-sign-in-dialog";
|
|
6
|
+
import { shortMcpServerName } from "./mcp-server-name";
|
|
7
|
+
|
|
8
|
+
interface McpSignInBarProps {
|
|
9
|
+
/** Servers this chat's Claude subprocess reported as needing a sign-in. */
|
|
10
|
+
needsAuth: string[];
|
|
11
|
+
projectName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A strip above the chat input naming the MCP servers that are configured but unusable
|
|
16
|
+
* until someone signs in, each with a button that does it. Without it the only sign of
|
|
17
|
+
* the problem is Claude saying it cannot reach a tool.
|
|
18
|
+
*
|
|
19
|
+
* Hiding it is remembered per server, not per chat: a plugin can ship a dozen connectors
|
|
20
|
+
* nobody means to use, and a bar that returns in every new chat is noise. It shows again
|
|
21
|
+
* only for a server outside the hidden list.
|
|
22
|
+
*/
|
|
23
|
+
export function McpSignInBar({ needsAuth, projectName }: McpSignInBarProps) {
|
|
24
|
+
const [signingIn, setSigningIn] = useState<string | null>(null);
|
|
25
|
+
const dismissed = useSettingsStore((s) => s.mcpSignInDismissed);
|
|
26
|
+
const dismissMcpSignIn = useSettingsStore((s) => s.dismissMcpSignIn);
|
|
27
|
+
const shown = needsAuth.filter((n) => !dismissed.includes(n));
|
|
28
|
+
|
|
29
|
+
const hide = () => {
|
|
30
|
+
dismissMcpSignIn(shown);
|
|
31
|
+
toast.info("Sign-in bar hidden", {
|
|
32
|
+
description: "You can still sign in to these MCP servers from AI Resources → MCP. The bar comes back only for a server that is not on this list.",
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// The dialog stays at the same place in the tree whether or not the bar shows: a
|
|
37
|
+
// successful sign-in empties `needsAuth` while the dialog is still open, and remounting
|
|
38
|
+
// it there would start a second sign-in.
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{shown.length > 0 && (
|
|
42
|
+
<div className="flex shrink-0 items-center gap-2 border-t border-border bg-surface-elevated/60 px-2 py-1.5 text-xs">
|
|
43
|
+
<KeyRound className="size-3.5 shrink-0 text-warning" />
|
|
44
|
+
<span className="shrink-0 text-text-secondary">
|
|
45
|
+
{shown.length === 1 ? "MCP server needs sign-in:" : "MCP servers need sign-in:"}
|
|
46
|
+
</span>
|
|
47
|
+
<div className="flex min-w-0 flex-1 gap-1.5 overflow-x-auto">
|
|
48
|
+
{shown.map((name) => (
|
|
49
|
+
<button
|
|
50
|
+
key={name}
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={() => setSigningIn(name)}
|
|
53
|
+
className="shrink-0 rounded border border-border bg-background px-2.5 py-1.5 font-medium text-text-primary transition-colors hover:bg-surface max-md:min-h-9"
|
|
54
|
+
title={`Sign in to ${name}`}
|
|
55
|
+
>
|
|
56
|
+
{shortMcpServerName(name)}
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={hide}
|
|
63
|
+
className="flex size-8 shrink-0 items-center justify-center rounded text-text-secondary transition-colors hover:bg-surface hover:text-text-primary max-md:size-10"
|
|
64
|
+
aria-label="Hide these servers"
|
|
65
|
+
title="Hide these servers"
|
|
66
|
+
>
|
|
67
|
+
<X className="size-3.5" />
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
<McpSignInDialog serverName={signingIn} project={projectName} onClose={() => setSigningIn(null)} />
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|