@hienlh/ppm 0.9.0-beta.9 → 0.9.1
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 +233 -0
- package/bun.lock +17 -0
- package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/api-settings-BUvk6Saw.js +1 -0
- package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
- package/dist/web/assets/arrow-up-BYhx9ckd.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
- package/dist/web/assets/browser-tab-CrkhFCaw.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
- package/dist/web/assets/channel-By7bn0Yq.js +1 -0
- package/dist/web/assets/chat-tab-C6jpiwh7.js +8 -0
- package/dist/web/assets/chevron-right-5HgK6l7K.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-CBIPzlP2.js +2 -0
- package/dist/web/assets/columns-2-cEVJHYd7.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +1 -0
- package/dist/web/assets/{csv-preview-DUbHtTAS.js → csv-preview-ncSOnJSC.js} +2 -2
- package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
- package/dist/web/assets/database-viewer-BqOJR_zi.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
- package/dist/web/assets/diff-viewer-CcLyp4eY.js +4 -0
- package/dist/web/assets/{dist-CALwEtco.js → dist-DIV6WgAG.js} +1 -1
- package/dist/web/assets/{dist-DGDPTxs1.js → dist-ovWkrgO-.js} +1 -1
- package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
- package/dist/web/assets/extension-webview-NiZ7Ybvv.js +3 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
- package/dist/web/assets/git-graph-CoTvMrIo.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
- package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-C8byznLO.js +37 -0
- package/dist/web/assets/index-KwC2YrG4.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
- package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
- package/dist/web/assets/jsx-runtime-kMwlnEGE.js +1 -0
- package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
- package/dist/web/assets/keybindings-store-DPYzBe_M.js +1 -0
- package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
- package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/{markdown-renderer-VIZB1GXE.js → markdown-renderer-DPLdR9xc.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
- package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
- package/dist/web/assets/postgres-viewer-BeiK4lCa.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
- package/dist/web/assets/settings-tab-D3AvU4lu.js +1 -0
- package/dist/web/assets/sqlite-viewer-nA2sD4Yv.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
- package/dist/web/assets/tab-store-BOgTrqRr.js +1 -0
- package/dist/web/assets/table-DFevCOMd.js +1 -0
- package/dist/web/assets/tag-CXMT0QB6.js +1 -0
- package/dist/web/assets/{terminal-tab-XhKfb4ei.js → terminal-tab-BBi0pEji.js} +1 -1
- package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/{use-monaco-theme-0p0-84jJ.js → use-monaco-theme-B5pG2d1w.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
- package/dist/web/index.html +10 -9
- package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
- package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
- package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +128 -1
- package/docs/codebase-summary.md +79 -12
- package/docs/extension-development-guide.md +532 -0
- package/docs/project-changelog.md +51 -1
- package/docs/project-roadmap.md +9 -3
- package/docs/streaming-input-guide.md +267 -0
- package/docs/system-architecture.md +432 -3
- package/package.json +6 -3
- package/packages/ext-database/package.json +41 -0
- package/packages/ext-database/src/connection-tree.ts +142 -0
- package/packages/ext-database/src/extension.ts +346 -0
- package/packages/ext-database/src/query-panel.ts +120 -0
- package/packages/ext-database/src/table-viewer-panel.ts +410 -0
- package/packages/ext-database/tsconfig.json +8 -0
- package/packages/vscode-compat/package.json +16 -0
- package/packages/vscode-compat/src/commands.ts +39 -0
- package/packages/vscode-compat/src/context.ts +65 -0
- package/packages/vscode-compat/src/disposable.ts +21 -0
- package/packages/vscode-compat/src/env.ts +20 -0
- package/packages/vscode-compat/src/event-emitter.ts +28 -0
- package/packages/vscode-compat/src/index.ts +93 -0
- package/packages/vscode-compat/src/not-supported.ts +15 -0
- package/packages/vscode-compat/src/types.ts +167 -0
- package/packages/vscode-compat/src/uri.ts +65 -0
- package/packages/vscode-compat/src/window.ts +229 -0
- package/packages/vscode-compat/src/workspace.ts +76 -0
- package/packages/vscode-compat/tsconfig.json +10 -0
- package/snapshot-state.md +1526 -0
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/ext-cmd.ts +121 -0
- package/src/cli/commands/restart.ts +9 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/index.ts +5 -3
- package/src/providers/claude-agent-sdk.ts +221 -17
- package/src/providers/cli-provider-base.ts +6 -0
- package/src/server/index.ts +55 -155
- package/src/server/routes/chat.ts +81 -11
- package/src/server/routes/extensions.ts +81 -0
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/settings.ts +27 -0
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +9 -3
- package/src/server/ws/extensions.ts +175 -0
- package/src/services/account-selector.service.ts +14 -5
- package/src/services/account.service.ts +7 -7
- package/src/services/claude-usage.service.ts +11 -11
- package/src/services/cloud-ws.service.ts +228 -0
- package/src/services/cloud.service.ts +1 -0
- package/src/services/contribution-registry.ts +110 -0
- package/src/services/db.service.ts +181 -4
- package/src/services/extension-host-worker.ts +160 -0
- package/src/services/extension-installer.ts +112 -0
- package/src/services/extension-manifest.ts +65 -0
- package/src/services/extension-rpc-handlers.ts +235 -0
- package/src/services/extension-rpc.ts +105 -0
- package/src/services/extension.service.ts +228 -0
- package/src/services/mcp-config.service.ts +15 -6
- package/src/services/supervisor.ts +271 -25
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +4 -0
- package/src/types/extension-messages.ts +64 -0
- package/src/types/extension.ts +131 -0
- package/src/web/app.tsx +69 -48
- package/src/web/components/chat/account-rotation-settings.tsx +163 -0
- package/src/web/components/chat/chat-history-bar.tsx +106 -10
- package/src/web/components/chat/chat-tab.tsx +15 -10
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-list.tsx +19 -6
- package/src/web/components/chat/session-picker.tsx +80 -32
- package/src/web/components/chat/usage-badge.tsx +68 -8
- package/src/web/components/extensions/extension-inputbox.tsx +92 -0
- package/src/web/components/extensions/extension-quickpick.tsx +194 -0
- package/src/web/components/extensions/extension-tree-view.tsx +240 -0
- package/src/web/components/extensions/extension-webview.tsx +83 -0
- package/src/web/components/layout/command-palette.tsx +22 -2
- package/src/web/components/layout/editor-panel.tsx +163 -18
- package/src/web/components/layout/mobile-nav.tsx +2 -1
- package/src/web/components/layout/sidebar.tsx +21 -3
- package/src/web/components/layout/status-bar.tsx +64 -0
- package/src/web/components/layout/tab-bar.tsx +2 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/upgrade-banner.tsx +15 -5
- package/src/web/components/settings/change-password-section.tsx +128 -0
- package/src/web/components/settings/extension-manager-section.tsx +214 -0
- package/src/web/components/settings/settings-tab.tsx +9 -2
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +28 -0
- package/src/web/hooks/use-extension-ws.ts +181 -0
- package/src/web/hooks/use-global-keybindings.ts +18 -2
- package/src/web/hooks/use-server-reload.ts +9 -0
- package/src/web/hooks/use-url-sync.ts +173 -21
- package/src/web/stores/connection-store.ts +39 -0
- package/src/web/stores/extension-store.ts +204 -0
- package/src/web/stores/panel-store.ts +63 -9
- package/src/web/stores/panel-utils.ts +145 -3
- package/src/web/stores/settings-store.ts +7 -2
- package/src/web/stores/tab-store.ts +2 -1
- package/test-session-ops.mjs +444 -0
- package/test-tokens.mjs +212 -0
- package/tsconfig.json +3 -1
- package/dist/web/assets/api-settings-CEMxVMCV.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-D1Zua62g.js +0 -1
- package/dist/web/assets/channel-C2fMafck.js +0 -1
- package/dist/web/assets/chat-tab-BnD27Vp9.js +0 -7
- package/dist/web/assets/chevron-right-CHnjJt4E.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
- package/dist/web/assets/clone-B2hUek6n.js +0 -1
- package/dist/web/assets/code-editor-DGRg8stf.js +0 -2
- package/dist/web/assets/columns-2-DbesTfa7.js +0 -1
- package/dist/web/assets/database-viewer-DxCXZQcE.js +0 -1
- package/dist/web/assets/diff-viewer-C1sDJG35.js +0 -4
- package/dist/web/assets/git-graph-BDn-EiGE.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
- package/dist/web/assets/index-Bun94AK3.js +0 -37
- package/dist/web/assets/index-Db8uky1a.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
- package/dist/web/assets/keybindings-store-COmK4Dte.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
- package/dist/web/assets/postgres-viewer-CvQZ8gkh.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
- package/dist/web/assets/settings-tab-RCnvZ29H.js +0 -1
- package/dist/web/assets/sqlite-viewer-CEEm2W4C.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
- package/dist/web/assets/tab-store-Bjh6bXFP.js +0 -1
- package/dist/web/assets/table-CQVQM2SB.js +0 -1
- package/dist/web/assets/tag-Q2dZiSPX.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
- /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{lib-BeaDXEkP.js → lib-BQ34Db2e.js} +0 -0
- /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{utils-DMiycH3O.js → utils-BNytJOb1.js} +0 -0
package/src/server/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { browserPreviewRoutes } from "./routes/browser-preview.ts";
|
|
|
19
19
|
import { initAdapters } from "../services/database/init-adapters.ts";
|
|
20
20
|
import { terminalWebSocket } from "./ws/terminal.ts";
|
|
21
21
|
import { chatWebSocket } from "./ws/chat.ts";
|
|
22
|
+
import { extensionWebSocket } from "./ws/extensions.ts";
|
|
22
23
|
import { ok, err } from "../types/api.ts";
|
|
23
24
|
|
|
24
25
|
/** Tee console.log/error to ~/.ppm/ppm.log while preserving terminal output */
|
|
@@ -149,6 +150,10 @@ app.route("/api/postgres", postgresRoutes);
|
|
|
149
150
|
app.route("/api/db", databaseRoutes);
|
|
150
151
|
app.route("/api/accounts", accountsRoutes);
|
|
151
152
|
|
|
153
|
+
// Extensions management
|
|
154
|
+
import { extensionRoutes } from "./routes/extensions.ts";
|
|
155
|
+
app.route("/api/extensions", extensionRoutes);
|
|
156
|
+
|
|
152
157
|
// Upgrade routes (check for updates, apply upgrade)
|
|
153
158
|
import { upgradeRoutes } from "./routes/upgrade.ts";
|
|
154
159
|
app.route("/api/upgrade", upgradeRoutes);
|
|
@@ -162,51 +167,43 @@ app.route("/", staticRoutes);
|
|
|
162
167
|
|
|
163
168
|
export async function startServer(options: {
|
|
164
169
|
port?: string;
|
|
165
|
-
foreground?: boolean;
|
|
166
|
-
daemon?: boolean; // compat, ignored (daemon is now default)
|
|
167
170
|
share?: boolean;
|
|
168
171
|
config?: string;
|
|
169
172
|
profile?: string;
|
|
170
173
|
}) {
|
|
174
|
+
// Tunnel always enabled — cloudflared shares the server publicly
|
|
175
|
+
options.share = true;
|
|
176
|
+
|
|
171
177
|
// Load config
|
|
172
178
|
configService.load(options.config);
|
|
173
179
|
const port = parseInt(options.port ?? String(configService.get("port")), 10);
|
|
174
180
|
const host = configService.get("host");
|
|
175
181
|
|
|
176
|
-
// Setup log file (both foreground and daemon modes)
|
|
177
182
|
await setupLogFile();
|
|
178
183
|
|
|
179
184
|
// Bootstrap CLI providers (checks binary availability)
|
|
180
185
|
const { bootstrapProviders } = await import("../providers/registry.ts");
|
|
181
186
|
await bootstrapProviders();
|
|
182
187
|
|
|
183
|
-
// Check if port is already in use before
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
});
|
|
200
|
-
if (portInUse) {
|
|
201
|
-
console.error(`\n ✗ Port ${port} is already in use.`);
|
|
202
|
-
console.error(` Run 'ppm stop' first or use a different port with --port.\n`);
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
188
|
+
// Check if port is already in use before spawning supervisor
|
|
189
|
+
const portInUse = await new Promise<boolean>((resolve) => {
|
|
190
|
+
const net = require("node:net") as typeof import("node:net");
|
|
191
|
+
const tester = net.createServer()
|
|
192
|
+
.once("error", (err: NodeJS.ErrnoException) => {
|
|
193
|
+
resolve(err.code === "EADDRINUSE");
|
|
194
|
+
})
|
|
195
|
+
.once("listening", () => {
|
|
196
|
+
tester.close(() => resolve(false));
|
|
197
|
+
})
|
|
198
|
+
.listen(port, host);
|
|
199
|
+
});
|
|
200
|
+
if (portInUse) {
|
|
201
|
+
console.error(`\n ✗ Port ${port} is already in use.`);
|
|
202
|
+
console.error(` Run 'ppm stop' first or use a different port with --port.\n`);
|
|
203
|
+
process.exit(1);
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (isDaemon) {
|
|
206
|
+
{
|
|
210
207
|
const { resolve } = await import("node:path");
|
|
211
208
|
const { homedir } = await import("node:os");
|
|
212
209
|
const { writeFileSync, readFileSync, mkdirSync, existsSync, openSync } = await import("node:fs");
|
|
@@ -272,7 +269,6 @@ export async function startServer(options: {
|
|
|
272
269
|
if (isNaN(supervisorPid)) {
|
|
273
270
|
console.error(" ✗ Failed to start supervisor on Windows.");
|
|
274
271
|
console.error(` ${result.stderr.toString().trim()}`);
|
|
275
|
-
console.error(" Try: ppm start -f (foreground mode)");
|
|
276
272
|
process.exit(1);
|
|
277
273
|
}
|
|
278
274
|
} else {
|
|
@@ -297,7 +293,6 @@ export async function startServer(options: {
|
|
|
297
293
|
try { process.kill(supervisorPid, 0); } catch {
|
|
298
294
|
console.error(" ✗ Supervisor exited immediately after start.");
|
|
299
295
|
console.error(" Check logs: ppm logs");
|
|
300
|
-
console.error(" Or try: ppm start -f (foreground mode)");
|
|
301
296
|
process.exit(1);
|
|
302
297
|
}
|
|
303
298
|
// Check if server PID appeared in status.json
|
|
@@ -353,130 +348,6 @@ export async function startServer(options: {
|
|
|
353
348
|
|
|
354
349
|
process.exit(0);
|
|
355
350
|
}
|
|
356
|
-
|
|
357
|
-
// Foreground mode — with WebSocket support
|
|
358
|
-
const server = Bun.serve({
|
|
359
|
-
port,
|
|
360
|
-
hostname: host,
|
|
361
|
-
fetch(req, server) {
|
|
362
|
-
const url = new URL(req.url);
|
|
363
|
-
|
|
364
|
-
// WebSocket upgrade: /ws/project/:projectName/terminal/:id
|
|
365
|
-
if (url.pathname.startsWith("/ws/project/")) {
|
|
366
|
-
const parts = url.pathname.split("/");
|
|
367
|
-
const projectName = parts[3] ?? "";
|
|
368
|
-
const wsType = parts[4] ?? "";
|
|
369
|
-
const id = parts[5] ?? "";
|
|
370
|
-
|
|
371
|
-
if (wsType === "terminal") {
|
|
372
|
-
const upgraded = server.upgrade(req, {
|
|
373
|
-
data: { type: "terminal", id, projectName },
|
|
374
|
-
});
|
|
375
|
-
if (upgraded) return undefined;
|
|
376
|
-
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (wsType === "chat") {
|
|
380
|
-
const sessionId = id;
|
|
381
|
-
const upgraded = server.upgrade(req, {
|
|
382
|
-
data: { type: "chat", sessionId, projectName },
|
|
383
|
-
});
|
|
384
|
-
if (upgraded) return undefined;
|
|
385
|
-
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return app.fetch(req, server);
|
|
390
|
-
},
|
|
391
|
-
websocket: {
|
|
392
|
-
idleTimeout: 960,
|
|
393
|
-
sendPong: true,
|
|
394
|
-
perMessageDeflate: false, // Disable compression — Cloudflare tunnels can mangle compressed frames
|
|
395
|
-
open(ws: any) {
|
|
396
|
-
if (ws.data?.type === "health") {
|
|
397
|
-
ws.send(JSON.stringify({ type: "health", status: "ok" }));
|
|
398
|
-
} else if (ws.data?.type === "chat") chatWebSocket.open(ws);
|
|
399
|
-
else terminalWebSocket.open(ws);
|
|
400
|
-
},
|
|
401
|
-
message(ws: any, msg: any) {
|
|
402
|
-
if (ws.data?.type === "health") {
|
|
403
|
-
// Respond to ping with pong
|
|
404
|
-
ws.send(JSON.stringify({ type: "health", status: "ok" }));
|
|
405
|
-
} else if (ws.data?.type === "chat") chatWebSocket.message(ws, msg);
|
|
406
|
-
else terminalWebSocket.message(ws, msg);
|
|
407
|
-
},
|
|
408
|
-
close(ws: any) {
|
|
409
|
-
if (ws.data?.type === "health") return;
|
|
410
|
-
if (ws.data?.type === "chat") chatWebSocket.close(ws);
|
|
411
|
-
else terminalWebSocket.close(ws);
|
|
412
|
-
},
|
|
413
|
-
} as Parameters<typeof Bun.serve>[0] extends { websocket?: infer W } ? W : never,
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// Mark server as started — survives bun --hot reloads (globalThis persists)
|
|
417
|
-
(globalThis as any).__PPM_SERVER_STARTED__ = true;
|
|
418
|
-
|
|
419
|
-
// Start background usage polling
|
|
420
|
-
import("../services/claude-usage.service.ts").then(({ startUsagePolling }) => startUsagePolling()).catch(() => {});
|
|
421
|
-
|
|
422
|
-
// Start background account token refresh
|
|
423
|
-
import("../services/account.service.ts").then(({ accountService }) => accountService.startAutoRefresh()).catch(() => {});
|
|
424
|
-
|
|
425
|
-
console.log(`\n PPM ready\n`);
|
|
426
|
-
console.log(` ➜ Local: http://localhost:${server.port}/`);
|
|
427
|
-
|
|
428
|
-
const { networkInterfaces } = await import("node:os");
|
|
429
|
-
const nets = networkInterfaces();
|
|
430
|
-
for (const name of Object.keys(nets)) {
|
|
431
|
-
for (const net of nets[name] ?? []) {
|
|
432
|
-
if (net.family === "IPv4" && !net.internal) {
|
|
433
|
-
console.log(` ➜ Network: http://${net.address}:${server.port}/`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Share tunnel in foreground mode
|
|
439
|
-
if (options.share) {
|
|
440
|
-
try {
|
|
441
|
-
const { tunnelService } = await import("../services/tunnel.service.ts");
|
|
442
|
-
console.log("\n Starting share tunnel...");
|
|
443
|
-
const shareUrl = await tunnelService.startTunnel(server.port!);
|
|
444
|
-
console.log(` ➜ Share: ${shareUrl}`);
|
|
445
|
-
if (!configService.get("auth").enabled) {
|
|
446
|
-
console.log(`\n ⚠ Warning: auth is disabled — your IDE is publicly accessible!`);
|
|
447
|
-
console.log(` Enable auth: run 'ppm config set auth.enabled true' or restart without --share.`);
|
|
448
|
-
}
|
|
449
|
-
const qr = await import("qrcode-terminal");
|
|
450
|
-
console.log();
|
|
451
|
-
qr.generate(shareUrl, { small: true });
|
|
452
|
-
} catch (err: unknown) {
|
|
453
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
454
|
-
console.error(` ✗ Share failed: ${msg}`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
console.log(`\n Auth: ${configService.get("auth").enabled ? "enabled" : "disabled"}`);
|
|
459
|
-
if (configService.get("auth").enabled) {
|
|
460
|
-
console.log(` Token: ${configService.get("auth").token}`);
|
|
461
|
-
}
|
|
462
|
-
console.log();
|
|
463
|
-
|
|
464
|
-
// Graceful shutdown — stop server + tunnel + preview tunnels + DB on exit
|
|
465
|
-
const shutdown = () => {
|
|
466
|
-
try { server.stop(true); } catch {}
|
|
467
|
-
try {
|
|
468
|
-
import("../services/tunnel.service.ts").then(({ tunnelService }) => tunnelService.stopTunnel()).catch(() => {});
|
|
469
|
-
} catch {}
|
|
470
|
-
try {
|
|
471
|
-
import("./routes/browser-preview.ts").then(({ stopAllPreviewTunnels }) => stopAllPreviewTunnels()).catch(() => {});
|
|
472
|
-
} catch {}
|
|
473
|
-
try {
|
|
474
|
-
import("../services/db.service.ts").then(({ closeDb }) => closeDb()).catch(() => {});
|
|
475
|
-
} catch {}
|
|
476
|
-
};
|
|
477
|
-
process.on("SIGINT", () => { shutdown(); process.exit(0); });
|
|
478
|
-
process.on("SIGTERM", () => { shutdown(); process.exit(0); });
|
|
479
|
-
process.on("exit", shutdown);
|
|
480
351
|
}
|
|
481
352
|
|
|
482
353
|
// Internal entry point for daemon child process
|
|
@@ -500,12 +371,16 @@ if (process.argv.includes("__serve__")) {
|
|
|
500
371
|
|
|
501
372
|
// Sync externally-started tunnel URL + PID into tunnelService
|
|
502
373
|
// so GET /api/tunnel reflects the correct state and Share button doesn't start a duplicate.
|
|
374
|
+
// Also write server version to status.json so supervisor heartbeat reports the actual running version.
|
|
503
375
|
try {
|
|
504
376
|
const { resolve: r } = await import("node:path");
|
|
505
377
|
const { homedir: h } = await import("node:os");
|
|
506
|
-
const { readFileSync: rf } = await import("node:fs");
|
|
378
|
+
const { readFileSync: rf, writeFileSync: wf } = await import("node:fs");
|
|
507
379
|
const statusFile = r(h(), ".ppm", "status.json");
|
|
508
380
|
const status = JSON.parse(rf(statusFile, "utf-8"));
|
|
381
|
+
// Write running server version — source of truth for heartbeat
|
|
382
|
+
status.serverVersion = VERSION;
|
|
383
|
+
wf(statusFile, JSON.stringify(status));
|
|
509
384
|
if (status.shareUrl) {
|
|
510
385
|
const { tunnelService } = await import("../services/tunnel.service.ts");
|
|
511
386
|
tunnelService.setExternalUrl(status.shareUrl);
|
|
@@ -525,6 +400,20 @@ if (process.argv.includes("__serve__")) {
|
|
|
525
400
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
526
401
|
}
|
|
527
402
|
|
|
403
|
+
if (url.pathname === "/ws/extensions") {
|
|
404
|
+
// Auth check for extension WS
|
|
405
|
+
const authConfig = configService.get("auth");
|
|
406
|
+
if (authConfig.enabled) {
|
|
407
|
+
const token = url.searchParams.get("token");
|
|
408
|
+
if (token !== authConfig.token) {
|
|
409
|
+
return new Response("Unauthorized", { status: 401 });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const upgraded = server.upgrade(req, { data: { type: "extensions" } });
|
|
413
|
+
if (upgraded) return undefined;
|
|
414
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
415
|
+
}
|
|
416
|
+
|
|
528
417
|
if (url.pathname.startsWith("/ws/project/")) {
|
|
529
418
|
const parts = url.pathname.split("/");
|
|
530
419
|
const projectName = parts[3] ?? "";
|
|
@@ -557,14 +446,17 @@ if (process.argv.includes("__serve__")) {
|
|
|
557
446
|
perMessageDeflate: false,
|
|
558
447
|
open(ws: any) {
|
|
559
448
|
if (ws.data?.type === "chat") chatWebSocket.open(ws);
|
|
449
|
+
else if (ws.data?.type === "extensions") extensionWebSocket.open(ws);
|
|
560
450
|
else terminalWebSocket.open(ws);
|
|
561
451
|
},
|
|
562
452
|
message(ws: any, msg: any) {
|
|
563
453
|
if (ws.data?.type === "chat") chatWebSocket.message(ws, msg);
|
|
454
|
+
else if (ws.data?.type === "extensions") extensionWebSocket.message(ws, msg);
|
|
564
455
|
else terminalWebSocket.message(ws, msg);
|
|
565
456
|
},
|
|
566
457
|
close(ws: any) {
|
|
567
458
|
if (ws.data?.type === "chat") chatWebSocket.close(ws);
|
|
459
|
+
else if (ws.data?.type === "extensions") extensionWebSocket.close(ws);
|
|
568
460
|
else terminalWebSocket.close(ws);
|
|
569
461
|
},
|
|
570
462
|
} as Parameters<typeof Bun.serve>[0] extends { websocket?: infer W } ? W : never,
|
|
@@ -573,5 +465,13 @@ if (process.argv.includes("__serve__")) {
|
|
|
573
465
|
// Start background account token refresh in daemon child
|
|
574
466
|
import("../services/account.service.ts").then(({ accountService }) => accountService.startAutoRefresh()).catch(() => {});
|
|
575
467
|
|
|
468
|
+
// Start background usage limit polling (every 5 min)
|
|
469
|
+
import("../services/claude-usage.service.ts").then(({ startUsagePolling }) => startUsagePolling()).catch(() => {});
|
|
470
|
+
|
|
471
|
+
// Discover + activate enabled extensions
|
|
472
|
+
import("../services/extension.service.ts").then(({ extensionService }) => extensionService.startup()).catch((e) => {
|
|
473
|
+
console.error("[ExtService] Startup error:", e);
|
|
474
|
+
});
|
|
475
|
+
|
|
576
476
|
console.log(`Server child ready on port ${port}`);
|
|
577
477
|
}
|
|
@@ -8,7 +8,7 @@ import { renameSession as sdkRenameSession } from "@anthropic-ai/claude-agent-sd
|
|
|
8
8
|
import { listSlashItems } from "../../services/slash-items.service.ts";
|
|
9
9
|
import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
|
|
10
10
|
import { getSessionLog } from "../../services/session-log.service.ts";
|
|
11
|
-
import { getSessionMapping, setSessionTitle } from "../../services/db.service.ts";
|
|
11
|
+
import { getSessionMapping, getSessionProjectPath, setSessionMapping, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionTitle } from "../../services/db.service.ts";
|
|
12
12
|
import { ok, err } from "../../types/api.ts";
|
|
13
13
|
|
|
14
14
|
type Env = { Variables: { projectPath: string; projectName: string } };
|
|
@@ -76,7 +76,16 @@ chatRoutes.get("/sessions", async (c) => {
|
|
|
76
76
|
const projectPath = c.get("projectPath");
|
|
77
77
|
const providerId = c.req.query("providerId");
|
|
78
78
|
const sessions = await chatService.listSessions(providerId, projectPath);
|
|
79
|
-
|
|
79
|
+
// Enrich with pin status
|
|
80
|
+
const pinnedIds = getPinnedSessionIds();
|
|
81
|
+
const enriched = sessions.map((s) => ({ ...s, pinned: pinnedIds.has(s.id) }));
|
|
82
|
+
// Sort: pinned first (by pinned_at implicit via Set order), then unpinned by createdAt
|
|
83
|
+
enriched.sort((a, b) => {
|
|
84
|
+
if (a.pinned && !b.pinned) return -1;
|
|
85
|
+
if (!a.pinned && b.pinned) return 1;
|
|
86
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
87
|
+
});
|
|
88
|
+
return c.json(ok(enriched));
|
|
80
89
|
} catch (e) {
|
|
81
90
|
return c.json(err((e as Error).message), 500);
|
|
82
91
|
}
|
|
@@ -116,7 +125,13 @@ chatRoutes.delete("/sessions/:id", async (c) => {
|
|
|
116
125
|
try {
|
|
117
126
|
const id = c.req.param("id");
|
|
118
127
|
const providerId = c.req.query("providerId") ?? "claude";
|
|
128
|
+
const sdkId = getSessionMapping(id) ?? id;
|
|
129
|
+
// Provider-specific cleanup (JSONL, process, etc.)
|
|
119
130
|
await chatService.deleteSession(providerId, id);
|
|
131
|
+
// Shared DB cleanup
|
|
132
|
+
deleteSessionMapping(id);
|
|
133
|
+
deleteSessionTitle(sdkId);
|
|
134
|
+
unpinSession(sdkId);
|
|
120
135
|
return c.json(ok({ deleted: id }));
|
|
121
136
|
} catch (e) {
|
|
122
137
|
return c.json(err((e as Error).message), 404);
|
|
@@ -146,6 +161,28 @@ chatRoutes.patch("/sessions/:id", async (c) => {
|
|
|
146
161
|
}
|
|
147
162
|
});
|
|
148
163
|
|
|
164
|
+
/** PUT /chat/sessions/:id/pin — pin a session */
|
|
165
|
+
chatRoutes.put("/sessions/:id/pin", (c) => {
|
|
166
|
+
try {
|
|
167
|
+
const id = c.req.param("id");
|
|
168
|
+
pinSession(id);
|
|
169
|
+
return c.json(ok({ id, pinned: true }));
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return c.json(err((e as Error).message), 500);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/** DELETE /chat/sessions/:id/pin — unpin a session */
|
|
176
|
+
chatRoutes.delete("/sessions/:id/pin", (c) => {
|
|
177
|
+
try {
|
|
178
|
+
const id = c.req.param("id");
|
|
179
|
+
unpinSession(id);
|
|
180
|
+
return c.json(ok({ id, pinned: false }));
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return c.json(err((e as Error).message), 500);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
149
186
|
/** POST /chat/sessions/:id/fork — fork session into a new one (for rewind/branch) */
|
|
150
187
|
chatRoutes.post("/sessions/:id/fork", async (c) => {
|
|
151
188
|
try {
|
|
@@ -153,16 +190,31 @@ chatRoutes.post("/sessions/:id/fork", async (c) => {
|
|
|
153
190
|
const projectName = c.get("projectName");
|
|
154
191
|
const projectPath = c.get("projectPath");
|
|
155
192
|
const providerId = c.req.query("providerId") ?? "claude";
|
|
156
|
-
|
|
157
|
-
const session = await chatService.createSession(providerId, {
|
|
158
|
-
projectName,
|
|
159
|
-
projectPath,
|
|
160
|
-
title: "Forked Chat",
|
|
161
|
-
});
|
|
162
|
-
// Store fork source so WS handler knows to use forkSession on first message
|
|
193
|
+
const body = await c.req.json<{ messageId?: string }>().catch(() => ({} as { messageId?: string }));
|
|
163
194
|
const provider = providerRegistry.get(providerId);
|
|
164
|
-
provider
|
|
165
|
-
|
|
195
|
+
if (!provider) return c.json(err("Provider not found"), 404);
|
|
196
|
+
|
|
197
|
+
if (body.messageId) {
|
|
198
|
+
// Mid-fork at a specific message
|
|
199
|
+
if (!provider.forkAtMessage) {
|
|
200
|
+
return c.json(err("Provider does not support forking"), 400);
|
|
201
|
+
}
|
|
202
|
+
const result = await provider.forkAtMessage(sourceId, body.messageId, {
|
|
203
|
+
title: "Forked Chat", dir: projectPath,
|
|
204
|
+
});
|
|
205
|
+
const session = await chatService.createSession(providerId, {
|
|
206
|
+
projectName, projectPath, title: "Forked Chat",
|
|
207
|
+
});
|
|
208
|
+
setSessionMapping(session.id, result.sessionId);
|
|
209
|
+
provider.markAsResumed?.(session.id);
|
|
210
|
+
return c.json(ok({ ...session, forkedFrom: sourceId }), 201);
|
|
211
|
+
} else {
|
|
212
|
+
// No messageId (fork at first message) — create a fresh empty session
|
|
213
|
+
const session = await chatService.createSession(providerId, {
|
|
214
|
+
projectName, projectPath, title: "Forked Chat",
|
|
215
|
+
});
|
|
216
|
+
return c.json(ok({ ...session, forkedFrom: sourceId }), 201);
|
|
217
|
+
}
|
|
166
218
|
} catch (e) {
|
|
167
219
|
return c.json(err((e as Error).message), 500);
|
|
168
220
|
}
|
|
@@ -180,6 +232,24 @@ chatRoutes.get("/sessions/:id/logs", (c) => {
|
|
|
180
232
|
}
|
|
181
233
|
});
|
|
182
234
|
|
|
235
|
+
/** GET /chat/sessions/:id/debug — session debug info (IDs, JSONL path) */
|
|
236
|
+
chatRoutes.get("/sessions/:id/debug", (c) => {
|
|
237
|
+
const ppmId = c.req.param("id");
|
|
238
|
+
const sdkId = getSessionMapping(ppmId) ?? ppmId;
|
|
239
|
+
// Resolve JSONL path: ~/.claude/projects/<encoded-cwd>/<sdkId>.jsonl
|
|
240
|
+
const homedir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
241
|
+
const provider = providerRegistry.get("claude") as any;
|
|
242
|
+
// Try in-memory first, fall back to DB-persisted project_path
|
|
243
|
+
const projectPath = provider?.activeSessions?.get(ppmId)?.projectPath
|
|
244
|
+
?? getSessionProjectPath(ppmId)
|
|
245
|
+
?? "";
|
|
246
|
+
const encodedCwd = projectPath ? projectPath.replace(/\//g, "-") : "";
|
|
247
|
+
const jsonlDir = encodedCwd ? resolve(homedir, ".claude", "projects", encodedCwd) : "";
|
|
248
|
+
const jsonlPath = jsonlDir ? resolve(jsonlDir, `${sdkId}.jsonl`) : "";
|
|
249
|
+
const jsonlExists = jsonlPath ? existsSync(jsonlPath) : false;
|
|
250
|
+
return c.json(ok({ ppmSessionId: ppmId, sdkSessionId: sdkId, jsonlPath: jsonlExists ? jsonlPath : null, jsonlDir, projectPath }));
|
|
251
|
+
});
|
|
252
|
+
|
|
183
253
|
/** POST /chat/upload — upload files for chat attachments, returns server-side paths */
|
|
184
254
|
chatRoutes.post("/upload", async (c) => {
|
|
185
255
|
try {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { extensionService } from "../../services/extension.service.ts";
|
|
3
|
+
import { contributionRegistry } from "../../services/contribution-registry.ts";
|
|
4
|
+
import { ok, err } from "../../types/api.ts";
|
|
5
|
+
|
|
6
|
+
export const extensionRoutes = new Hono();
|
|
7
|
+
|
|
8
|
+
// GET /api/extensions — list all extensions
|
|
9
|
+
extensionRoutes.get("/", (c) => {
|
|
10
|
+
const extensions = extensionService.list();
|
|
11
|
+
return c.json(ok(extensions));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// GET /api/extensions/contributions — all contribution points (for UI)
|
|
15
|
+
extensionRoutes.get("/contributions", (c) => {
|
|
16
|
+
return c.json(ok(contributionRegistry.getAll()));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// GET /api/extensions/:id — get single extension info
|
|
20
|
+
extensionRoutes.get("/:id{.+}", (c) => {
|
|
21
|
+
const id = c.req.param("id");
|
|
22
|
+
const ext = extensionService.get(id);
|
|
23
|
+
if (!ext) return c.json(err("Extension not found"), 404);
|
|
24
|
+
return c.json(ok(ext));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// POST /api/extensions/install — install extension
|
|
28
|
+
extensionRoutes.post("/install", async (c) => {
|
|
29
|
+
const body = await c.req.json<{ name?: string }>().catch(() => ({}) as { name?: string });
|
|
30
|
+
if (!body.name) return c.json(err("Missing 'name' field"), 400);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const manifest = await extensionService.install(body.name);
|
|
34
|
+
return c.json(ok(manifest), 201);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
37
|
+
return c.json(err(msg), 500);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// POST /api/extensions/dev-link — symlink local extension for development
|
|
42
|
+
extensionRoutes.post("/dev-link", async (c) => {
|
|
43
|
+
const body = await c.req.json<{ path?: string }>().catch(() => ({}) as { path?: string });
|
|
44
|
+
if (!body.path) return c.json(err("Missing 'path' field"), 400);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const manifest = await extensionService.devLink(body.path);
|
|
48
|
+
return c.json(ok(manifest), 201);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
51
|
+
return c.json(err(msg), 500);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// DELETE /api/extensions/:id — remove extension
|
|
56
|
+
extensionRoutes.delete("/:id{.+}", async (c) => {
|
|
57
|
+
const id = c.req.param("id");
|
|
58
|
+
try {
|
|
59
|
+
await extensionService.remove(id);
|
|
60
|
+
return c.json(ok({ removed: id }));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
63
|
+
return c.json(err(msg), 500);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// PATCH /api/extensions/:id — enable/disable
|
|
68
|
+
extensionRoutes.patch("/:id{.+}", async (c) => {
|
|
69
|
+
const id = c.req.param("id");
|
|
70
|
+
const body = await c.req.json<{ enabled?: boolean }>().catch(() => ({}) as { enabled?: boolean });
|
|
71
|
+
if (body.enabled === undefined) return c.json(err("Missing 'enabled' field"), 400);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await extensionService.setEnabled(id, body.enabled);
|
|
75
|
+
const ext = extensionService.get(id);
|
|
76
|
+
return c.json(ok(ext));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
79
|
+
return c.json(err(msg), 500);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
@@ -4,6 +4,7 @@ import { chatRoutes } from "./chat.ts";
|
|
|
4
4
|
import { gitRoutes } from "./git.ts";
|
|
5
5
|
import { fileRoutes } from "./files.ts";
|
|
6
6
|
import { sqliteRoutes } from "./sqlite.ts";
|
|
7
|
+
import { workspaceRoutes } from "./workspace.ts";
|
|
7
8
|
|
|
8
9
|
type Env = { Variables: { projectPath: string; projectName: string } };
|
|
9
10
|
|
|
@@ -27,3 +28,4 @@ projectScopedRouter.route("/chat", chatRoutes);
|
|
|
27
28
|
projectScopedRouter.route("/git", gitRoutes);
|
|
28
29
|
projectScopedRouter.route("/files", fileRoutes);
|
|
29
30
|
projectScopedRouter.route("/sqlite", sqliteRoutes);
|
|
31
|
+
projectScopedRouter.route("/workspace", workspaceRoutes);
|
|
@@ -252,6 +252,33 @@ settingsRoutes.post("/telegram/test", async (c) => {
|
|
|
252
252
|
}
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
+
// ── Auth / Password ──────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/** PUT /settings/auth/password — change the access password (token) */
|
|
258
|
+
settingsRoutes.put("/auth/password", async (c) => {
|
|
259
|
+
try {
|
|
260
|
+
const { password, confirm } = await c.req.json<{ password: string; confirm: string }>();
|
|
261
|
+
if (typeof password !== "string" || !password.trim()) {
|
|
262
|
+
return c.json(err("Password is required"), 400);
|
|
263
|
+
}
|
|
264
|
+
if (password !== confirm) {
|
|
265
|
+
return c.json(err("Passwords do not match"), 400);
|
|
266
|
+
}
|
|
267
|
+
const trimmed = password.trim();
|
|
268
|
+
if (trimmed.length < 4) {
|
|
269
|
+
return c.json(err("Password must be at least 4 characters"), 400);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const auth = configService.get("auth");
|
|
273
|
+
configService.set("auth", { ...auth, token: trimmed });
|
|
274
|
+
configService.save();
|
|
275
|
+
|
|
276
|
+
return c.json(ok({ token: trimmed }));
|
|
277
|
+
} catch (e) {
|
|
278
|
+
return c.json(err((e as Error).message), 400);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
255
282
|
// ── Proxy ────────────────────────────────────────────────────────────
|
|
256
283
|
|
|
257
284
|
/** GET /settings/proxy — proxy status */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { getWorkspace, setWorkspace } from "../../services/db.service.ts";
|
|
3
|
+
import { ok, err } from "../../types/api.ts";
|
|
4
|
+
|
|
5
|
+
type Env = { Variables: { projectPath: string; projectName: string } };
|
|
6
|
+
|
|
7
|
+
export const workspaceRoutes = new Hono<Env>();
|
|
8
|
+
|
|
9
|
+
/** GET /workspace — load saved workspace layout */
|
|
10
|
+
workspaceRoutes.get("/", (c) => {
|
|
11
|
+
try {
|
|
12
|
+
const projectName = c.get("projectName");
|
|
13
|
+
const row = getWorkspace(projectName);
|
|
14
|
+
if (!row) return c.json(ok(null));
|
|
15
|
+
return c.json(ok({
|
|
16
|
+
layout: JSON.parse(row.layout_json),
|
|
17
|
+
updatedAt: row.updated_at,
|
|
18
|
+
}));
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return c.json(err((e as Error).message), 500);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** PUT /workspace — save workspace layout */
|
|
25
|
+
workspaceRoutes.put("/", async (c) => {
|
|
26
|
+
try {
|
|
27
|
+
const projectName = c.get("projectName");
|
|
28
|
+
const body = await c.req.json<{ layout: unknown }>();
|
|
29
|
+
if (!body.layout) return c.json(err("Missing layout"), 400);
|
|
30
|
+
const updatedAt = setWorkspace(projectName, JSON.stringify(body.layout));
|
|
31
|
+
return c.json(ok({ updatedAt }));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return c.json(err((e as Error).message), 500);
|
|
34
|
+
}
|
|
35
|
+
});
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -11,7 +11,7 @@ const CLEANUP_TIMEOUT_MS = 5 * 60_000; // 5min after Claude done + no FE
|
|
|
11
11
|
const MAX_TURN_EVENTS = 10_000; // memory safety cap
|
|
12
12
|
const BUFFERABLE_TYPES = new Set([
|
|
13
13
|
"text", "thinking", "tool_use", "tool_result",
|
|
14
|
-
"approval_request", "error", "done", "account_info",
|
|
14
|
+
"approval_request", "error", "done", "account_info", "account_retry",
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
type ChatWsSocket = {
|
|
@@ -218,8 +218,14 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
218
218
|
continue;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// System events → transition connecting → thinking
|
|
221
|
+
// System events → transition connecting → thinking, forward compact events
|
|
222
222
|
if (evType === "system") {
|
|
223
|
+
const sub = (ev as any).subtype;
|
|
224
|
+
if (sub === "compacting") {
|
|
225
|
+
broadcast(sessionId, { type: "compact_status", status: "compacting" });
|
|
226
|
+
} else if (sub === "compact_done") {
|
|
227
|
+
broadcast(sessionId, { type: "compact_status", status: "done" });
|
|
228
|
+
}
|
|
223
229
|
if (!firstEventReceived) {
|
|
224
230
|
if (heartbeat) clearInterval(heartbeat);
|
|
225
231
|
setPhase(sessionId, "thinking");
|
|
@@ -228,7 +234,7 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
// First content event — stop heartbeat, transition phase
|
|
231
|
-
const isMetadataEvent = evType === "account_info" || evType === "streaming_status";
|
|
237
|
+
const isMetadataEvent = evType === "account_info" || evType === "account_retry" || evType === "streaming_status";
|
|
232
238
|
if (!firstEventReceived && !isMetadataEvent) {
|
|
233
239
|
firstEventReceived = true;
|
|
234
240
|
const waitMs = Date.now() - startTime;
|