@hienlh/ppm 0.9.0-beta.6 → 0.9.0-beta.8

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.

Potentially problematic release.


This version of @hienlh/ppm might be problematic. Click here for more details.

Files changed (182) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-5PGDJbfF.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  5. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-D21InCnR.js} +1 -1
  6. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-BAOivWpI.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
  9. package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
  10. package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
  11. package/dist/web/assets/browser-tab-BEe89aSD.js +1 -0
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
  13. package/dist/web/assets/channel-By7bn0Yq.js +1 -0
  14. package/dist/web/assets/chat-tab-9lqvWozA.js +7 -0
  15. package/dist/web/assets/chevron-right-CHnjJt4E.js +1 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
  45. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  46. package/dist/web/assets/code-editor-COAIZx-B.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
  48. package/dist/web/assets/csv-preview-DLqYtXxt.js +10 -0
  49. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-DHq9bhnd.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
  51. package/dist/web/assets/database-viewer-aRR9n_Ui.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
  55. package/dist/web/assets/diff-viewer-C4KMvpHr.js +4 -0
  56. package/dist/web/assets/dist-CALwEtco.js +41 -0
  57. package/dist/web/assets/dist-CVTST7Gc.js +1 -0
  58. package/dist/web/assets/dist-DGDPTxs1.js +13 -0
  59. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
  60. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
  61. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
  62. package/dist/web/assets/git-graph-CfJjl4E3.js +1 -0
  63. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  64. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
  65. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BcsNnGcW.js} +1 -1
  66. package/dist/web/assets/index-Db8uky1a.css +2 -0
  67. package/dist/web/assets/index-DxZuwBDe.js +37 -0
  68. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  69. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
  70. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-bnrF3Qbc.js} +1 -1
  71. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
  72. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
  73. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
  74. package/dist/web/assets/keybindings-store-_uWVCZMv.js +1 -0
  75. package/dist/web/assets/lib-BQ34Db2e.js +4 -0
  76. package/dist/web/assets/{line-DBLLF7lH.js → line-CVvo3dRu.js} +1 -1
  77. package/dist/web/assets/{linear-BLFWatDe.js → linear-DP4mkX3m.js} +1 -1
  78. package/dist/web/assets/markdown-renderer-DklUd_Gv.js +69 -0
  79. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
  80. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
  81. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-_K3x1fkz.js} +1 -1
  82. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
  83. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
  84. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
  85. package/dist/web/assets/postgres-viewer-DEAvAyaX.js +1 -0
  86. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
  87. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
  88. package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
  89. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
  90. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
  91. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
  92. package/dist/web/assets/settings-tab-BQedc-No.js +1 -0
  93. package/dist/web/assets/sqlite-viewer-BPA5idzT.js +1 -0
  94. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
  95. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
  96. package/dist/web/assets/{tab-store-DcIBZTD4.js → tab-store-DhK6EpBT.js} +1 -1
  97. package/dist/web/assets/{terminal-tab-CAZtLK6i.js → terminal-tab-CqRuiIFn.js} +2 -2
  98. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
  99. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  100. package/dist/web/assets/use-monaco-theme-Dcz3aLAE.js +11 -0
  101. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
  102. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
  103. package/dist/web/index.html +12 -10
  104. package/dist/web/sw.js +1 -1
  105. package/docs/codebase-summary.md +17 -5
  106. package/docs/design-guidelines.md +21 -0
  107. package/docs/project-changelog.md +28 -1
  108. package/docs/project-roadmap.md +41 -19
  109. package/docs/system-architecture.md +151 -0
  110. package/package.json +2 -1
  111. package/src/providers/claude-agent-sdk.ts +32 -10
  112. package/src/server/index.ts +6 -0
  113. package/src/server/routes/browser-preview.ts +2 -2
  114. package/src/server/routes/chat.ts +4 -2
  115. package/src/server/routes/mcp.ts +84 -0
  116. package/src/server/ws/chat.ts +18 -12
  117. package/src/services/account-selector.service.ts +8 -2
  118. package/src/services/claude-usage.service.ts +37 -11
  119. package/src/services/db.service.ts +53 -6
  120. package/src/services/mcp-config.service.ts +102 -0
  121. package/src/services/supervisor.ts +12 -2
  122. package/src/types/mcp.ts +47 -0
  123. package/src/web/components/editor/code-editor.tsx +36 -26
  124. package/src/web/components/editor/csv-preview.tsx +228 -0
  125. package/src/web/components/editor/editor-breadcrumb.tsx +225 -0
  126. package/src/web/components/editor/editor-toolbar.tsx +74 -0
  127. package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
  128. package/src/web/components/settings/mcp-settings-section.tsx +143 -0
  129. package/src/web/components/settings/settings-tab.tsx +5 -2
  130. package/src/web/lib/api-mcp.ts +38 -0
  131. package/src/web/lib/csv-parser.ts +134 -0
  132. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  133. package/dist/web/assets/browser-tab-DAvH4mv0.js +0 -1
  134. package/dist/web/assets/channel-w7yboq56.js +0 -1
  135. package/dist/web/assets/chat-tab-WEBXxGgN.js +0 -7
  136. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  137. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  138. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  139. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  140. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  141. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  142. package/dist/web/assets/code-editor-B5sg_uJQ.js +0 -1
  143. package/dist/web/assets/database-viewer-CwtyWCkE.js +0 -1
  144. package/dist/web/assets/diff-viewer-CzE5M-Wd.js +0 -4
  145. package/dist/web/assets/dist-T0Vhi0Mh.js +0 -16
  146. package/dist/web/assets/git-graph-6yxCeeN9.js +0 -1
  147. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  148. package/dist/web/assets/index-DE8b9u8F.css +0 -2
  149. package/dist/web/assets/index-wuWZBO9y.js +0 -37
  150. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  151. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  152. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  153. package/dist/web/assets/keybindings-store-mkBHnWN1.js +0 -1
  154. package/dist/web/assets/markdown-renderer-CxWxvrzT.js +0 -69
  155. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  156. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  157. package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +0 -1
  158. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  159. package/dist/web/assets/settings-store-Bbhg_ptG.js +0 -2
  160. package/dist/web/assets/settings-tab-BoBXlVHe.js +0 -1
  161. package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +0 -1
  162. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  163. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  164. package/dist/web/assets/use-monaco-theme-vwto-Vlf.js +0 -11
  165. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-BfBM3I7n.js} +0 -0
  166. /package/dist/web/assets/{array-BGFCBI0e.js → array-B9UHiPd-.js} +0 -0
  167. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DbesTfa7.js} +0 -0
  168. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-BW-DbntU.js} +0 -0
  169. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-5eAKkKJC.js} +0 -0
  170. /package/dist/web/assets/{dist-Cce3efmT.js → dist-CSJdAyA9.js} +0 -0
  171. /package/dist/web/assets/{init-B8gtcn7T.js → init-DlZdxViB.js} +0 -0
  172. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  173. /package/dist/web/assets/{katex-Bbu770d9.js → katex-Bqvo_ZG0.js} +0 -0
  174. /package/dist/web/assets/{math-DwgHI-Cu.js → math-069Z4SuC.js} +0 -0
  175. /package/dist/web/assets/{path-DZF-JdEe.js → path-6uRLdFF7.js} +0 -0
  176. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-Bf_JiD2A.js} +0 -0
  177. /package/dist/web/assets/{react-BGf7KNLk.js → react-SKk5z-bm.js} +0 -0
  178. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-JX0wREDd.js} +0 -0
  179. /package/dist/web/assets/{src-BoSBNdA_.js → src-BqX54PbV.js} +0 -0
  180. /package/dist/web/assets/{table-Yo02WRH-.js → table-CQVQM2SB.js} +0 -0
  181. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-Q2dZiSPX.js} +0 -0
  182. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  # PPM Project Roadmap
2
2
 
3
- **Last Updated:** March 22, 2026
3
+ **Last Updated:** March 27, 2026
4
4
 
5
5
  ## Vision
6
6
 
@@ -56,19 +56,26 @@ PPM is the **lightest path from phone to code** — a self-hosted, BYOK, multi-d
56
56
 
57
57
  ### v0.9.0 — "Open Platform" (Q2–Q3 2026)
58
58
 
59
- **Theme:** Multi-provider AI + extension system. Expand user base beyond Claude-only developers.
59
+ **Theme:** Multi-provider AI (Claude + Cursor) + extension system. Ship a focused release, expand providers later.
60
60
 
61
- | Feature | Priority | Description |
62
- |---------|----------|-------------|
63
- | **Multi-provider AI** | Critical | Refactor `ProviderInterface` for clean provider abstraction. Tiered support: Tier 1 (full agentic) = Claude Agent SDK; Tier 2 (chat + tools) = Gemini CLI, OpenAI Codex; Tier 3 (chat-only) = any OpenAI-compatible API. Clean base code for future Chinese providers (DeepSeek, Qwen). |
64
- | **Extension architecture** | High | Dynamic extension loading system. Extensions = npm packages exporting skills + optional UI panels. First extension: extract DB viewer from core. Extension API: register routes, UI panels, sidebar tabs, skills. Config: `"extensions": ["@ppm/ext-database", "@ppm/ext-docker"]`. |
65
- | **MCP Management** | Medium | UI to add/remove/configure MCP servers. Test connection. Per-project MCP overrides. Store in SQLite. Pass to Agent SDK via `mcpServers`. |
61
+ **Overall progress: ~53%** (2/3 features complete, merge all at 100%)
66
62
 
67
- **Multi-provider tiered approach:**
63
+ | Feature | Priority | Status | Description |
64
+ |---------|----------|--------|-------------|
65
+ | **Multi-provider AI** | Critical | ✅ Done | ProviderInterface, registry, Cursor CLI, CLI provider base, UI provider/model selector, permission mode selector, system prompt customization, comprehensive tests — all on beta branch. |
66
+ | **MCP Management** | Medium | ✅ Done | REST API (CRUD + import), SQLite storage, Settings UI, auto-import from `~/.claude.json`, validation, SDK integration. |
67
+ | **Extension architecture** | High | 🔴 0% | Dynamic extension loading. Extensions = npm packages. First extension: extract DB viewer from core. Extension API: register routes, UI panels, sidebar tabs, skills. Config: `"extensions": ["@ppm/ext-database", "@ppm/ext-docker"]`. |
68
+
69
+ **Multi-provider — v0.9 scope (reduced):**
68
70
  - Tier 1 (full agentic): Claude Agent SDK — file edit, terminal, git, full autonomy
69
- - Tier 2 (chat + tools): Provider-specific CLIs (Gemini CLI, Codex) — agentic via their own tool system
70
- - Tier 3 (chat-only): Any OpenAI-compatible API conversation only, no tools
71
- - Provider interface refactor is foundation work — do it clean now, avoid painful refactor later
71
+ - Tier 2 (agentic CLI): Cursor — agentic via its own tool system
72
+ - Provider interface is clean enough to add more providers later without refactor
73
+
74
+ **Deferred to v0.9.5+:**
75
+ - Gemini CLI (Tier 2)
76
+ - OpenAI Codex (Tier 2)
77
+ - Tier 3 (chat-only): Any OpenAI-compatible API
78
+ - Chinese providers (DeepSeek, Qwen) — v1.0+
72
79
 
73
80
  **Extension architecture — design principles:**
74
81
  - Extensions are npm packages: `ppm ext install @ppm/ext-database`
@@ -79,15 +86,27 @@ PPM is the **lightest path from phone to code** — a self-hosted, BYOK, multi-d
79
86
 
80
87
  ---
81
88
 
82
- ### v0.10.0 — "Intelligence" (Q3 2026)
89
+ ### v0.10.0 — "Enhanced Workflow" (Q3 2026)
83
90
 
84
- **Theme:** PPM's own AI layer. Built-in bot + programmable skills.
91
+ **Theme:** Chat UX upgrade + git workflow. High-impact, independent features that ship fast.
85
92
 
86
93
  | Feature | Priority | Description |
87
94
  |---------|----------|-------------|
95
+ | **Chat history graph** | High | Visual branching tree of chat sessions. Fork conversations, navigate history graph. Game-changer for AI chat UX. |
96
+ | **Worktree management** | Medium | UI to create/switch/delete git worktrees. Use different providers on different branches. Integrated with project switcher. |
97
+
98
+ ---
99
+
100
+ ### v0.11.0 — "Intelligence" (Q3–Q4 2026)
101
+
102
+ **Theme:** Event system + PPM's own AI layer. Hooks → Skills API → Clawbot dependency chain.
103
+
104
+ | Feature | Priority | Description |
105
+ |---------|----------|-------------|
106
+ | **Hooks system** | High | Event hooks for PPM lifecycle (file save, git commit, chat message, etc.). Foundation for Skills API and deeper extension integration. |
88
107
  | **PPM Skills API** | High | Stable internal API for AI to control PPM: file.read/write/search, terminal.run, git.status/commit/diff, db.query, editor.open/goto, project.switch. Skills are the bridge between AI and PPM features. |
89
108
  | **Built-in Clawbot** | High | Lightweight AI agent built into PPM using Anthropic Messages API (not Agent SDK). Uses Skills API + MCP tools. Instant response, no external CLI deps. For quick tasks: file search, code explanation, simple refactors. |
90
- | **Inline SQL** | Medium | Select text in Monaco run as SQL. Connection picker in editor context menu. Results panel below editor. Leverages existing DB service. |
109
+ | **More providers** | Medium | Gemini CLI (Tier 2), OpenAI Codex (Tier 2), Tier 3 chat-only (any OpenAI-compatible API). Provider interface already clean from v0.9. |
91
110
 
92
111
  **Built-in Clawbot — why it matters:**
93
112
  - Claude Agent SDK spawns subprocess — heavy, slow startup, requires CLI installed
@@ -106,6 +125,7 @@ PPM is the **lightest path from phone to code** — a self-hosted, BYOK, multi-d
106
125
  | **Self-hosted PPM Cloud** | High | Docker image of PPM Cloud for enterprise/team. Same codebase, self-hosted config flag. `docker-compose up` and it works. LDAP/SSO integration. |
107
126
  | **PPM Marketplace** | High | Publish/install/update extensions. Browse community extensions. Revenue sharing for paid extensions. Clawbot can create extension → test → publish in minutes. |
108
127
  | **Stability & hardening** | Critical | Security audit, performance optimization, comprehensive test coverage (>80%), documentation for contributors, CI/CD pipeline. |
128
+ | **Inline SQL** | Medium | Select text in Monaco → run as SQL. Connection picker in editor context menu. Results panel below editor. Leverages existing DB service. |
109
129
 
110
130
  ---
111
131
 
@@ -130,6 +150,7 @@ Features to pick from after v1.0. Will be reviewed and scheduled based on user f
130
150
  | **Cross-platform binaries** | Distribution | Compile macOS/Linux/Windows binaries via `bun build --compile`. `npx ppm` without Bun. |
131
151
  | **OLED dark mode** | UX | True black background for OLED screens. |
132
152
  | **Collaborative editing** | Social | Real-time multi-user file editing with CRDT (yjs/automerge). |
153
+ | **Custom domain** | Cloud | Map custom domain to PPM Cloud tunnel URL. DNS CNAME + SSL via Let's Encrypt or Cloudflare. Access PPM at `code.yourdomain.com`. |
133
154
 
134
155
  ---
135
156
 
@@ -139,9 +160,10 @@ Features to pick from after v1.0. Will be reviewed and scheduled based on user f
139
160
  |---------|-------|-------------|--------|
140
161
  | **v0.7** | Multi-Account & Mobile | Account management, usage tracking, mobile UX | ✅ Current |
141
162
  | **v0.8** | Always On | PPM Cloud, auto-start, AI chat enhancements | Q2 2026 |
142
- | **v0.9** | Open Platform | Multi-provider AI, extension architecture, MCP | Q2–Q3 2026 |
143
- | **v0.10** | Intelligence | Skills API, built-in Clawbot, inline SQL | Q3 2026 |
144
- | **v1.0** | Production Ready | Self-hosted Cloud, Marketplace, stability | Q4 2026 |
163
+ | **v0.9** | Open Platform | Multi-provider (Claude + Cursor), extension architecture, MCP | Q2–Q3 2026 |
164
+ | **v0.10** | Enhanced Workflow | Chat history graph, worktree management | Q3 2026 |
165
+ | **v0.11** | Intelligence | Hooks, Skills API, Clawbot, more providers (Gemini/Codex/Tier 3) | Q3–Q4 2026 |
166
+ | **v1.0** | Production Ready | Self-hosted Cloud, Marketplace, stability, inline SQL | Q4 2026 |
145
167
 
146
168
  ---
147
169
 
@@ -149,7 +171,7 @@ Features to pick from after v1.0. Will be reviewed and scheduled based on user f
149
171
 
150
172
  1. **Own "phone to code"** — PPM wins on multi-device access. Don't chase Cursor/Windsurf feature parity.
151
173
  2. **PPM Cloud stays razor-thin** — Device registry + tunnel URLs only. No code storage. No cloud execution.
152
- 3. **Multi-provider is tiered** — Full agentic = Claude SDK. Other providers get appropriate tier. Clean interface for future providers.
174
+ 3. **Multi-provider is tiered** — v0.9: Claude SDK (Tier 1) + Cursor (Tier 2). v0.11: Gemini, Codex, Tier 3. Clean interface for future providers.
153
175
  4. **Extensions keep core lightweight** — Features are opt-in. DB viewer, future tools = extensions. Core stays fast.
154
176
  5. **Clawbot enables the ecosystem** — Users create extensions with AI, publish to Marketplace. Zero-friction.
155
177
  6. **Self-hosted first, always** — Cloud is optional convenience. PPM works 100% offline/local.
@@ -160,7 +182,7 @@ Features to pick from after v1.0. Will be reviewed and scheduled based on user f
160
182
 
161
183
  | Item | Priority | Notes |
162
184
  |------|----------|-------|
163
- | Refactor ProviderInterface for multi-provider | High | Foundation for v0.9 |
185
+ | ~~Refactor ProviderInterface for multi-provider~~ | ~~High~~ | Done on beta branch (v0.9.0-beta.5) |
164
186
  | Simplify ChatService streaming | Medium | Reduce async generator complexity |
165
187
  | Extract WebSocket common logic | Low | DRY for chat/terminal WS |
166
188
  | Round-robin cursor bug in AccountSelector | Medium | Positional cursor not advancing correctly |
@@ -945,6 +945,157 @@ ppm db data <name> <table> # Show table data (paginated)
945
945
 
946
946
  ---
947
947
 
948
+ ## MCP Server Management
949
+
950
+ ### Overview
951
+ MCP (Model Context Protocol) servers extend Claude with custom tools and resources. PPM manages MCP server configurations via Settings UI, storing them in SQLite and passing them to the Claude Agent SDK.
952
+
953
+ **Features:**
954
+ - **Add/Edit/Delete** MCP servers via Settings UI
955
+ - **Auto-import** from `~/.claude.json` on first access (convenience, no forced import)
956
+ - **Three transport types:** stdio, HTTP, SSE
957
+ - **Validation** on name and config before storage
958
+ - **SDK integration:** Servers passed to `query()` as `mcpServers` object, tools auto-allowed via `mcp__*` wildcard
959
+
960
+ ### Storage Schema
961
+
962
+ ```sql
963
+ CREATE TABLE mcp_servers (
964
+ name TEXT PRIMARY KEY,
965
+ transport TEXT NOT NULL DEFAULT 'stdio', -- 'stdio' | 'http' | 'sse'
966
+ config TEXT NOT NULL, -- JSON: McpServerConfig
967
+ created_at TEXT DEFAULT (datetime('now')),
968
+ updated_at TEXT DEFAULT (datetime('now'))
969
+ );
970
+ ```
971
+
972
+ **Config Format (JSON):**
973
+ ```json
974
+ {
975
+ "type": "stdio",
976
+ "command": "path/to/server",
977
+ "args": ["--flag"],
978
+ "env": { "VAR": "value" }
979
+ }
980
+ ```
981
+
982
+ Or HTTP/SSE:
983
+ ```json
984
+ {
985
+ "type": "http",
986
+ "url": "http://localhost:3000",
987
+ "headers": { "Authorization": "Bearer token" }
988
+ }
989
+ ```
990
+
991
+ ### REST API
992
+
993
+ **Endpoints** (`src/server/routes/mcp.ts`):
994
+
995
+ | Method | Endpoint | Description |
996
+ |--------|----------|-------------|
997
+ | **GET** | `/api/settings/mcp` | List all servers; auto-import on first access |
998
+ | **GET** | `/api/settings/mcp/:name` | Get single server config |
999
+ | **POST** | `/api/settings/mcp` | Add new server (validates name + config) |
1000
+ | **PUT** | `/api/settings/mcp/:name` | Update existing server |
1001
+ | **DELETE** | `/api/settings/mcp/:name` | Remove server |
1002
+ | **GET** | `/api/settings/mcp/import/preview` | Preview servers in `~/.claude.json` |
1003
+ | **POST** | `/api/settings/mcp/import` | Bulk import from `~/.claude.json` |
1004
+
1005
+ **Add Server Example:**
1006
+ ```bash
1007
+ POST /api/settings/mcp
1008
+ Content-Type: application/json
1009
+
1010
+ {
1011
+ "name": "file-server",
1012
+ "config": {
1013
+ "type": "stdio",
1014
+ "command": "/usr/local/bin/file-server",
1015
+ "args": ["--port", "8000"]
1016
+ }
1017
+ }
1018
+ ```
1019
+
1020
+ ### Service Layer
1021
+
1022
+ **McpConfigService** (`src/services/mcp-config.service.ts`):
1023
+ - `list()` — Record<name, McpServerConfig> (SDK-compatible format)
1024
+ - `listWithMeta()` — Array with metadata (for UI)
1025
+ - `get(name)` — Single server config
1026
+ - `set(name, config)` — Add or update (upsert)
1027
+ - `remove(name)` — Delete server
1028
+ - `exists(name)` — Check if name exists
1029
+ - `bulkImport(servers)` — Transactional import from `~/.claude.json`, skips existing/invalid
1030
+
1031
+ **Validation:**
1032
+ - `validateMcpName(name)` — alphanumeric + hyphens/underscores, max 50 chars
1033
+ - `validateMcpConfig(config)` — type-specific checks (command for stdio, url for http/sse)
1034
+
1035
+ ### Frontend Integration
1036
+
1037
+ **UI Components:**
1038
+ - `MCP Settings Section` (`src/web/components/settings/mcp-settings-section.tsx`) — Tab in Settings UI
1039
+ - `MCP Server Dialog` (`src/web/components/settings/mcp-server-dialog.tsx`) — Add/Edit modal
1040
+ - `API client` (`src/web/lib/api-mcp.ts`) — Fetch/mutate operations
1041
+
1042
+ **Workflow:**
1043
+ 1. User opens Settings → MCP tab
1044
+ 2. **GET** `/api/settings/mcp` (auto-imports on first access)
1045
+ 3. Display list with transport badge + actions (edit, delete)
1046
+ 4. Click "Add" → Dialog with name + transport selector + config fields
1047
+ 5. **POST** to `/api/settings/mcp` or **PUT** to update
1048
+ 6. On success, list refreshes
1049
+
1050
+ ### SDK Integration
1051
+
1052
+ **Claude Agent SDK Provider** (`src/providers/claude-agent-sdk.ts`):
1053
+ ```typescript
1054
+ // Line ~574
1055
+ const mcpServers = mcpConfigService.list();
1056
+ const hasMcp = Object.keys(mcpServers).length > 0;
1057
+
1058
+ // Line ~589: Pass to query() if servers exist
1059
+ const mcpTools = ["mcp__*"];
1060
+ const queryConfig = {
1061
+ // ... other options
1062
+ ...(hasMcp && { mcpServers }),
1063
+ allowedTools: [...otherTools, ...mcpTools],
1064
+ };
1065
+
1066
+ const query = new Query(messages, queryConfig);
1067
+ ```
1068
+
1069
+ **Tool Allow List:**
1070
+ - All MCP tools automatically allowed via wildcard `mcp__*`
1071
+ - MCP server connection failures don't block chat (logged as warning)
1072
+
1073
+ ### Import Flow
1074
+
1075
+ **Auto-import on first access:**
1076
+ 1. GET `/api/settings/mcp` called
1077
+ 2. If table is empty, read `~/.claude.json`
1078
+ 3. If `mcpServers` key exists, bulk import (validate + skip duplicates)
1079
+ 4. Return populated list
1080
+
1081
+ **Manual import:**
1082
+ 1. GET `/api/settings/mcp/import/preview` — show what's available
1083
+ 2. POST `/api/settings/mcp/import` — import validated servers
1084
+ 3. Returns `{ imported: N, skipped: M }`
1085
+
1086
+ ### Error Handling
1087
+
1088
+ | Scenario | Response |
1089
+ |----------|----------|
1090
+ | Invalid name (non-alphanumeric) | 400 Bad Request |
1091
+ | Invalid config (missing required fields) | 400 Bad Request |
1092
+ | Duplicate name | 409 Conflict |
1093
+ | Server not found (GET/:name, PUT/:name, DELETE/:name) | 404 Not Found |
1094
+ | `~/.claude.json` not found (import) | 404 Not Found |
1095
+ | Corrupt config JSON (recovery) | Log warning, skip entry, continue |
1096
+
1097
+ ---
1098
+
948
1099
  ## Deployment Architecture
949
1100
 
950
1101
  ### Single-Machine Deployment (Current)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.0-beta.6",
3
+ "version": "0.9.0-beta.8",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "@radix-ui/react-switch": "^1.2.6",
46
46
  "@skitee3000/bun-pty": "^0.3.3",
47
47
  "@tanstack/react-table": "^8.21.3",
48
+ "@tanstack/react-virtual": "^3.13.23",
48
49
  "@uiw/react-codemirror": "^4.25.8",
49
50
  "@xterm/addon-fit": "^0.11.0",
50
51
  "@xterm/addon-web-links": "^0.12.0",
@@ -13,8 +13,9 @@ import type {
13
13
  ModelOption,
14
14
  } from "./provider.interface.ts";
15
15
  import { configService } from "../services/config.service.ts";
16
+ import { mcpConfigService } from "../services/mcp-config.service.ts";
16
17
  import { updateFromSdkEvent } from "../services/claude-usage.service.ts";
17
- import { getSessionMapping, setSessionMapping } from "../services/db.service.ts";
18
+ import { getSessionMapping, setSessionMapping, getSessionTitles } from "../services/db.service.ts";
18
19
  import { accountSelector } from "../services/account-selector.service.ts";
19
20
  import { accountService } from "../services/account.service.ts";
20
21
  import { resolve } from "node:path";
@@ -281,10 +282,13 @@ export class ClaudeAgentSdkProvider implements AIProvider {
281
282
  async listSessionsByDir(dir?: string): Promise<SessionInfo[]> {
282
283
  try {
283
284
  const sdkSessions = await sdkListSessions({ dir, limit: 50 });
285
+ // Overlay DB titles (user-set) over SDK titles
286
+ const ids = sdkSessions.map((s) => s.sessionId);
287
+ const dbTitles = getSessionTitles(ids);
284
288
  return sdkSessions.map((s) => ({
285
289
  id: s.sessionId,
286
290
  providerId: this.id,
287
- title: s.customTitle ?? s.summary ?? s.firstPrompt ?? "Chat",
291
+ title: dbTitles[s.sessionId] ?? s.customTitle ?? s.summary ?? s.firstPrompt ?? "Chat",
288
292
  createdAt: new Date(s.lastModified).toISOString(),
289
293
  updatedAt: new Date(s.lastModified).toISOString(),
290
294
  }));
@@ -430,9 +434,10 @@ export class ClaudeAgentSdkProvider implements AIProvider {
430
434
  // go through the permission evaluation chain → canUseTool callback.
431
435
  const readOnlyTools = ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "ToolSearch"];
432
436
  const writeTools = ["Write", "Edit", "Bash", "Agent", "Skill", "TodoWrite", "AskUserQuestion"];
437
+ const mcpTools = ["mcp__*"];
433
438
  const allowedTools = isBypass
434
- ? [...readOnlyTools, ...writeTools]
435
- : readOnlyTools;
439
+ ? [...readOnlyTools, ...writeTools, ...mcpTools]
440
+ : [...readOnlyTools, ...mcpTools];
436
441
 
437
442
  /**
438
443
  * Approval events to yield from the generator.
@@ -568,6 +573,10 @@ export class ClaudeAgentSdkProvider implements AIProvider {
568
573
  }
569
574
  console.log(`[sdk] query: session=${sessionId} sdkId=${sdkId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${effectiveCwd} platform=${process.platform} accountMode=${!!account} permissionMode=${permissionMode} isBypass=${isBypass}`);
570
575
 
576
+ // Read MCP servers from PPM DB (fresh per query — user may add/remove between chats)
577
+ const mcpServers = mcpConfigService.list();
578
+ const hasMcp = Object.keys(mcpServers).length > 0;
579
+
571
580
  const queryOptions: Record<string, any> = {
572
581
  // On Windows, child_process.spawn("bun") fails with ENOENT — force node
573
582
  ...(process.platform === "win32" && { executable: "node" }),
@@ -580,6 +589,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
580
589
  env: queryEnv,
581
590
  settings: { permissions: { allow: [], deny: [] } },
582
591
  allowedTools,
592
+ ...(hasMcp && { mcpServers }),
583
593
  permissionMode,
584
594
  allowDangerouslySkipPermissions: isBypass,
585
595
  ...(providerConfig.model && { model: providerConfig.model }),
@@ -925,7 +935,16 @@ export class ClaudeAgentSdkProvider implements AIProvider {
925
935
  }
926
936
 
927
937
  // Surface non-success subtypes as errors so FE can display them
938
+ // But suppress abort errors — user-initiated cancel is not a real error
928
939
  if (subtype && subtype !== "success") {
940
+ const errorsArr0 = Array.isArray(result.errors) ? result.errors : [];
941
+ const abortDetail = errorsArr0.join(" ") + " " + (typeof result.error === "string" ? result.error : "");
942
+ if (subtype === "error_during_execution" && /abort|request was aborted/i.test(abortDetail)) {
943
+ console.log(`[sdk] session=${sessionId} suppressing abort error (user-initiated cancel)`);
944
+ resultSubtype = subtype;
945
+ resultNumTurns = result.num_turns as number | undefined;
946
+ break;
947
+ }
929
948
  // SDK error results use `errors: string[]` array (not singular `error`)
930
949
  const errorsArr = Array.isArray(result.errors) ? result.errors : [];
931
950
  const sdkDetail = errorsArr.length > 0
@@ -1050,20 +1069,23 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1050
1069
  }
1051
1070
 
1052
1071
 
1053
- /** Interrupt the current turn session stays alive for the next message */
1072
+ /** Abort and fully teardown the streaming session user must resume to continue */
1054
1073
  abortQuery(sessionId: string): void {
1055
1074
  const ss = this.streamingSessions.get(sessionId);
1056
- if (ss && typeof ss.query.interrupt === "function") {
1057
- ss.query.interrupt().catch(() => {});
1058
- console.log(`[sdk] abortQuery: interrupted session=${sessionId}`);
1075
+ if (ss) {
1076
+ // Signal generator to end, then close the query (kills bun subprocess)
1077
+ ss.controller.done();
1078
+ ss.query.close();
1079
+ this.streamingSessions.delete(sessionId);
1080
+ this.activeQueries.delete(sessionId);
1081
+ console.log(`[sdk] abortQuery: closed streaming session=${sessionId}`);
1059
1082
  return;
1060
1083
  }
1061
- // Fallback: close query entirely and clean up streaming session
1084
+ // Non-streaming fallback
1062
1085
  const q = this.activeQueries.get(sessionId);
1063
1086
  if (q) {
1064
1087
  q.close();
1065
1088
  this.activeQueries.delete(sessionId);
1066
- this.streamingSessions.delete(sessionId);
1067
1089
  }
1068
1090
  }
1069
1091
 
@@ -14,6 +14,7 @@ import { databaseRoutes } from "./routes/database.ts";
14
14
  import { fsBrowseRoutes } from "./routes/fs-browse.ts";
15
15
  import { accountsRoutes } from "./routes/accounts.ts";
16
16
  import { proxyRoutes } from "./routes/proxy.ts";
17
+ import { mcpRoutes } from "./routes/mcp.ts";
17
18
  import { browserPreviewRoutes } from "./routes/browser-preview.ts";
18
19
  import { initAdapters } from "../services/database/init-adapters.ts";
19
20
  import { terminalWebSocket } from "./ws/terminal.ts";
@@ -22,6 +23,10 @@ import { ok, err } from "../types/api.ts";
22
23
 
23
24
  /** Tee console.log/error to ~/.ppm/ppm.log while preserving terminal output */
24
25
  async function setupLogFile() {
26
+ // Guard: prevent re-wrapping console on hot-reload (bun --hot re-executes the module)
27
+ if ((globalThis as any).__PPM_LOG_SETUP__) return;
28
+ (globalThis as any).__PPM_LOG_SETUP__ = true;
29
+
25
30
  const { resolve } = await import("node:path");
26
31
  const { homedir } = await import("node:os");
27
32
  const { appendFileSync, mkdirSync, existsSync } = await import("node:fs");
@@ -135,6 +140,7 @@ app.route("/api/fs", fsBrowseRoutes);
135
140
 
136
141
  // API routes
137
142
  app.route("/api/settings", settingsRoutes);
143
+ app.route("/api/settings/mcp", mcpRoutes);
138
144
  app.route("/api/tunnel", tunnelRoutes);
139
145
  app.route("/api/push", pushRoutes);
140
146
  app.route("/api/projects", projectRoutes);
@@ -21,8 +21,8 @@ interface ActiveTunnel {
21
21
  startedAt: number;
22
22
  }
23
23
 
24
- /** Active tunnels keyed by port */
25
- const activeTunnels = new Map<number, ActiveTunnel>();
24
+ /** Active tunnels keyed by port — exported for testing */
25
+ export const activeTunnels = new Map<number, ActiveTunnel>();
26
26
 
27
27
  /** Start a tunnel for a localhost port */
28
28
  browserPreviewRoutes.post("/tunnel", async (c) => {
@@ -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 } from "../../services/db.service.ts";
11
+ import { getSessionMapping, setSessionTitle } 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 } };
@@ -133,7 +133,9 @@ chatRoutes.patch("/sessions/:id", async (c) => {
133
133
  // Resolve PPM UUID → SDK session ID if mapped
134
134
  const sdkId = getSessionMapping(id) ?? id;
135
135
  const projectPath = c.get("projectPath");
136
- // Persist to SDK so Claude Code CLI also sees the custom title
136
+ // Persist to PPM DB (authoritative source for user-set titles)
137
+ setSessionTitle(sdkId, title);
138
+ // Also persist to SDK so Claude Code CLI sees the custom title
137
139
  await sdkRenameSession(sdkId, title, { dir: projectPath });
138
140
  // Also update in-memory session
139
141
  const session = chatService.getSession(id);
@@ -0,0 +1,84 @@
1
+ import { Hono } from "hono";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { mcpConfigService } from "../../services/mcp-config.service";
6
+ import { validateMcpName, validateMcpConfig, type McpServerConfig } from "../../types/mcp";
7
+ import { ok, err } from "../../types/api";
8
+
9
+ export const mcpRoutes = new Hono();
10
+
11
+ const CLAUDE_CONFIG = join(homedir(), ".claude.json");
12
+
13
+ function readClaudeMcpServers(): Record<string, unknown> | null {
14
+ if (!existsSync(CLAUDE_CONFIG)) return null;
15
+ try {
16
+ const data = JSON.parse(readFileSync(CLAUDE_CONFIG, "utf-8"));
17
+ return data.mcpServers ?? null;
18
+ } catch { return null; }
19
+ }
20
+
21
+ // GET / — list all (auto-imports from ~/.claude.json on first access if table empty)
22
+ mcpRoutes.get("/", (c) => {
23
+ let servers = mcpConfigService.listWithMeta();
24
+ if (servers.length === 0) {
25
+ const claudeServers = readClaudeMcpServers();
26
+ if (claudeServers && Object.keys(claudeServers).length > 0) {
27
+ mcpConfigService.bulkImport(claudeServers as Record<string, McpServerConfig>);
28
+ servers = mcpConfigService.listWithMeta();
29
+ }
30
+ }
31
+ return c.json(ok(servers));
32
+ });
33
+
34
+ // GET /import/preview — show what would be imported
35
+ mcpRoutes.get("/import/preview", (c) => {
36
+ const servers = readClaudeMcpServers();
37
+ if (!servers) return c.json(ok({ available: false, servers: {} }));
38
+ return c.json(ok({ available: true, servers }));
39
+ });
40
+
41
+ // POST /import — import from ~/.claude.json
42
+ mcpRoutes.post("/import", (c) => {
43
+ const servers = readClaudeMcpServers();
44
+ if (!servers) return c.json(err("~/.claude.json not found or has no mcpServers"), 404);
45
+ const result = mcpConfigService.bulkImport(servers as Record<string, McpServerConfig>);
46
+ return c.json(ok(result));
47
+ });
48
+
49
+ // GET /:name — single server
50
+ mcpRoutes.get("/:name", (c) => {
51
+ const config = mcpConfigService.get(c.req.param("name"));
52
+ if (!config) return c.json(err("Server not found"), 404);
53
+ return c.json(ok(config));
54
+ });
55
+
56
+ // POST / — add new server
57
+ mcpRoutes.post("/", async (c) => {
58
+ const { name, config } = await c.req.json();
59
+ const nameErr = validateMcpName(name);
60
+ if (nameErr) return c.json(err(nameErr), 400);
61
+ const configErrs = validateMcpConfig(config);
62
+ if (configErrs.length) return c.json(err(configErrs.join("; ")), 400);
63
+ if (mcpConfigService.exists(name)) return c.json(err("Server already exists"), 409);
64
+ mcpConfigService.set(name, config);
65
+ return c.json(ok({ name }), 201);
66
+ });
67
+
68
+ // PUT /:name — update server config
69
+ mcpRoutes.put("/:name", async (c) => {
70
+ const name = c.req.param("name");
71
+ if (!mcpConfigService.exists(name)) return c.json(err("Server not found"), 404);
72
+ const config = await c.req.json();
73
+ const configErrs = validateMcpConfig(config);
74
+ if (configErrs.length) return c.json(err(configErrs.join("; ")), 400);
75
+ mcpConfigService.set(name, config);
76
+ return c.json(ok({ name }));
77
+ });
78
+
79
+ // DELETE /:name — remove server
80
+ mcpRoutes.delete("/:name", (c) => {
81
+ const removed = mcpConfigService.remove(c.req.param("name"));
82
+ if (!removed) return c.json(err("Server not found"), 404);
83
+ return c.json(ok(true));
84
+ });
@@ -3,6 +3,7 @@ import { providerRegistry } from "../../providers/registry.ts";
3
3
  import { resolveProjectPath } from "../helpers/resolve-project.ts";
4
4
  import { logSessionEvent } from "../../services/session-log.service.ts";
5
5
  import { listSessions as sdkListSessions } from "@anthropic-ai/claude-agent-sdk";
6
+ import { getSessionTitle } from "../../services/db.service.ts";
6
7
  import type { ChatWsClientMessage, SessionPhase } from "../../types/api.ts";
7
8
 
8
9
  const PING_INTERVAL_MS = 15_000; // 15s keepalive
@@ -118,11 +119,12 @@ function clearClientPing(entry: SessionEntry, ws: ChatWsSocket): void {
118
119
  }
119
120
  }
120
121
 
121
- /** Start cleanup timer — only called when Claude is done AND no FE connected */
122
- function startCleanupTimer(sessionId: string): void {
122
+ /** Start cleanup timer — called when no FE connected. Urgent mode (30s) for orphaned streaming sessions. */
123
+ function startCleanupTimer(sessionId: string, urgent = false): void {
123
124
  const entry = activeSessions.get(sessionId);
124
125
  if (!entry) return;
125
126
  if (entry.cleanupTimer) clearTimeout(entry.cleanupTimer);
127
+ const delay = urgent ? 30_000 : CLEANUP_TIMEOUT_MS;
126
128
  entry.cleanupTimer = setTimeout(() => {
127
129
  console.log(`[chat] session=${sessionId} cleanup: no FE reconnected within timeout`);
128
130
  logSessionEvent(sessionId, "INFO", "Session cleaned up (no FE reconnected)");
@@ -134,7 +136,7 @@ function startCleanupTimer(sessionId: string): void {
134
136
  for (const interval of entry.pingIntervals.values()) clearInterval(interval);
135
137
  entry.pingIntervals.clear();
136
138
  activeSessions.delete(sessionId);
137
- }, CLEANUP_TIMEOUT_MS);
139
+ }, delay);
138
140
  }
139
141
 
140
142
  /**
@@ -259,10 +261,11 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
259
261
  logSessionEvent(sessionId, "DONE", `subtype=${ev.resultSubtype ?? "none"} turns=${ev.numTurns ?? "?"} ctx=${ev.contextWindowPct ?? "?"}%`);
260
262
  if (ev.contextWindowPct != null) lastContextWindowPct = ev.contextWindowPct;
261
263
 
262
- // Fire-and-forget: title + notification
264
+ // Fire-and-forget: fetch updated session title (DB title takes priority) + notification
263
265
  sdkListSessions({ dir: entry.projectPath, limit: 50 }).then((sessions) => {
264
266
  const found = sessions.find((s) => s.sessionId === sessionId || s.sessionId === ev.sessionId);
265
- const title = found?.customTitle ?? found?.summary;
267
+ const dbTitle = getSessionTitle(found?.sessionId ?? sessionId);
268
+ const title = dbTitle ?? found?.customTitle ?? found?.summary;
266
269
  if (title) {
267
270
  broadcast(sessionId, { type: "title_updated", title });
268
271
  const session = chatService.getSession(sessionId);
@@ -386,11 +389,12 @@ export const chatWebSocket = {
386
389
  existing.clients.add(ws);
387
390
  setupClientPing(existing, ws);
388
391
 
389
- // Async: resolve title from SDK if in-memory title is generic
392
+ // Async: resolve title from SDK if in-memory title is generic (DB title takes priority)
390
393
  if (!session?.title || session.title === "Chat" || session.title === "Resumed Chat") {
391
394
  sdkListSessions({ dir: projectPath, limit: 50 }).then((sessions) => {
392
395
  const found = sessions.find((s) => s.sessionId === sessionId);
393
- const title = found?.customTitle ?? found?.summary;
396
+ const dbTitle = getSessionTitle(found?.sessionId ?? sessionId);
397
+ const title = dbTitle ?? found?.customTitle ?? found?.summary;
394
398
  if (title) {
395
399
  broadcast(sessionId, { type: "title_updated", title });
396
400
  if (session) session.title = title;
@@ -423,11 +427,12 @@ export const chatWebSocket = {
423
427
  sessionTitle: session?.title || null,
424
428
  }));
425
429
 
426
- // Async: resolve title from SDK if in-memory title is generic
430
+ // Async: resolve title from SDK if in-memory title is generic (DB title takes priority)
427
431
  if (!session?.title || session.title === "Chat" || session.title === "Resumed Chat") {
428
432
  sdkListSessions({ dir: projectPath, limit: 50 }).then((sessions) => {
429
433
  const found = sessions.find((s) => s.sessionId === sessionId);
430
- const title = found?.customTitle ?? found?.summary;
434
+ const dbTitle = getSessionTitle(found?.sessionId ?? sessionId);
435
+ const title = dbTitle ?? found?.customTitle ?? found?.summary;
431
436
  if (title) {
432
437
  broadcast(sessionId, { type: "title_updated", title });
433
438
  if (session) session.title = title;
@@ -564,7 +569,7 @@ export const chatWebSocket = {
564
569
  console.log(`[chat] session=${sessionId} follow-up pushed to generator`);
565
570
  }
566
571
  } else if (parsed.type === "cancel") {
567
- // Interrupt current turnsession stays alive for next message
572
+ // Fully teardown streaming session user must resume to continue
568
573
  const provider = providerRegistry.get(providerId);
569
574
  provider?.abortQuery?.(sessionId);
570
575
  } else if (parsed.type === "approval_response") {
@@ -589,8 +594,9 @@ export const chatWebSocket = {
589
594
  evictClient(entry, ws);
590
595
  console.log(`[chat] session=${sessionId} FE disconnected (phase=${entry.phase}, clients=${entry.clients.size})`);
591
596
 
592
- if (entry.clients.size === 0 && !entry.isStreamingActive) {
593
- startCleanupTimer(sessionId);
597
+ if (entry.clients.size === 0) {
598
+ // Use shorter timeout if streaming is still active (orphaned session — no FE to consume events)
599
+ startCleanupTimer(sessionId, entry.isStreamingActive);
594
600
  }
595
601
  },
596
602
  };