@hienlh/ppm 0.23.11 → 0.23.12
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 +18 -0
- package/CLAUDE.md +13 -2
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +37 -1
- package/bun.lock +34 -0
- package/dist/web/assets/accounts-settings-section-CGe5ikvs.js +1 -0
- package/dist/web/assets/accounts-settings-section-CGe5ikvs.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-CGe5ikvs.js.gz +0 -0
- package/dist/web/assets/{agent-session-sheet-CBMsnl_p.js → agent-session-sheet-vBhfHYrC.js} +2 -2
- package/dist/web/assets/agent-session-sheet-vBhfHYrC.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-vBhfHYrC.js.gz +0 -0
- package/dist/web/assets/{agent-session-window-content-D9C9FA7E.js → agent-session-window-content-BxIxDN5i.js} +1 -1
- package/dist/web/assets/agent-session-window-content-BxIxDN5i.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-BxIxDN5i.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-DhUVgDEG.js → ai-resource-editor-Y8clBPRv.js} +1 -1
- package/dist/web/assets/ai-resource-editor-Y8clBPRv.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-Y8clBPRv.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-DOckWvp4.js → ai-settings-section-BB8vscte.js} +1 -1
- package/dist/web/assets/{ai-settings-section-CETpSpYd.js → ai-settings-section-Ck8uN_Bw.js} +1 -1
- package/dist/web/assets/ai-settings-section-Ck8uN_Bw.js.br +0 -0
- package/dist/web/assets/ai-settings-section-Ck8uN_Bw.js.gz +0 -0
- package/dist/web/assets/android-tab-BIOUVVoW.js +2 -0
- package/dist/web/assets/android-tab-BIOUVVoW.js.br +0 -0
- package/dist/web/assets/android-tab-BIOUVVoW.js.gz +0 -0
- package/dist/web/assets/{api-settings-JIgKfcCx.js → api-settings-D1axn9vm.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-BJUiLUb7.js → appearance-settings-section-Bv5_n9lH.js} +1 -1
- package/dist/web/assets/appearance-settings-section-Bv5_n9lH.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-Bv5_n9lH.js.gz +0 -0
- package/dist/web/assets/{apps-panel-1VElRgV3.js → apps-panel-DXHF0QEv.js} +1 -1
- package/dist/web/assets/apps-panel-DXHF0QEv.js.br +0 -0
- package/dist/web/assets/apps-panel-DXHF0QEv.js.gz +0 -0
- package/dist/web/assets/{audio-preview-DYkyHu5X.js → audio-preview-DpjOKEm4.js} +1 -1
- package/dist/web/assets/audio-preview-DpjOKEm4.js.br +0 -0
- package/dist/web/assets/audio-preview-DpjOKEm4.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-4dgMNadQ.js +1 -0
- package/dist/web/assets/branch-review-tab-4dgMNadQ.js.br +0 -0
- package/dist/web/assets/branch-review-tab-4dgMNadQ.js.gz +0 -0
- package/dist/web/assets/chat-tab-Bbe_77oJ.js +15 -0
- package/dist/web/assets/chat-tab-Bbe_77oJ.js.br +0 -0
- package/dist/web/assets/chat-tab-Bbe_77oJ.js.gz +0 -0
- package/dist/web/assets/chat-tab-D3i5O3dO.js +1 -0
- package/dist/web/assets/chat-tab-D3i5O3dO.js.br +0 -0
- package/dist/web/assets/chat-tab-D3i5O3dO.js.gz +0 -0
- package/dist/web/assets/code-editor-DXsIWoQ1.js +7 -0
- package/dist/web/assets/code-editor-DXsIWoQ1.js.br +0 -0
- package/dist/web/assets/code-editor-DXsIWoQ1.js.gz +0 -0
- package/dist/web/assets/{codex-reset-credit-button-Fr45FUxi.js → codex-reset-credit-button-DRhievFQ.js} +1 -1
- package/dist/web/assets/codex-reset-credit-button-DRhievFQ.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-DRhievFQ.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-B-wdjSXI.js → conflict-editor-D_RImo_V.js} +3 -3
- package/dist/web/assets/conflict-editor-D_RImo_V.js.br +0 -0
- package/dist/web/assets/conflict-editor-D_RImo_V.js.gz +0 -0
- package/dist/web/assets/{csv-preview-B6RodObB.js → csv-preview-BuFdfLo3.js} +1 -1
- package/dist/web/assets/csv-preview-BuFdfLo3.js.br +0 -0
- package/dist/web/assets/csv-preview-BuFdfLo3.js.gz +0 -0
- package/dist/web/assets/{database-viewer-CdPBUQPY.js → database-viewer-ChcbYn9E.js} +1 -1
- package/dist/web/assets/database-viewer-ChcbYn9E.js.br +0 -0
- package/dist/web/assets/database-viewer-ChcbYn9E.js.gz +0 -0
- package/dist/web/assets/{design-settings-section-BpCpaBwL.js → design-settings-section-B_xOf51h.js} +1 -1
- package/dist/web/assets/design-settings-section-B_xOf51h.js.br +0 -0
- package/dist/web/assets/design-settings-section-B_xOf51h.js.gz +0 -0
- package/dist/web/assets/{design-skill-suggestion-DkT2E2IV.js → design-skill-suggestion-BbEDg15Z.js} +1 -1
- package/dist/web/assets/design-skill-suggestion-BbEDg15Z.js.br +0 -0
- package/dist/web/assets/design-skill-suggestion-BbEDg15Z.js.gz +0 -0
- package/dist/web/assets/{design-tab-BZb9eemI.js → design-tab-BQSYJ4cS.js} +6 -6
- package/dist/web/assets/design-tab-BQSYJ4cS.js.br +0 -0
- package/dist/web/assets/design-tab-BQSYJ4cS.js.gz +0 -0
- package/dist/web/assets/{designs-sidebar-panel-CF4tp6j-.js → designs-sidebar-panel-AiPaYfSU.js} +1 -1
- package/dist/web/assets/designs-sidebar-panel-AiPaYfSU.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-AiPaYfSU.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-BqEqwSw6.js → diff-viewer-BZ6glGK-.js} +1 -1
- package/dist/web/assets/diff-viewer-BZ6glGK-.js.br +0 -0
- package/dist/web/assets/diff-viewer-BZ6glGK-.js.gz +0 -0
- package/dist/web/assets/{edit-diff-preview-DD4HayXY.js → edit-diff-preview-D-FNLOU2.js} +1 -1
- package/dist/web/assets/edit-diff-preview-D-FNLOU2.js.br +0 -0
- package/dist/web/assets/edit-diff-preview-D-FNLOU2.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-CkA5sEdV.js → editor-language-service-4sbsdqBV.js} +1 -1
- package/dist/web/assets/editor-language-service-4sbsdqBV.js.br +0 -0
- package/dist/web/assets/editor-language-service-4sbsdqBV.js.gz +0 -0
- package/dist/web/assets/explorer-body-DbgrG1fe.js +2 -0
- package/dist/web/assets/explorer-body-DbgrG1fe.js.br +0 -0
- package/dist/web/assets/explorer-body-DbgrG1fe.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-CRJ2eMZ4.js → explorer-window-content-BCjWvHpp.js} +1 -1
- package/dist/web/assets/explorer-window-content-BCjWvHpp.js.br +0 -0
- package/dist/web/assets/explorer-window-content-BCjWvHpp.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-DwQTdCII.js → extension-manager-section-DiMF4RnT.js} +1 -1
- package/dist/web/assets/extension-manager-section-DiMF4RnT.js.br +0 -0
- package/dist/web/assets/extension-manager-section-DiMF4RnT.js.gz +0 -0
- package/dist/web/assets/{extension-webview-CMq-VTLu.js → extension-webview-Coti6Vfw.js} +1 -1
- package/dist/web/assets/extension-webview-Coti6Vfw.js.br +0 -0
- package/dist/web/assets/extension-webview-Coti6Vfw.js.gz +0 -0
- package/dist/web/assets/file-store-Cm7-pHux.js +1 -0
- package/dist/web/assets/file-store-Cm7-pHux.js.br +0 -0
- package/dist/web/assets/file-store-Cm7-pHux.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-B2lcyh-6.js → files-settings-section-ZTVwOwx0.js} +1 -1
- package/dist/web/assets/files-settings-section-ZTVwOwx0.js.br +0 -0
- package/dist/web/assets/files-settings-section-ZTVwOwx0.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-Doepm2sH.js → general-settings-section-B7Hjx_fb.js} +1 -1
- package/dist/web/assets/general-settings-section-B7Hjx_fb.js.br +0 -0
- package/dist/web/assets/general-settings-section-B7Hjx_fb.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-CJakNMDc.js → git-log-panel-CbcJ_YEH.js} +1 -1
- package/dist/web/assets/git-log-panel-CbcJ_YEH.js.br +0 -0
- package/dist/web/assets/git-log-panel-CbcJ_YEH.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-C_SeOhSq.js → glide-data-grid-BfByGSf1.js} +3 -3
- package/dist/web/assets/glide-data-grid-BfByGSf1.js.br +0 -0
- package/dist/web/assets/glide-data-grid-BfByGSf1.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-DCo9OP3D.js +1 -0
- package/dist/web/assets/group-chat-tab-DCo9OP3D.js.br +0 -0
- package/dist/web/assets/group-chat-tab-DCo9OP3D.js.gz +0 -0
- package/dist/web/assets/{image-preview-C1Frlx22.js → image-preview-Cjlps7Pt.js} +1 -1
- package/dist/web/assets/index-CyDLBddf.js +52 -0
- package/dist/web/assets/index-CyDLBddf.js.br +0 -0
- package/dist/web/assets/index-CyDLBddf.js.gz +0 -0
- package/dist/web/assets/index-Dh5apC3I.css +1 -0
- package/dist/web/assets/index-Dh5apC3I.css.br +0 -0
- package/dist/web/assets/index-Dh5apC3I.css.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-BmkIGQFV.js → jira-watcher-section-Dz_-sUC2.js} +1 -1
- package/dist/web/assets/keybindings-store-BTq5Z7XP.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BgZe3QDr.js → keyboard-shortcuts-section-Ba5NYwlG.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-Ba5NYwlG.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-Ba5NYwlG.js.gz +0 -0
- package/dist/web/assets/{language-servers-section-CCMzgOaE.js → language-servers-section-DJJ7SFXo.js} +2 -2
- package/dist/web/assets/language-servers-section-DJJ7SFXo.js.br +0 -0
- package/dist/web/assets/language-servers-section-DJJ7SFXo.js.gz +0 -0
- package/dist/web/assets/{lsp-client-B10suuaZ.js → lsp-client-Dr3vQ1bb.js} +1 -1
- package/dist/web/assets/lsp-client-Dr3vQ1bb.js.br +0 -0
- package/dist/web/assets/lsp-client-Dr3vQ1bb.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-AHHFS3jc.js → markdown-renderer-CmnqFfTU.js} +3 -3
- package/dist/web/assets/markdown-renderer-CmnqFfTU.js.br +0 -0
- package/dist/web/assets/markdown-renderer-CmnqFfTU.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-LJdVMbGl.js +1 -0
- package/dist/web/assets/{mobile-explorer-sheet-CqCsLWSr.js → mobile-explorer-sheet-BiIhZtJ2.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BiIhZtJ2.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BiIhZtJ2.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-DI_hxvFV.js +1 -0
- package/dist/web/assets/new-design-dialog-DI_hxvFV.js.br +0 -0
- package/dist/web/assets/new-design-dialog-DI_hxvFV.js.gz +0 -0
- package/dist/web/assets/notification-store-9lBSplOV.js +1 -0
- package/dist/web/assets/{notifications-settings-section-CXgzH_bK.js → notifications-settings-section-0s-2OKRv.js} +1 -1
- package/dist/web/assets/notifications-settings-section-0s-2OKRv.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-0s-2OKRv.js.gz +0 -0
- package/dist/web/assets/{overview-panel-BYB8mgRE.js → overview-panel-3htxfZS2.js} +1 -1
- package/dist/web/assets/overview-panel-3htxfZS2.js.br +0 -0
- package/dist/web/assets/overview-panel-3htxfZS2.js.gz +0 -0
- package/dist/web/assets/panel-store-B6PoCwBA.js +1 -0
- package/dist/web/assets/panel-store-B6PoCwBA.js.br +0 -0
- package/dist/web/assets/panel-store-B6PoCwBA.js.gz +0 -0
- package/dist/web/assets/{performance-panel-Dpuq3g0W.js → performance-panel-V8gpnnf7.js} +2 -2
- package/dist/web/assets/performance-panel-V8gpnnf7.js.br +0 -0
- package/dist/web/assets/performance-panel-V8gpnnf7.js.gz +0 -0
- package/dist/web/assets/{postgres-viewer-DW0Gkmow.js → postgres-viewer-sWS-ogTC.js} +1 -1
- package/dist/web/assets/postgres-viewer-sWS-ogTC.js.br +0 -0
- package/dist/web/assets/postgres-viewer-sWS-ogTC.js.gz +0 -0
- package/dist/web/assets/{ppmbot-settings-section-DFtlGoW6.js → ppmbot-settings-section-CT8vvlzk.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-CT8vvlzk.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-CT8vvlzk.js.gz +0 -0
- package/dist/web/assets/{pptx-export-CNyG32Xr.js → pptx-export-Ugjv0hso.js} +4 -4
- package/dist/web/assets/pptx-export-Ugjv0hso.js.br +0 -0
- package/dist/web/assets/pptx-export-Ugjv0hso.js.gz +0 -0
- package/dist/web/assets/{problems-panel-V5zqpuXq.js → problems-panel-Gcy79pSS.js} +1 -1
- package/dist/web/assets/problems-panel-Gcy79pSS.js.br +0 -0
- package/dist/web/assets/problems-panel-Gcy79pSS.js.gz +0 -0
- package/dist/web/assets/{process-row-format-CiJJSf_V.js → process-row-format-u6y7lOee.js} +1 -1
- package/dist/web/assets/process-table-BUTIp9Oy.js +1 -0
- package/dist/web/assets/process-table-BUTIp9Oy.js.br +0 -0
- package/dist/web/assets/process-table-BUTIp9Oy.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-B7AVlTfY.js → proxy-settings-section-Buv_8y54.js} +1 -1
- package/dist/web/assets/proxy-settings-section-Buv_8y54.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-Buv_8y54.js.gz +0 -0
- package/dist/web/assets/{query-audit-section-gHmRfWKr.js → query-audit-section-N2DQtgJx.js} +1 -1
- package/dist/web/assets/query-audit-section-N2DQtgJx.js.br +0 -0
- package/dist/web/assets/query-audit-section-N2DQtgJx.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js +1 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js +1 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js.gz +0 -0
- package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js +1 -0
- package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js.br +0 -0
- package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js.gz +0 -0
- package/dist/web/assets/{run-in-terminal-DKBcEGVu.js → run-in-terminal-DCBNtYHQ.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-sYDAsBMO.js → schedules-settings-section-_cHX2qOa.js} +1 -1
- package/dist/web/assets/schedules-settings-section-_cHX2qOa.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-_cHX2qOa.js.gz +0 -0
- package/dist/web/assets/{send-to-chat-CCdOrKc7.js → send-to-chat-dNqNV7Y4.js} +1 -1
- package/dist/web/assets/send-to-chat-dNqNV7Y4.js.br +0 -0
- package/dist/web/assets/send-to-chat-dNqNV7Y4.js.gz +0 -0
- package/dist/web/assets/{services-panel-CKa2aP1d.js → services-panel-Dm54cnW3.js} +1 -1
- package/dist/web/assets/services-panel-Dm54cnW3.js.br +0 -0
- package/dist/web/assets/services-panel-Dm54cnW3.js.gz +0 -0
- package/dist/web/assets/settings-body-DRRQNaDA.js +1 -0
- package/dist/web/assets/settings-body-DRRQNaDA.js.br +0 -0
- package/dist/web/assets/settings-body-DRRQNaDA.js.gz +0 -0
- package/dist/web/assets/settings-section-content-DRaHfvm8.js +1 -0
- package/dist/web/assets/settings-section-content-U3mIx6hF.js +2 -0
- package/dist/web/assets/settings-section-content-U3mIx6hF.js.br +0 -0
- package/dist/web/assets/settings-section-content-U3mIx6hF.js.gz +0 -0
- package/dist/web/assets/{settings-store-DfuiLq_9.js → settings-store-Cyuo8ceh.js} +2 -2
- package/dist/web/assets/settings-store-Cyuo8ceh.js.br +0 -0
- package/dist/web/assets/settings-store-Cyuo8ceh.js.gz +0 -0
- package/dist/web/assets/settings-tab-CB_BhHTK.js +1 -0
- package/dist/web/assets/settings-window-content-CgZIMNX8.js +1 -0
- package/dist/web/assets/{slash-command-picker-CLevA_Gf.js → slash-command-picker-CQlIqMW0.js} +1 -1
- package/dist/web/assets/slash-command-picker-CQlIqMW0.js.br +0 -0
- package/dist/web/assets/slash-command-picker-CQlIqMW0.js.gz +0 -0
- package/dist/web/assets/{sql-query-editor-Db8b0jEr.js → sql-query-editor--Z4bcqd8.js} +1 -1
- package/dist/web/assets/sql-query-editor--Z4bcqd8.js.br +0 -0
- package/dist/web/assets/sql-query-editor--Z4bcqd8.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-VzYLMqkD.js → sqlite-viewer-CSmX3qaj.js} +1 -1
- package/dist/web/assets/sqlite-viewer-CSmX3qaj.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-CSmX3qaj.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-BR60W_mG.js → system-monitor-body-LannpU6H.js} +2 -2
- package/dist/web/assets/system-monitor-body-LannpU6H.js.br +0 -0
- package/dist/web/assets/system-monitor-body-LannpU6H.js.gz +0 -0
- package/dist/web/assets/system-monitor-tab-BfVucwEF.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-BLw1QBXo.js → system-monitor-window-content-kLNW9Ln3.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-BL61SB5m.js → tab-host-window-content-II_FtOYM.js} +1 -1
- package/dist/web/assets/tab-host-window-content-II_FtOYM.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-II_FtOYM.js.gz +0 -0
- package/dist/web/assets/{tab-store-CbPh8CAX.js → tab-store-DCpc0g0Q.js} +1 -1
- package/dist/web/assets/{terminal-tab-B4y6at4h.js → terminal-tab-CTe2polt.js} +1 -1
- package/dist/web/assets/terminal-tab-CTe2polt.js.br +0 -0
- package/dist/web/assets/terminal-tab-CTe2polt.js.gz +0 -0
- package/dist/web/assets/{tool-cards-K0r_yxI0.js → tool-cards-rxLXbBSm.js} +5 -5
- package/dist/web/assets/tool-cards-rxLXbBSm.js.br +0 -0
- package/dist/web/assets/tool-cards-rxLXbBSm.js.gz +0 -0
- package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js +1 -0
- package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js.br +0 -0
- package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-BK23icsE.js → use-inline-blame-CxZ_QWzS.js} +3 -3
- package/dist/web/assets/use-inline-blame-CxZ_QWzS.js.br +0 -0
- package/dist/web/assets/use-inline-blame-CxZ_QWzS.js.gz +0 -0
- package/dist/web/assets/{use-lsp-CRdCPCKW.js → use-lsp-gCNE5A9K.js} +1 -1
- package/dist/web/assets/use-lsp-gCNE5A9K.js.br +0 -0
- package/dist/web/assets/use-lsp-gCNE5A9K.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-CpXT2uh3.js → use-monaco-theme-DvNGcCvz.js} +1 -1
- package/dist/web/assets/{use-process-kill-BWYX03KF.js → use-process-kill-Cbiu1mbY.js} +1 -1
- package/dist/web/assets/use-process-kill-Cbiu1mbY.js.br +0 -0
- package/dist/web/assets/use-process-kill-Cbiu1mbY.js.gz +0 -0
- package/dist/web/assets/{use-websocket-CiWQKhHr.js → use-websocket-AC7EG1LK.js} +1 -1
- package/dist/web/assets/video-preview-C2Y61WUE.js +1 -0
- package/dist/web/assets/video-preview-C2Y61WUE.js.br +0 -0
- package/dist/web/assets/video-preview-C2Y61WUE.js.gz +0 -0
- package/dist/web/assets/{voice-settings-section-Ceihggqf.js → voice-settings-section-DCXZeLYI.js} +1 -1
- package/dist/web/assets/voice-settings-section-DCXZeLYI.js.br +0 -0
- package/dist/web/assets/voice-settings-section-DCXZeLYI.js.gz +0 -0
- package/dist/web/assets/{window-pip-registry-mDgwpAwP.js → window-pip-registry-9Y9LQQYS.js} +1 -1
- package/dist/web/index.html +50 -7
- 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/architecture/android-emulator-phase0-report-260921.md +310 -0
- package/docs/architecture/android-emulator-phase2-report-260921.md +163 -0
- package/docs/architecture/android-emulator-phase3-report-260921.md +203 -0
- package/docs/architecture/android-emulator-phase4-report-260921.md +191 -0
- package/docs/architecture/android-emulator-plan-260920.md +376 -0
- package/docs/architecture/plugins-and-tracing.md +48 -7
- package/package.json +4 -2
- package/scripts/gen-android-proto.ts +37 -0
- package/scripts/release.sh +3 -1
- package/src/cli/commands/chat-cmd.ts +2 -2
- package/src/providers/claude-agent-sdk-query-options.ts +28 -0
- package/src/providers/claude-agent-sdk.ts +153 -71
- package/src/providers/claude-warm-spare.ts +176 -0
- package/src/providers/provider.interface.ts +1 -0
- package/src/server/index.ts +53 -0
- package/src/server/middleware/gzip-json.ts +13 -1
- package/src/server/routes/android.ts +616 -0
- package/src/server/routes/chat.ts +31 -0
- package/src/server/routes/files.ts +52 -5
- package/src/server/routes/remote-desktop.ts +95 -2
- package/src/server/routes/trace.ts +66 -0
- package/src/server/ws/android.ts +101 -0
- package/src/server/ws/chat.ts +12 -17
- package/src/server/ws/global.ts +13 -1
- package/src/server/ws/remote-desktop.ts +6 -1
- package/src/services/android/android-apk.ts +387 -0
- package/src/services/android/android-capabilities.ts +154 -0
- package/src/services/android/android-flag.ts +12 -0
- package/src/services/android/android-grpc.ts +124 -0
- package/src/services/android/android-input.ts +129 -0
- package/src/services/android/android-logcat.ts +169 -0
- package/src/services/android/android-nonce.ts +50 -0
- package/src/services/android/android-operations.ts +124 -0
- package/src/services/android/android-screenshot.ts +53 -0
- package/src/services/android/android-session.ts +510 -0
- package/src/services/android/android-video.ts +495 -0
- package/src/services/android/avd-list.ts +131 -0
- package/src/services/android/avd-manager.ts +397 -0
- package/src/services/android/device-registry.ts +223 -0
- package/src/services/android/emulator-discovery.ts +160 -0
- package/src/services/android/emulator-launcher.ts +290 -0
- package/src/services/android/proto/emulator-controller-descriptor.ts +10 -0
- package/src/services/android/proto/emulator_controller.proto +1791 -0
- package/src/services/android/sdk-discovery.ts +178 -0
- package/src/services/android/system-images.ts +124 -0
- package/src/services/chat.service.ts +81 -4
- package/src/services/config.service.ts +1 -1
- package/src/services/file-filter.service.ts +2 -61
- package/src/services/file-glob.ts +67 -0
- package/src/services/file-index/file-index-runner.ts +169 -0
- package/src/services/file-index/file-index-worker.ts +73 -0
- package/src/services/file-index/index-search.ts +152 -0
- package/src/services/file-index/index-walk.ts +109 -0
- package/src/services/file-list-index.service.ts +189 -70
- package/src/services/file-watcher/linux-inotify.ts +238 -0
- package/src/services/file-watcher/watch-tree.ts +37 -6
- package/src/services/file-watcher.service.ts +40 -8
- package/src/services/file.service.ts +17 -6
- package/src/services/group-chat/group-chat.service.ts +1 -1
- package/src/services/jira-debug-session.service.ts +1 -1
- package/src/services/ppmbot/ppmbot-delegation.ts +1 -1
- package/src/services/ppmbot/ppmbot-service.ts +1 -1
- package/src/services/project-content-search.service.ts +45 -11
- package/src/services/proxy-agent-turn.ts +7 -3
- package/src/services/remote-desktop/mediamtx-catalog.ts +89 -0
- package/src/services/remote-desktop/mediamtx-config.ts +91 -0
- package/src/services/remote-desktop/mediamtx-install.service.ts +191 -0
- package/src/services/remote-desktop/mediamtx-paths.ts +51 -0
- package/src/services/remote-desktop/mediamtx-process.ts +152 -0
- package/src/services/remote-desktop/remote-desktop-capture-wayland.ts +312 -0
- package/src/services/remote-desktop/remote-desktop-capture.ts +60 -4
- package/src/services/remote-desktop/remote-desktop-portal.ts +529 -0
- package/src/services/remote-desktop/remote-desktop-requirements.ts +56 -20
- package/src/services/remote-desktop/remote-desktop-session.ts +77 -3
- package/src/services/remote-desktop/remote-desktop-whep-registry.ts +36 -0
- package/src/services/scheduler-runner.ts +2 -3
- package/src/services/session-trace/browser-trace-ingest.ts +131 -0
- package/src/services/session-trace/session-trace-cleanup.ts +65 -0
- package/src/services/session-trace/session-trace-db.ts +89 -0
- package/src/services/session-trace/session-trace-store.ts +132 -0
- package/src/services/session-trace/trace-coalescer.ts +86 -0
- package/src/services/session-trace/trace-recorder.ts +304 -0
- package/src/services/session-trace/trace-writer.ts +83 -0
- package/src/shared/android-avd.ts +53 -0
- package/src/shared/android-protocol.ts +184 -0
- package/src/shared/file-index-limits.ts +17 -0
- package/src/shared/session-trace.ts +61 -0
- package/src/types/chat.ts +17 -0
- package/src/types/config.ts +27 -0
- package/src/types/project.ts +9 -0
- package/src/web/app.tsx +5 -0
- package/src/web/components/android/android-apk-upload.ts +75 -0
- package/src/web/components/android/android-avd-create-dialog.tsx +305 -0
- package/src/web/components/android/android-avd-destroy-dialog.tsx +151 -0
- package/src/web/components/android/android-clipboard-panel.tsx +94 -0
- package/src/web/components/android/android-controls.tsx +128 -0
- package/src/web/components/android/android-coords.ts +99 -0
- package/src/web/components/android/android-device-list.tsx +329 -0
- package/src/web/components/android/android-install-panel.tsx +280 -0
- package/src/web/components/android/android-log-filter.ts +75 -0
- package/src/web/components/android/android-logcat-panel.tsx +214 -0
- package/src/web/components/android/android-screenshot-download.ts +56 -0
- package/src/web/components/android/android-tab.tsx +66 -0
- package/src/web/components/android/android-tools-drawer.tsx +76 -0
- package/src/web/components/android/android-viewer.tsx +200 -0
- package/src/web/components/android/android-ws-url.ts +21 -0
- package/src/web/components/android/open-android.ts +21 -0
- package/src/web/components/android/use-android-available.ts +29 -0
- package/src/web/components/android/use-android-keyboard.ts +111 -0
- package/src/web/components/android/use-android-session.ts +277 -0
- package/src/web/components/android/use-android-touch.ts +123 -0
- package/src/web/components/chat/chat-tab.tsx +28 -2
- package/src/web/components/chat/message-input.tsx +3 -5
- package/src/web/components/editor/compare-picker.tsx +12 -2
- package/src/web/components/explorer/file-tree.tsx +6 -7
- package/src/web/components/explorer/use-tree-row-dnd.ts +0 -2
- package/src/web/components/floating-window/window-content-registry.ts +8 -8
- package/src/web/components/layout/command-palette.tsx +31 -9
- package/src/web/components/layout/mobile-drawer.tsx +8 -1
- package/src/web/components/layout/nav-section-rail.tsx +8 -1
- package/src/web/components/layout/reparenting-tab.tsx +2 -2
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/tab-pool.tsx +31 -20
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +23 -1
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +64 -6
- package/src/web/components/remote-desktop/use-webrtc-canvas-video.ts +255 -0
- package/src/web/components/root-error-boundary.tsx +5 -0
- package/src/web/components/settings/remote-desktop-settings-section.tsx +176 -0
- package/src/web/components/settings/settings-categories.ts +3 -0
- package/src/web/components/settings/settings-section-content.tsx +25 -18
- package/src/web/hooks/use-chat-prewarm.ts +53 -0
- package/src/web/hooks/use-chat.ts +18 -4
- package/src/web/hooks/use-file-index-invalidation.ts +7 -5
- package/src/web/hooks/use-global-events.ts +12 -1
- package/src/web/hooks/use-remote-file-search.ts +39 -0
- package/src/web/hooks/use-ui-preload.ts +38 -0
- package/src/web/index.html +43 -0
- package/src/web/lib/chunk-recovery.ts +5 -1
- package/src/web/lib/device-id.ts +39 -0
- package/src/web/lib/feature-badges.ts +2 -0
- package/src/web/lib/lazy-with-preload.tsx +43 -0
- package/src/web/lib/preload-ui.ts +60 -0
- package/src/web/lib/score-file-search.ts +27 -14
- package/src/web/lib/tab-type-icons.ts +2 -1
- package/src/web/lib/trace-boot.ts +8 -0
- package/src/web/lib/trace-client.ts +516 -0
- package/src/web/main.tsx +12 -0
- package/src/web/stores/file-store.ts +124 -18
- package/src/web/stores/panel-utils.ts +3 -0
- package/src/web/stores/tab-store.ts +2 -1
- package/src/web/stores/window-panel-persistence.ts +3 -0
- package/vite.config.ts +3 -0
- package/dist/web/assets/accounts-settings-section-DEIm-W-D.js +0 -1
- package/dist/web/assets/accounts-settings-section-DEIm-W-D.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-DEIm-W-D.js.gz +0 -0
- package/dist/web/assets/agent-session-sheet-CBMsnl_p.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-CBMsnl_p.js.gz +0 -0
- package/dist/web/assets/agent-session-window-content-D9C9FA7E.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-D9C9FA7E.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-DhUVgDEG.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-DhUVgDEG.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-CETpSpYd.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CETpSpYd.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-BJUiLUb7.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-BJUiLUb7.js.gz +0 -0
- package/dist/web/assets/apps-panel-1VElRgV3.js.br +0 -0
- package/dist/web/assets/apps-panel-1VElRgV3.js.gz +0 -0
- package/dist/web/assets/audio-preview-DYkyHu5X.js.br +0 -0
- package/dist/web/assets/audio-preview-DYkyHu5X.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-DhTMSpSU.js +0 -1
- package/dist/web/assets/branch-review-tab-DhTMSpSU.js.br +0 -0
- package/dist/web/assets/branch-review-tab-DhTMSpSU.js.gz +0 -0
- package/dist/web/assets/chat-tab-Cg3BKugo.js +0 -15
- package/dist/web/assets/chat-tab-Cg3BKugo.js.br +0 -0
- package/dist/web/assets/chat-tab-Cg3BKugo.js.gz +0 -0
- package/dist/web/assets/chat-tab-tbUDK__Y.js +0 -1
- package/dist/web/assets/chat-tab-tbUDK__Y.js.br +0 -0
- package/dist/web/assets/chat-tab-tbUDK__Y.js.gz +0 -0
- package/dist/web/assets/code-editor-6ZiQehbr.js +0 -7
- package/dist/web/assets/code-editor-6ZiQehbr.js.br +0 -0
- package/dist/web/assets/code-editor-6ZiQehbr.js.gz +0 -0
- package/dist/web/assets/codex-reset-credit-button-Fr45FUxi.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-Fr45FUxi.js.gz +0 -0
- package/dist/web/assets/conflict-editor-B-wdjSXI.js.br +0 -0
- package/dist/web/assets/conflict-editor-B-wdjSXI.js.gz +0 -0
- package/dist/web/assets/csv-preview-B6RodObB.js.br +0 -0
- package/dist/web/assets/csv-preview-B6RodObB.js.gz +0 -0
- package/dist/web/assets/database-viewer-CdPBUQPY.js.br +0 -0
- package/dist/web/assets/database-viewer-CdPBUQPY.js.gz +0 -0
- package/dist/web/assets/design-settings-section-BpCpaBwL.js.br +0 -0
- package/dist/web/assets/design-settings-section-BpCpaBwL.js.gz +0 -0
- package/dist/web/assets/design-skill-suggestion-DkT2E2IV.js.br +0 -0
- package/dist/web/assets/design-skill-suggestion-DkT2E2IV.js.gz +0 -0
- package/dist/web/assets/design-tab-BZb9eemI.js.br +0 -0
- package/dist/web/assets/design-tab-BZb9eemI.js.gz +0 -0
- package/dist/web/assets/designs-sidebar-panel-CF4tp6j-.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-CF4tp6j-.js.gz +0 -0
- package/dist/web/assets/diff-viewer-BqEqwSw6.js.br +0 -0
- package/dist/web/assets/diff-viewer-BqEqwSw6.js.gz +0 -0
- package/dist/web/assets/edit-diff-preview-DD4HayXY.js.br +0 -0
- package/dist/web/assets/edit-diff-preview-DD4HayXY.js.gz +0 -0
- package/dist/web/assets/editor-language-service-CkA5sEdV.js.br +0 -0
- package/dist/web/assets/editor-language-service-CkA5sEdV.js.gz +0 -0
- package/dist/web/assets/explorer-body-Ds7oKuz7.js +0 -2
- package/dist/web/assets/explorer-body-Ds7oKuz7.js.br +0 -0
- package/dist/web/assets/explorer-body-Ds7oKuz7.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-CRJ2eMZ4.js.br +0 -0
- package/dist/web/assets/explorer-window-content-CRJ2eMZ4.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-DwQTdCII.js.br +0 -0
- package/dist/web/assets/extension-manager-section-DwQTdCII.js.gz +0 -0
- package/dist/web/assets/extension-webview-CMq-VTLu.js.br +0 -0
- package/dist/web/assets/extension-webview-CMq-VTLu.js.gz +0 -0
- package/dist/web/assets/file-store-DY1_0nzY.js +0 -1
- package/dist/web/assets/file-store-DY1_0nzY.js.br +0 -0
- package/dist/web/assets/file-store-DY1_0nzY.js.gz +0 -0
- package/dist/web/assets/files-settings-section-B2lcyh-6.js.br +0 -0
- package/dist/web/assets/files-settings-section-B2lcyh-6.js.gz +0 -0
- package/dist/web/assets/general-settings-section-Doepm2sH.js.br +0 -0
- package/dist/web/assets/general-settings-section-Doepm2sH.js.gz +0 -0
- package/dist/web/assets/git-log-panel-CJakNMDc.js.br +0 -0
- package/dist/web/assets/git-log-panel-CJakNMDc.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-C_SeOhSq.js.br +0 -0
- package/dist/web/assets/glide-data-grid-C_SeOhSq.js.gz +0 -0
- package/dist/web/assets/group-chat-tab--by1xCQW.js +0 -1
- package/dist/web/assets/group-chat-tab--by1xCQW.js.br +0 -0
- package/dist/web/assets/group-chat-tab--by1xCQW.js.gz +0 -0
- package/dist/web/assets/index-DhWjx5iR.js +0 -52
- package/dist/web/assets/index-DhWjx5iR.js.br +0 -0
- package/dist/web/assets/index-DhWjx5iR.js.gz +0 -0
- package/dist/web/assets/index-oIvYSKTT.css +0 -1
- package/dist/web/assets/index-oIvYSKTT.css.br +0 -0
- package/dist/web/assets/index-oIvYSKTT.css.gz +0 -0
- package/dist/web/assets/keybindings-store-30LYZU0l.js +0 -1
- package/dist/web/assets/keyboard-shortcuts-section-BgZe3QDr.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-BgZe3QDr.js.gz +0 -0
- package/dist/web/assets/language-servers-section-CCMzgOaE.js.br +0 -0
- package/dist/web/assets/language-servers-section-CCMzgOaE.js.gz +0 -0
- package/dist/web/assets/lsp-client-B10suuaZ.js.br +0 -0
- package/dist/web/assets/lsp-client-B10suuaZ.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-AHHFS3jc.js.br +0 -0
- package/dist/web/assets/markdown-renderer-AHHFS3jc.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-DxnXtBoj.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-CqCsLWSr.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-CqCsLWSr.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-B9tgnQXq.js +0 -1
- package/dist/web/assets/new-design-dialog-B9tgnQXq.js.br +0 -0
- package/dist/web/assets/new-design-dialog-B9tgnQXq.js.gz +0 -0
- package/dist/web/assets/notification-store-HUVzO_bp.js +0 -1
- package/dist/web/assets/notifications-settings-section-CXgzH_bK.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-CXgzH_bK.js.gz +0 -0
- package/dist/web/assets/overview-panel-BYB8mgRE.js.br +0 -0
- package/dist/web/assets/overview-panel-BYB8mgRE.js.gz +0 -0
- package/dist/web/assets/panel-store-CWHPmHwj.js +0 -1
- package/dist/web/assets/panel-store-CWHPmHwj.js.br +0 -0
- package/dist/web/assets/panel-store-CWHPmHwj.js.gz +0 -0
- package/dist/web/assets/performance-panel-Dpuq3g0W.js.br +0 -0
- package/dist/web/assets/performance-panel-Dpuq3g0W.js.gz +0 -0
- package/dist/web/assets/postgres-viewer-DW0Gkmow.js.br +0 -0
- package/dist/web/assets/postgres-viewer-DW0Gkmow.js.gz +0 -0
- package/dist/web/assets/ppmbot-settings-section-DFtlGoW6.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-DFtlGoW6.js.gz +0 -0
- package/dist/web/assets/pptx-export-CNyG32Xr.js.br +0 -0
- package/dist/web/assets/pptx-export-CNyG32Xr.js.gz +0 -0
- package/dist/web/assets/problems-panel-V5zqpuXq.js.br +0 -0
- package/dist/web/assets/problems-panel-V5zqpuXq.js.gz +0 -0
- package/dist/web/assets/process-table-DAj46HIl.js +0 -1
- package/dist/web/assets/process-table-DAj46HIl.js.br +0 -0
- package/dist/web/assets/process-table-DAj46HIl.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-B7AVlTfY.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-B7AVlTfY.js.gz +0 -0
- package/dist/web/assets/query-audit-section-gHmRfWKr.js.br +0 -0
- package/dist/web/assets/query-audit-section-gHmRfWKr.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js +0 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js +0 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js +0 -1
- package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js +0 -1
- package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-BnznIby3.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-BnznIby3.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-BnznIby3.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-sYDAsBMO.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-sYDAsBMO.js.gz +0 -0
- package/dist/web/assets/send-to-chat-CCdOrKc7.js.br +0 -0
- package/dist/web/assets/send-to-chat-CCdOrKc7.js.gz +0 -0
- package/dist/web/assets/services-panel-CKa2aP1d.js.br +0 -0
- package/dist/web/assets/services-panel-CKa2aP1d.js.gz +0 -0
- package/dist/web/assets/settings-body-CEIF8lkl.js +0 -2
- package/dist/web/assets/settings-body-CEIF8lkl.js.br +0 -0
- package/dist/web/assets/settings-body-CEIF8lkl.js.gz +0 -0
- package/dist/web/assets/settings-store-DfuiLq_9.js.br +0 -0
- package/dist/web/assets/settings-store-DfuiLq_9.js.gz +0 -0
- package/dist/web/assets/settings-tab-Du5AUPni.js +0 -1
- package/dist/web/assets/settings-window-content-BTflfLez.js +0 -1
- package/dist/web/assets/slash-command-picker-CLevA_Gf.js.br +0 -0
- package/dist/web/assets/slash-command-picker-CLevA_Gf.js.gz +0 -0
- package/dist/web/assets/sql-query-editor-Db8b0jEr.js.br +0 -0
- package/dist/web/assets/sql-query-editor-Db8b0jEr.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-VzYLMqkD.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-VzYLMqkD.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-BR60W_mG.js.br +0 -0
- package/dist/web/assets/system-monitor-body-BR60W_mG.js.gz +0 -0
- package/dist/web/assets/system-monitor-tab-BYpantE2.js +0 -1
- package/dist/web/assets/tab-host-window-content-BL61SB5m.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-BL61SB5m.js.gz +0 -0
- package/dist/web/assets/terminal-tab-B4y6at4h.js.br +0 -0
- package/dist/web/assets/terminal-tab-B4y6at4h.js.gz +0 -0
- package/dist/web/assets/tool-cards-K0r_yxI0.js.br +0 -0
- package/dist/web/assets/tool-cards-K0r_yxI0.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-BK23icsE.js.br +0 -0
- package/dist/web/assets/use-inline-blame-BK23icsE.js.gz +0 -0
- package/dist/web/assets/use-lsp-CRdCPCKW.js.br +0 -0
- package/dist/web/assets/use-lsp-CRdCPCKW.js.gz +0 -0
- package/dist/web/assets/use-process-kill-BWYX03KF.js.br +0 -0
- package/dist/web/assets/use-process-kill-BWYX03KF.js.gz +0 -0
- package/dist/web/assets/video-preview-DugVkMB7.js +0 -1
- package/dist/web/assets/video-preview-DugVkMB7.js.br +0 -0
- package/dist/web/assets/video-preview-DugVkMB7.js.gz +0 -0
- package/dist/web/assets/voice-settings-section-Ceihggqf.js.br +0 -0
- package/dist/web/assets/voice-settings-section-Ceihggqf.js.gz +0 -0
- /package/dist/web/assets/{__vite-browser-external-B_XMEguz.js → __vite-browser-external-C5mXParC.js} +0 -0
- /package/dist/web/assets/{agent-session-sheet-store-DO682WSg.js → agent-session-sheet-store-_OeezCXB.js} +0 -0
- /package/dist/web/assets/{api-settings-CpO8SPk9.js → api-settings-Cc_CpfmC.js} +0 -0
- /package/dist/web/assets/{api-settings-CpO8SPk9.js.br → api-settings-Cc_CpfmC.js.br} +0 -0
- /package/dist/web/assets/{api-settings-CpO8SPk9.js.gz → api-settings-Cc_CpfmC.js.gz} +0 -0
- /package/dist/web/assets/{api-themes-CJipDH_l.js → api-themes-BtMBSjuM.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js → can-open-in-ppm-DeE2-kd5.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js.br → can-open-in-ppm-DeE2-kd5.js.br} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js.gz → can-open-in-ppm-DeE2-kd5.js.gz} +0 -0
- /package/dist/web/assets/{csv-parser-DcRWxcNV.js → csv-parser-CoxPdyLw.js} +0 -0
- /package/dist/web/assets/{csv-parser-DcRWxcNV.js.br → csv-parser-CoxPdyLw.js.br} +0 -0
- /package/dist/web/assets/{csv-parser-DcRWxcNV.js.gz → csv-parser-CoxPdyLw.js.gz} +0 -0
- /package/dist/web/assets/{design-export-client-CHffN3DM.js → design-export-client-BfdZB7xK.js} +0 -0
- /package/dist/web/assets/{design-export-client-CHffN3DM.js.br → design-export-client-BfdZB7xK.js.br} +0 -0
- /package/dist/web/assets/{design-export-client-CHffN3DM.js.gz → design-export-client-BfdZB7xK.js.gz} +0 -0
- /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js → design-responsive-dialog-BiYy3ibS.js} +0 -0
- /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js.br → design-responsive-dialog-BiYy3ibS.js.br} +0 -0
- /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js.gz → design-responsive-dialog-BiYy3ibS.js.gz} +0 -0
- /package/dist/web/assets/{design-system-status-label-B_0I_3a8.js → design-system-status-label-ekiLyjKn.js} +0 -0
- /package/dist/web/assets/{docx-preview-B3eJyY2y.js → docx-preview-Dt6RQpfA.js} +0 -0
- /package/dist/web/assets/{docx-preview-B3eJyY2y.js.br → docx-preview-Dt6RQpfA.js.br} +0 -0
- /package/dist/web/assets/{docx-preview-B3eJyY2y.js.gz → docx-preview-Dt6RQpfA.js.gz} +0 -0
- /package/dist/web/assets/{editor-font-BNwo7zo5.js → editor-font-CBszn6vt.js} +0 -0
- /package/dist/web/assets/{format-bytes-CRZfxO2J.js → format-bytes-FQmpZ25A.js} +0 -0
- /package/dist/web/assets/{image-gallery-DC3gRAqk.js → image-gallery-B16hO6Ab.js} +0 -0
- /package/dist/web/assets/{image-preview-geometry-DN3DZQp0.js → image-preview-geometry-D4Ihz4UV.js} +0 -0
- /package/dist/web/assets/{lib-B9k5tn9s.js → lib-6APiJkU3.js} +0 -0
- /package/dist/web/assets/{lib-B9k5tn9s.js.br → lib-6APiJkU3.js.br} +0 -0
- /package/dist/web/assets/{lib-B9k5tn9s.js.gz → lib-6APiJkU3.js.gz} +0 -0
- /package/dist/web/assets/{libesm-5f7T4AHA.js → libesm-5wREk7JN.js} +0 -0
- /package/dist/web/assets/{libesm-5f7T4AHA.js.br → libesm-5wREk7JN.js.br} +0 -0
- /package/dist/web/assets/{libesm-5f7T4AHA.js.gz → libesm-5wREk7JN.js.gz} +0 -0
- /package/dist/web/assets/{media-url-CCGOIxzS.js → media-url-CrT-qulZ.js} +0 -0
- /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js → metric-chart-canvas-D7f-LQ8x.js} +0 -0
- /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js.br → metric-chart-canvas-D7f-LQ8x.js.br} +0 -0
- /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js.gz → metric-chart-canvas-D7f-LQ8x.js.gz} +0 -0
- /package/dist/web/assets/{monaco-dotenv-language-Bwq3gQsi.js → monaco-dotenv-language-DbSss7YR.js} +0 -0
- /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js → onboarding-entry-CGFdGLLP.js} +0 -0
- /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js.br → onboarding-entry-CGFdGLLP.js.br} +0 -0
- /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js.gz → onboarding-entry-CGFdGLLP.js.gz} +0 -0
- /package/dist/web/assets/{pdf-preview-X8bdpfE9.js → pdf-preview-B8A4T-ES.js} +0 -0
- /package/dist/web/assets/{pdf-preview-X8bdpfE9.js.br → pdf-preview-B8A4T-ES.js.br} +0 -0
- /package/dist/web/assets/{pdf-preview-X8bdpfE9.js.gz → pdf-preview-B8A4T-ES.js.gz} +0 -0
- /package/dist/web/assets/{separator-DMu7VFQa.js → separator-dHj3Km--.js} +0 -0
- /package/dist/web/assets/{sortable-header-CipIP7aH.js → sortable-header-BOa8o7NJ.js} +0 -0
- /package/dist/web/assets/{sortable-header-CipIP7aH.js.br → sortable-header-BOa8o7NJ.js.br} +0 -0
- /package/dist/web/assets/{sortable-header-CipIP7aH.js.gz → sortable-header-BOa8o7NJ.js.gz} +0 -0
- /package/dist/web/assets/{use-blob-url-DjuRQWnR.js → use-blob-url-BC69n15x.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js → vendor-xterm-CfasyE0m.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js.br → vendor-xterm-CfasyE0m.js.br} +0 -0
- /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js.gz → vendor-xterm-CfasyE0m.js.gz} +0 -0
|
@@ -16,6 +16,9 @@ import { avc1CodecString } from "./avc1-codec-string.ts";
|
|
|
16
16
|
import type { AccessUnit } from "./access-unit-assembler.ts";
|
|
17
17
|
import { injectPointer, injectKey, injectWheel, injectText, releaseAllModifiers, isInputAvailable, releaseRemoteInput } from "./remote-desktop-input.ts";
|
|
18
18
|
import { resolveDisplay, type RemoteDisplay } from "./remote-desktop-displays.ts";
|
|
19
|
+
import { startRelay, type RelayHandle } from "./mediamtx-process.ts";
|
|
20
|
+
import { findMediamtxBinary } from "./mediamtx-paths.ts";
|
|
21
|
+
import { registerWhepTarget, releaseWhepTicket } from "./remote-desktop-whep-registry.ts";
|
|
19
22
|
import {
|
|
20
23
|
MAX_CLIPBOARD_CHARS, pasteComboCodes, readHostClipboard, writeHostClipboard,
|
|
21
24
|
} from "./remote-desktop-clipboard.ts";
|
|
@@ -55,6 +58,11 @@ const FRAME_AUDIO = 2;
|
|
|
55
58
|
|
|
56
59
|
export class RemoteDesktopSession {
|
|
57
60
|
private capture: CaptureHandle | null = null;
|
|
61
|
+
/** The MediaMTX relay, when this session streams over WebRTC. Outlives individual ffmpeg
|
|
62
|
+
* respawns (a quality change restarts the publisher, not the relay). */
|
|
63
|
+
private relay: RelayHandle | null = null;
|
|
64
|
+
/** What the client presents to `POST /api/remote-desktop/whep/:ticket`. */
|
|
65
|
+
private whepTicket: string | null = null;
|
|
58
66
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
59
67
|
private lastPingAt = Date.now();
|
|
60
68
|
private droppingUntilKey = false;
|
|
@@ -116,6 +124,9 @@ export class RemoteDesktopSession {
|
|
|
116
124
|
private readonly display: RemoteDisplay | null,
|
|
117
125
|
drawMouse = true,
|
|
118
126
|
encoder: string | null = null,
|
|
127
|
+
/** Stream over WebRTC through the local relay instead of framing access units onto this
|
|
128
|
+
* socket. Only honoured when the relay is actually installed; see `beginCapture`. */
|
|
129
|
+
private readonly preferWebrtc = false,
|
|
119
130
|
) {
|
|
120
131
|
this.drawMouse = drawMouse;
|
|
121
132
|
this.encoder = encoder;
|
|
@@ -162,12 +173,17 @@ export class RemoteDesktopSession {
|
|
|
162
173
|
this.captureExited = new Promise<void>((resolve) => { resolveExit = resolve; });
|
|
163
174
|
this.sentConfig = false;
|
|
164
175
|
this.droppingUntilKey = false;
|
|
176
|
+
const publishUrl = await this.ensureRelay();
|
|
165
177
|
this.capture = await startCapture({
|
|
166
178
|
display: this.display,
|
|
167
179
|
preset: this.effectivePreset(),
|
|
168
180
|
drawMouse: this.drawMouse,
|
|
169
181
|
...(this.encoder ? { encoder: this.encoder } : {}),
|
|
170
|
-
|
|
182
|
+
// Exactly one of these. With a relay the encoded stream leaves over RTSP and this socket
|
|
183
|
+
// carries only control, so there are no access units to frame.
|
|
184
|
+
...(publishUrl
|
|
185
|
+
? { publishUrl }
|
|
186
|
+
: { onAccessUnit: (au: AccessUnit) => this.handleAccessUnit(au, generation) }),
|
|
171
187
|
// `reason` is only set when ffmpeg died on its own (crash, access denied, etc) — tell
|
|
172
188
|
// the client *why* before closing instead of leaving it to guess from a bare
|
|
173
189
|
// disconnect (this is exactly what happens today for e.g. gdigrab failing against a
|
|
@@ -184,6 +200,57 @@ export class RemoteDesktopSession {
|
|
|
184
200
|
}
|
|
185
201
|
},
|
|
186
202
|
});
|
|
203
|
+
// Announced after the publisher is spawned, but the client still has to retry: MediaMTX
|
|
204
|
+
// answers 404 until ffmpeg's first packet arrives, and a quality change respawns the
|
|
205
|
+
// publisher under the same path, which drops the browser's reader and needs a fresh
|
|
206
|
+
// handshake. One message covers both, because the path never changes.
|
|
207
|
+
if (publishUrl) this.announceWebrtc();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Bring up this session's relay, or answer null to stay on the WebSocket path.
|
|
212
|
+
*
|
|
213
|
+
* Not installed is not an error: the relay is an optional download, and a host without it
|
|
214
|
+
* must keep working exactly as before rather than failing to connect. A relay that fails to
|
|
215
|
+
* *start* is logged and also falls back, for the same reason — a viewer that shows the
|
|
216
|
+
* desktop over WebSocket beats one that shows an error.
|
|
217
|
+
*/
|
|
218
|
+
private async ensureRelay(): Promise<string | null> {
|
|
219
|
+
if (!this.preferWebrtc) return null;
|
|
220
|
+
if (this.relay && !this.relay.isStopped()) return this.relay.publishUrl;
|
|
221
|
+
if (!findMediamtxBinary()) return null;
|
|
222
|
+
try {
|
|
223
|
+
this.relay = await startRelay();
|
|
224
|
+
this.whepTicket = registerWhepTarget(this.relay.whepUrl);
|
|
225
|
+
return this.relay.publishUrl;
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.warn(`[remote-desktop] WebRTC relay unavailable, using the WebSocket path: ${(e as Error).message}`);
|
|
228
|
+
this.relay = null;
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Tell the client where to run the WHEP handshake. Only ever a PPM path — never the relay's
|
|
235
|
+
* own loopback URL, which the browser could not reach and must not learn.
|
|
236
|
+
*
|
|
237
|
+
* This carries `preset` and `encoder` too, because it replaces the `config` message on this
|
|
238
|
+
* transport: `config` is sent from `handleAccessUnit`, which never runs when the stream
|
|
239
|
+
* leaves over RTSP — so without them the quality and codec menus would show their defaults
|
|
240
|
+
* rather than what this host is actually running. The codec *string* is deliberately absent:
|
|
241
|
+
* WebRTC learns it from the SDP answer, and PPM has no SPS to derive one from here.
|
|
242
|
+
*/
|
|
243
|
+
private announceWebrtc(): void {
|
|
244
|
+
if (!this.whepTicket || this.closed) return;
|
|
245
|
+
try {
|
|
246
|
+
this.ws.send(JSON.stringify({
|
|
247
|
+
type: "webrtc",
|
|
248
|
+
whepPath: `/api/remote-desktop/whep/${this.whepTicket}`,
|
|
249
|
+
preset: this.presetId,
|
|
250
|
+
held: this.ratioScale,
|
|
251
|
+
encoder: this.encoder,
|
|
252
|
+
}));
|
|
253
|
+
} catch { /* socket closing; the session is going away anyway */ }
|
|
187
254
|
}
|
|
188
255
|
|
|
189
256
|
/** Apply a change that ffmpeg can only take at startup: stop the old process, wait for it to
|
|
@@ -566,6 +633,10 @@ export class RemoteDesktopSession {
|
|
|
566
633
|
this.closed = true;
|
|
567
634
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
568
635
|
this.capture?.stop();
|
|
636
|
+
// Relay after capture: the publisher should go first so MediaMTX sees a clean disconnect.
|
|
637
|
+
this.relay?.stop();
|
|
638
|
+
this.relay = null;
|
|
639
|
+
if (this.whepTicket) { releaseWhepTicket(this.whepTicket); this.whepTicket = null; }
|
|
569
640
|
this.audio?.stop();
|
|
570
641
|
this.audio = null;
|
|
571
642
|
// Before anything that could throw: a held grab is the only piece of state here that
|
|
@@ -667,13 +738,16 @@ export interface CreateRemoteDesktopSessionOptions {
|
|
|
667
738
|
* first host's GPU encoder — and an encoder ffmpeg cannot open makes it exit at once, i.e. a
|
|
668
739
|
* viewer that never shows a frame and blames "Capture failed". */
|
|
669
740
|
encoder?: string;
|
|
741
|
+
/** Ask for the WebRTC transport. Honoured only when the relay is installed and starts; the
|
|
742
|
+
* session otherwise streams over this WebSocket exactly as before, with no error. */
|
|
743
|
+
webrtc?: boolean;
|
|
670
744
|
}
|
|
671
745
|
|
|
672
746
|
/** Evict any previous session (awaiting its ffmpeg exit, capped so a wedged process can't
|
|
673
747
|
* hang a reconnect) before starting the new one. */
|
|
674
748
|
export async function createRemoteDesktopSession(
|
|
675
749
|
ws: RemoteDesktopSocket,
|
|
676
|
-
{ displayId, showCursor = true, encoder }: CreateRemoteDesktopSessionOptions = {},
|
|
750
|
+
{ displayId, showCursor = true, encoder, webrtc = false }: CreateRemoteDesktopSessionOptions = {},
|
|
677
751
|
): Promise<RemoteDesktopSession> {
|
|
678
752
|
for (const existing of [...activeSessions]) {
|
|
679
753
|
existing.close();
|
|
@@ -681,7 +755,7 @@ export async function createRemoteDesktopSession(
|
|
|
681
755
|
}
|
|
682
756
|
const usable = encoder && (await workingEncoders()).includes(encoder) ? encoder : null;
|
|
683
757
|
const session = new RemoteDesktopSession(
|
|
684
|
-
ws, await resolveDisplay(displayId), showCursor, usable,
|
|
758
|
+
ws, await resolveDisplay(displayId), showCursor, usable, webrtc,
|
|
685
759
|
);
|
|
686
760
|
await session.start();
|
|
687
761
|
activeSessions.add(session);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket → loopback WHEP endpoint, so the browser can negotiate WebRTC without the relay ever
|
|
3
|
+
* being reachable from outside this host.
|
|
4
|
+
*
|
|
5
|
+
* The rule this exists to enforce: **the client never names an upstream.** It presents a
|
|
6
|
+
* ticket PPM minted and handed it over the already-authenticated WebSocket, and the proxy
|
|
7
|
+
* looks the URL up here. A route that forwarded a client-supplied URL instead would be an
|
|
8
|
+
* open SSRF hole in a feature that already requires host control to reach.
|
|
9
|
+
*
|
|
10
|
+
* A ticket lives exactly as long as its session: registered when the relay comes up, dropped
|
|
11
|
+
* when the session closes, so a captured ticket stops working the moment the viewer does.
|
|
12
|
+
*/
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
const tickets = new Map<string, string>();
|
|
16
|
+
|
|
17
|
+
/** Register a session's relay and return the ticket the client should present. */
|
|
18
|
+
export function registerWhepTarget(loopbackWhepUrl: string): string {
|
|
19
|
+
const ticket = randomBytes(24).toString("base64url");
|
|
20
|
+
tickets.set(ticket, loopbackWhepUrl);
|
|
21
|
+
return ticket;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The endpoint a ticket stands for, or null when it is unknown or already released. */
|
|
25
|
+
export function resolveWhepTarget(ticket: string): string | null {
|
|
26
|
+
return tickets.get(ticket) ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function releaseWhepTicket(ticket: string): void {
|
|
30
|
+
tickets.delete(ticket);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Test seam. */
|
|
34
|
+
export function clearWhepTickets(): void {
|
|
35
|
+
tickets.clear();
|
|
36
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Single-run execution for scheduled agents: session reuse, stream drain, bounded output. */
|
|
2
2
|
import { chatService } from "./chat.service.ts";
|
|
3
|
-
import { providerRegistry } from "../providers/registry.ts";
|
|
4
3
|
import { setScheduleSessionId } from "./scheduler-db.service.ts";
|
|
5
4
|
import type { Schedule, RunResult } from "../types/scheduler.ts";
|
|
6
5
|
|
|
@@ -62,16 +61,16 @@ export async function runScheduleOnce(schedule: Schedule, sessionId: string): Pr
|
|
|
62
61
|
let resultSubtype: string | undefined;
|
|
63
62
|
let timedOut = false;
|
|
64
63
|
|
|
65
|
-
const provider = providerRegistry.get(schedule.provider_id);
|
|
66
64
|
const killer = setTimeout(() => {
|
|
67
65
|
timedOut = true;
|
|
68
|
-
try {
|
|
66
|
+
try { chatService.abortQuery(schedule.provider_id, sessionId, "scheduler-timeout", "scheduler"); } catch { /* best-effort */ }
|
|
69
67
|
}, schedule.timeout_ms);
|
|
70
68
|
|
|
71
69
|
try {
|
|
72
70
|
const stream = chatService.sendMessage(schedule.provider_id, sessionId, schedule.prompt, {
|
|
73
71
|
permissionMode: schedule.permission_mode,
|
|
74
72
|
...(schedule.max_turns != null && { maxTurns: schedule.max_turns }),
|
|
73
|
+
origin: "scheduler",
|
|
75
74
|
});
|
|
76
75
|
for await (const event of stream) {
|
|
77
76
|
if (event.type === "text") {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BROWSER_TRACE_TYPES,
|
|
3
|
+
TRACE_DEVICE_ID_RE,
|
|
4
|
+
TRACE_INGEST_MAX_BATCH,
|
|
5
|
+
TRACE_INGEST_MAX_PAYLOAD_BYTES,
|
|
6
|
+
type BrowserTraceBatch,
|
|
7
|
+
type BrowserTraceEntry,
|
|
8
|
+
} from "../../shared/session-trace.ts";
|
|
9
|
+
import { redactSecrets } from "../redact-secrets.ts";
|
|
10
|
+
import type { TraceRow } from "./session-trace-store.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The server half of browser logging: what `POST /api/trace` accepts, from whom, and how often.
|
|
14
|
+
*
|
|
15
|
+
* The body is hostile by default — it arrives from any tab holding the token, including one
|
|
16
|
+
* stuck in a render loop — so it is shape-checked here, entries that do not fit are dropped,
|
|
17
|
+
* and a whole batch that does not fit is refused.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** A body past this is refused before it is parsed: 50 entries at the 16 KB cap plus framing. */
|
|
21
|
+
export const TRACE_INGEST_MAX_BODY_BYTES = TRACE_INGEST_MAX_BATCH * TRACE_INGEST_MAX_PAYLOAD_BYTES + 64 * 1024;
|
|
22
|
+
|
|
23
|
+
const ALLOWED_TYPES: ReadonlySet<string> = new Set(BROWSER_TRACE_TYPES);
|
|
24
|
+
const REF_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
25
|
+
/** How far back a corrected timestamp may land; older is a broken clock, not a buffered error. */
|
|
26
|
+
const MAX_BACKDATE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
export type ParsedBatch =
|
|
29
|
+
| { ok: true; batch: BrowserTraceBatch; dropped: number }
|
|
30
|
+
| { ok: false; error: string };
|
|
31
|
+
|
|
32
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Validate a batch. Refuses the whole thing only when its envelope is wrong or it is too big. */
|
|
37
|
+
export function parseBrowserBatch(raw: unknown): ParsedBatch {
|
|
38
|
+
if (!isPlainObject(raw)) return { ok: false, error: "body must be an object" };
|
|
39
|
+
const { deviceId, sentAt, entries } = raw;
|
|
40
|
+
if (typeof deviceId !== "string" || !TRACE_DEVICE_ID_RE.test(deviceId)) return { ok: false, error: "invalid deviceId" };
|
|
41
|
+
if (typeof sentAt !== "number" || !Number.isFinite(sentAt)) return { ok: false, error: "invalid sentAt" };
|
|
42
|
+
if (!Array.isArray(entries)) return { ok: false, error: "entries must be an array" };
|
|
43
|
+
if (entries.length > TRACE_INGEST_MAX_BATCH) return { ok: false, error: `at most ${TRACE_INGEST_MAX_BATCH} entries per batch` };
|
|
44
|
+
|
|
45
|
+
const kept: BrowserTraceEntry[] = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!isPlainObject(entry)) continue;
|
|
48
|
+
const { ts, type, refId, payload } = entry;
|
|
49
|
+
if (typeof type !== "string" || !ALLOWED_TYPES.has(type)) continue;
|
|
50
|
+
if (typeof ts !== "number" || !Number.isFinite(ts)) continue;
|
|
51
|
+
if (refId !== null && refId !== undefined && (typeof refId !== "string" || !REF_ID_RE.test(refId))) continue;
|
|
52
|
+
if (!isPlainObject(payload)) continue;
|
|
53
|
+
let size: number;
|
|
54
|
+
try { size = Buffer.byteLength(JSON.stringify(payload)); } catch { continue; }
|
|
55
|
+
if (size > TRACE_INGEST_MAX_PAYLOAD_BYTES) continue;
|
|
56
|
+
kept.push({ ts, type: type as BrowserTraceEntry["type"], refId: (refId as string | null | undefined) ?? null, payload });
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, batch: { deviceId, sentAt, entries: kept }, dropped: entries.length - kept.length };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Redact every string inside a value, one string at a time. Redacting the serialised JSON
|
|
63
|
+
* instead would let a rule like `password: <rest>` eat a closing quote and corrupt the row.
|
|
64
|
+
*/
|
|
65
|
+
function redactDeep(value: unknown, depth = 0): unknown {
|
|
66
|
+
if (typeof value === "string") return redactSecrets(value);
|
|
67
|
+
if (depth > 8 || value === null || typeof value !== "object") return value;
|
|
68
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v, depth + 1));
|
|
69
|
+
const out: Record<string, unknown> = {};
|
|
70
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v, depth + 1);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Rows for a validated batch. Timestamps are moved onto the server's clock by the offset the
|
|
76
|
+
* batch itself reveals (`sentAt` against now), which keeps the delay of an error buffered
|
|
77
|
+
* offline while removing a skewed clock — so browser rows sort correctly against agent rows.
|
|
78
|
+
*/
|
|
79
|
+
export function browserRows(batch: BrowserTraceBatch, now = Date.now()): TraceRow[] {
|
|
80
|
+
const offset = now - batch.sentAt;
|
|
81
|
+
return batch.entries.map((entry) => ({
|
|
82
|
+
traceId: `browser:${batch.deviceId}`,
|
|
83
|
+
turnId: null,
|
|
84
|
+
ts: Math.round(Math.min(now, Math.max(now - MAX_BACKDATE_MS, entry.ts + offset))),
|
|
85
|
+
source: "browser",
|
|
86
|
+
origin: "browser",
|
|
87
|
+
providerId: null,
|
|
88
|
+
refId: entry.refId,
|
|
89
|
+
type: entry.type,
|
|
90
|
+
payloadJson: JSON.stringify({ type: entry.type, ...(redactDeep(entry.payload) as Record<string, unknown>) }),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RateLimits {
|
|
95
|
+
windowMs: number;
|
|
96
|
+
maxRequests: number;
|
|
97
|
+
maxEntries: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DEFAULT_LIMITS: RateLimits = { windowMs: 60_000, maxRequests: 60, maxEntries: 1_000 };
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A fixed-window limit per device id. Never per IP: behind a Cloudflare tunnel every client is
|
|
104
|
+
* one IP, so a per-IP limit would let one machine in a render loop lock out all the others.
|
|
105
|
+
*/
|
|
106
|
+
export class DeviceRateLimiter {
|
|
107
|
+
private windows = new Map<string, { start: number; requests: number; entries: number }>();
|
|
108
|
+
|
|
109
|
+
constructor(private readonly limits: RateLimits = DEFAULT_LIMITS, private readonly now: () => number = Date.now) {}
|
|
110
|
+
|
|
111
|
+
/** Count a request, or say how long until this device may send again. */
|
|
112
|
+
take(deviceId: string, entries: number): { allowed: true } | { allowed: false; retryAfterMs: number } {
|
|
113
|
+
const now = this.now();
|
|
114
|
+
let w = this.windows.get(deviceId);
|
|
115
|
+
if (!w || now - w.start >= this.limits.windowMs) {
|
|
116
|
+
w = { start: now, requests: 0, entries: 0 };
|
|
117
|
+
this.windows.set(deviceId, w);
|
|
118
|
+
if (this.windows.size > 1_000) this.prune(now);
|
|
119
|
+
}
|
|
120
|
+
if (w.requests + 1 > this.limits.maxRequests || w.entries + entries > this.limits.maxEntries) {
|
|
121
|
+
return { allowed: false, retryAfterMs: Math.max(1, w.start + this.limits.windowMs - now) };
|
|
122
|
+
}
|
|
123
|
+
w.requests += 1;
|
|
124
|
+
w.entries += entries;
|
|
125
|
+
return { allowed: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private prune(now: number): void {
|
|
129
|
+
for (const [id, w] of this.windows) if (now - w.start >= this.limits.windowMs) this.windows.delete(id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getTraceDb, getTraceDbSizeBytes } from "./session-trace-db.ts";
|
|
2
|
+
|
|
3
|
+
/** Ceiling on rows removed per pass, so the write lock is never held for long. */
|
|
4
|
+
const MAX_BATCH = 2_000;
|
|
5
|
+
/** Stops a pathological loop if vacuuming somehow never reduces the file. */
|
|
6
|
+
const MAX_PASSES = 40;
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How many rows to drop to get back under the cap, sized from the actual excess so the log
|
|
11
|
+
* stays as full as the cap allows (same reasoning as `query-audit-cleanup.ts`).
|
|
12
|
+
*/
|
|
13
|
+
function rowsToDrop(currentBytes: number, maxBytes: number, rowCount: number): number {
|
|
14
|
+
if (rowCount <= 0) return 0;
|
|
15
|
+
const avgRowBytes = Math.max(1, currentBytes / rowCount);
|
|
16
|
+
const needed = Math.ceil((currentBytes - maxBytes) / avgRowBytes) + 1;
|
|
17
|
+
return Math.max(1, Math.min(needed, MAX_BATCH, rowCount));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TraceCleanupResult {
|
|
21
|
+
deletedByAge: number;
|
|
22
|
+
deletedBySize: number;
|
|
23
|
+
freedBytes: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Prune the trace by age, then by size, oldest first — mirrors `cleanupQueryAudit`.
|
|
28
|
+
*
|
|
29
|
+
* Deleting alone never shrinks a SQLite file; `incremental_vacuum` returns the pages, and only
|
|
30
|
+
* because the database was created with `auto_vacuum = INCREMENTAL`. Size comes from page
|
|
31
|
+
* counts so an active WAL does not distort it.
|
|
32
|
+
*/
|
|
33
|
+
export function cleanupSessionTrace(retentionDays: number, maxSizeMb: number, now = Date.now()): TraceCleanupResult {
|
|
34
|
+
const db = getTraceDb();
|
|
35
|
+
const sizeBefore = getTraceDbSizeBytes();
|
|
36
|
+
const cutoff = now - Math.max(1, Math.floor(retentionDays)) * DAY_MS;
|
|
37
|
+
|
|
38
|
+
const deletedByAge = db.run("DELETE FROM session_events WHERE ts < ?", [cutoff]).changes;
|
|
39
|
+
db.exec("PRAGMA incremental_vacuum");
|
|
40
|
+
|
|
41
|
+
const maxBytes = Math.max(1, maxSizeMb) * 1024 * 1024;
|
|
42
|
+
let deletedBySize = 0;
|
|
43
|
+
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
44
|
+
const currentBytes = getTraceDbSizeBytes();
|
|
45
|
+
if (currentBytes <= maxBytes) break;
|
|
46
|
+
const rowCount = (db.query("SELECT COUNT(*) AS count FROM session_events").get() as { count: number }).count;
|
|
47
|
+
const batch = rowsToDrop(currentBytes, maxBytes, rowCount);
|
|
48
|
+
// Nothing left to drop — what remains is schema overhead, not rows.
|
|
49
|
+
if (batch === 0) break;
|
|
50
|
+
const removed = db.run(
|
|
51
|
+
`DELETE FROM session_events WHERE rowid IN (
|
|
52
|
+
SELECT rowid FROM session_events ORDER BY ts ASC LIMIT ${batch}
|
|
53
|
+
)`,
|
|
54
|
+
).changes;
|
|
55
|
+
if (removed === 0) break;
|
|
56
|
+
deletedBySize += removed;
|
|
57
|
+
db.exec("PRAGMA incremental_vacuum");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// An alias outlives nothing it points at: a conversation still being written keeps its alias
|
|
61
|
+
// however old, one whose rows are all gone loses it.
|
|
62
|
+
db.run("DELETE FROM trace_aliases WHERE NOT EXISTS (SELECT 1 FROM session_events e WHERE e.trace_id = trace_aliases.trace_id)");
|
|
63
|
+
|
|
64
|
+
return { deletedByAge, deletedBySize, freedBytes: Math.max(0, sizeBefore - getTraceDbSizeBytes()) };
|
|
65
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getPpmDir } from "../ppm-dir.ts";
|
|
5
|
+
|
|
6
|
+
const SCHEMA_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
let cached: Database | undefined;
|
|
9
|
+
|
|
10
|
+
export function getTraceDbPath(): string {
|
|
11
|
+
return join(getPpmDir(), "session-trace.db");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Open (or reuse) the trace database.
|
|
16
|
+
*
|
|
17
|
+
* A file of its own for the reason `query-audit.db` is one: event volume must not bloat config
|
|
18
|
+
* and workspace state in `ppm.db`. Deliberately outside `db-backup` — this is a log, not state.
|
|
19
|
+
*/
|
|
20
|
+
export function getTraceDb(): Database {
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
|
|
23
|
+
const dir = getPpmDir();
|
|
24
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
const db = new Database(getTraceDbPath());
|
|
27
|
+
// Server and CLI append from separate processes; without a wait a concurrent write
|
|
28
|
+
// surfaces as SQLITE_BUSY and the batch is dropped. First, because the pragmas below take
|
|
29
|
+
// locks too: set after them, a lock another process held for 300 ms failed this open.
|
|
30
|
+
db.exec("PRAGMA busy_timeout = 2000");
|
|
31
|
+
// Only takes effect when set before the first table exists. Without it DELETE never
|
|
32
|
+
// returns disk space, which would make the size cap in the cleanup job meaningless.
|
|
33
|
+
db.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
|
34
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
35
|
+
// A log: losing the last few milliseconds on power loss is the right trade for not
|
|
36
|
+
// paying an fsync per commit. Per connection, so it is set on every open.
|
|
37
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
38
|
+
migrate(db);
|
|
39
|
+
|
|
40
|
+
cached = db;
|
|
41
|
+
return db;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function migrate(db: Database): void {
|
|
45
|
+
const { user_version } = db.query("PRAGMA user_version").get() as { user_version: number };
|
|
46
|
+
|
|
47
|
+
if (user_version < 1) {
|
|
48
|
+
// `trace_id` is the id a run started under and never the one a provider swaps in
|
|
49
|
+
// mid-stream (`session_migrated`); `trace_aliases` maps those later ids back, so one
|
|
50
|
+
// conversation stays one trace. `seq` is ours, assigned at insert — timestamps order
|
|
51
|
+
// nothing across two processes.
|
|
52
|
+
db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
54
|
+
trace_id TEXT NOT NULL,
|
|
55
|
+
seq INTEGER NOT NULL,
|
|
56
|
+
turn_id TEXT,
|
|
57
|
+
ts INTEGER NOT NULL,
|
|
58
|
+
source TEXT NOT NULL,
|
|
59
|
+
origin TEXT NOT NULL,
|
|
60
|
+
provider_id TEXT,
|
|
61
|
+
ref_id TEXT,
|
|
62
|
+
type TEXT NOT NULL,
|
|
63
|
+
payload_json TEXT NOT NULL,
|
|
64
|
+
PRIMARY KEY (trace_id, seq)
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_ref ON session_events(ref_id, ts) WHERE ref_id IS NOT NULL;
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_ts ON session_events(ts);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS trace_aliases (
|
|
69
|
+
alias_id TEXT PRIMARY KEY,
|
|
70
|
+
trace_id TEXT NOT NULL,
|
|
71
|
+
ts INTEGER NOT NULL
|
|
72
|
+
);
|
|
73
|
+
PRAGMA user_version = ${SCHEMA_VERSION};
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Current size on disk, derived from pages so it stays accurate while WAL is active. */
|
|
79
|
+
export function getTraceDbSizeBytes(): number {
|
|
80
|
+
const db = getTraceDb();
|
|
81
|
+
const { page_count } = db.query("PRAGMA page_count").get() as { page_count: number };
|
|
82
|
+
const { page_size } = db.query("PRAGMA page_size").get() as { page_size: number };
|
|
83
|
+
return page_count * page_size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function closeTraceDb(): void {
|
|
87
|
+
cached?.close();
|
|
88
|
+
cached = undefined;
|
|
89
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { getTraceDb } from "./session-trace-db.ts";
|
|
2
|
+
import type { TraceOrigin, TraceSource } from "../../shared/session-trace.ts";
|
|
3
|
+
|
|
4
|
+
/** One row as the writer hands it over. `seq` is not here: it is assigned at insert. */
|
|
5
|
+
export interface TraceRow {
|
|
6
|
+
traceId: string;
|
|
7
|
+
turnId: string | null;
|
|
8
|
+
ts: number;
|
|
9
|
+
source: TraceSource;
|
|
10
|
+
origin: TraceOrigin;
|
|
11
|
+
providerId: string | null;
|
|
12
|
+
refId: string | null;
|
|
13
|
+
type: string;
|
|
14
|
+
payloadJson: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One row as read back. */
|
|
18
|
+
export interface TraceEvent {
|
|
19
|
+
traceId: string;
|
|
20
|
+
seq: number;
|
|
21
|
+
turnId: string | null;
|
|
22
|
+
ts: number;
|
|
23
|
+
source: TraceSource;
|
|
24
|
+
origin: TraceOrigin;
|
|
25
|
+
providerId: string | null;
|
|
26
|
+
refId: string | null;
|
|
27
|
+
type: string;
|
|
28
|
+
payload: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DbRow {
|
|
32
|
+
trace_id: string;
|
|
33
|
+
seq: number;
|
|
34
|
+
turn_id: string | null;
|
|
35
|
+
ts: number;
|
|
36
|
+
source: TraceSource;
|
|
37
|
+
origin: TraceOrigin;
|
|
38
|
+
provider_id: string | null;
|
|
39
|
+
ref_id: string | null;
|
|
40
|
+
type: string;
|
|
41
|
+
payload_json: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// One statement, so the MAX and the insert cannot be split by another writer; the batch
|
|
45
|
+
// runs under BEGIN IMMEDIATE, so the server and a CLI appending to one trace take turns.
|
|
46
|
+
const INSERT_SQL = `
|
|
47
|
+
INSERT INTO session_events (trace_id, seq, turn_id, ts, source, origin, provider_id, ref_id, type, payload_json)
|
|
48
|
+
SELECT ?1, COALESCE(MAX(seq), 0) + 1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9
|
|
49
|
+
FROM session_events WHERE trace_id = ?1`;
|
|
50
|
+
|
|
51
|
+
/** Append rows in one transaction. Throws on failure — the writer decides to drop. */
|
|
52
|
+
export function appendBatch(rows: readonly TraceRow[]): void {
|
|
53
|
+
if (rows.length === 0) return;
|
|
54
|
+
const db = getTraceDb();
|
|
55
|
+
const insert = db.query(INSERT_SQL);
|
|
56
|
+
db.transaction((batch: readonly TraceRow[]) => {
|
|
57
|
+
for (const r of batch) {
|
|
58
|
+
insert.run(r.traceId, r.turnId, r.ts, r.source, r.origin, r.providerId, r.refId, r.type, r.payloadJson);
|
|
59
|
+
}
|
|
60
|
+
}).immediate(rows);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toEvent(row: DbRow): TraceEvent {
|
|
64
|
+
let payload: unknown;
|
|
65
|
+
try { payload = JSON.parse(row.payload_json); } catch { payload = row.payload_json; }
|
|
66
|
+
return {
|
|
67
|
+
traceId: row.trace_id,
|
|
68
|
+
seq: row.seq,
|
|
69
|
+
turnId: row.turn_id,
|
|
70
|
+
ts: row.ts,
|
|
71
|
+
source: row.source,
|
|
72
|
+
origin: row.origin,
|
|
73
|
+
providerId: row.provider_id,
|
|
74
|
+
refId: row.ref_id,
|
|
75
|
+
type: row.type,
|
|
76
|
+
payload,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Every row of one trace, in the order it was written. */
|
|
81
|
+
export function readEvents(traceId: string): TraceEvent[] {
|
|
82
|
+
const rows = getTraceDb()
|
|
83
|
+
.query("SELECT * FROM session_events WHERE trace_id = ? ORDER BY seq")
|
|
84
|
+
.all(traceId) as DbRow[];
|
|
85
|
+
return rows.map(toEvent);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The trace a session id belongs to: itself, unless a provider renamed the session
|
|
90
|
+
* mid-run (`session_migrated`), in which case the id the run started under.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveTraceId(sessionId: string): string {
|
|
93
|
+
const row = getTraceDb()
|
|
94
|
+
.query("SELECT trace_id FROM trace_aliases WHERE alias_id = ?")
|
|
95
|
+
.get(sessionId) as { trace_id: string } | null;
|
|
96
|
+
return row?.trace_id ?? sessionId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Record that `aliasId` now names the conversation traced under `traceId`. */
|
|
100
|
+
export function recordTraceAlias(aliasId: string, traceId: string): void {
|
|
101
|
+
const root = resolveTraceId(traceId);
|
|
102
|
+
if (aliasId === root) return;
|
|
103
|
+
getTraceDb()
|
|
104
|
+
.query("INSERT OR IGNORE INTO trace_aliases (alias_id, trace_id, ts) VALUES (?, ?, ?)")
|
|
105
|
+
.run(aliasId, root, Date.now());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A session's whole story: its own trace plus every browser row filed against it, under
|
|
110
|
+
* any id the session has had. Browser rows carry `ref_id`, so this is the one query that
|
|
111
|
+
* answers "which turn was running when the tab broke".
|
|
112
|
+
*/
|
|
113
|
+
export function readSessionTimeline(sessionId: string, limit = 5000): TraceEvent[] {
|
|
114
|
+
const db = getTraceDb();
|
|
115
|
+
const traceId = resolveTraceId(sessionId);
|
|
116
|
+
const aliases = (db.query("SELECT alias_id FROM trace_aliases WHERE trace_id = ?").all(traceId) as { alias_id: string }[])
|
|
117
|
+
.map((r) => r.alias_id);
|
|
118
|
+
const ids = [traceId, ...aliases];
|
|
119
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
120
|
+
const rows = db
|
|
121
|
+
.query(`SELECT * FROM (
|
|
122
|
+
SELECT * FROM session_events
|
|
123
|
+
WHERE trace_id = ? OR ref_id IN (${placeholders})
|
|
124
|
+
ORDER BY ts DESC, seq DESC LIMIT ?
|
|
125
|
+
) ORDER BY ts, seq`)
|
|
126
|
+
.all(traceId, ...ids, Math.max(1, Math.floor(limit))) as DbRow[];
|
|
127
|
+
return rows.map(toEvent);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function countTraceEvents(): number {
|
|
131
|
+
return (getTraceDb().query("SELECT COUNT(*) AS count FROM session_events").get() as { count: number }).count;
|
|
132
|
+
}
|