@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.
- package/CHANGELOG.md +22 -0
- package/bun.lock +5 -0
- package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-D21InCnR.js} +1 -1
- package/dist/web/assets/{arc-C2Qaz-ch.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
- package/dist/web/assets/browser-tab-BEe89aSD.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
- package/dist/web/assets/channel-By7bn0Yq.js +1 -0
- package/dist/web/assets/chat-tab-9lqvWozA.js +7 -0
- package/dist/web/assets/chevron-right-CHnjJt4E.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.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-BSrqCL_3.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.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-COAIZx-B.js +2 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/csv-preview-DLqYtXxt.js +10 -0
- package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-DHq9bhnd.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
- package/dist/web/assets/database-viewer-aRR9n_Ui.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
- package/dist/web/assets/diff-viewer-C4KMvpHr.js +4 -0
- package/dist/web/assets/dist-CALwEtco.js +41 -0
- package/dist/web/assets/dist-CVTST7Gc.js +1 -0
- package/dist/web/assets/dist-DGDPTxs1.js +13 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
- package/dist/web/assets/git-graph-CfJjl4E3.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
- package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-Db8uky1a.css +2 -0
- package/dist/web/assets/index-DxZuwBDe.js +37 -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-C0YYdhYj.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
- package/dist/web/assets/keybindings-store-_uWVCZMv.js +1 -0
- package/dist/web/assets/lib-BQ34Db2e.js +4 -0
- package/dist/web/assets/{line-DBLLF7lH.js → line-CVvo3dRu.js} +1 -1
- package/dist/web/assets/{linear-BLFWatDe.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/markdown-renderer-DklUd_Gv.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
- package/dist/web/assets/{ordinal-CCj7PWgZ.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-Bkh2E4zE.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
- package/dist/web/assets/postgres-viewer-DEAvAyaX.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
- package/dist/web/assets/settings-tab-BQedc-No.js +1 -0
- package/dist/web/assets/sqlite-viewer-BPA5idzT.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.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-DcIBZTD4.js → tab-store-DhK6EpBT.js} +1 -1
- package/dist/web/assets/{terminal-tab-CAZtLK6i.js → terminal-tab-CqRuiIFn.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.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-Dcz3aLAE.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
- package/dist/web/index.html +12 -10
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +17 -5
- package/docs/design-guidelines.md +21 -0
- package/docs/project-changelog.md +28 -1
- package/docs/project-roadmap.md +41 -19
- package/docs/system-architecture.md +151 -0
- package/package.json +2 -1
- package/src/providers/claude-agent-sdk.ts +32 -10
- package/src/server/index.ts +6 -0
- package/src/server/routes/browser-preview.ts +2 -2
- package/src/server/routes/chat.ts +4 -2
- package/src/server/routes/mcp.ts +84 -0
- package/src/server/ws/chat.ts +18 -12
- package/src/services/account-selector.service.ts +8 -2
- package/src/services/claude-usage.service.ts +37 -11
- package/src/services/db.service.ts +53 -6
- package/src/services/mcp-config.service.ts +102 -0
- package/src/services/supervisor.ts +12 -2
- package/src/types/mcp.ts +47 -0
- package/src/web/components/editor/code-editor.tsx +36 -26
- package/src/web/components/editor/csv-preview.tsx +228 -0
- package/src/web/components/editor/editor-breadcrumb.tsx +225 -0
- package/src/web/components/editor/editor-toolbar.tsx +74 -0
- package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
- package/src/web/components/settings/mcp-settings-section.tsx +143 -0
- package/src/web/components/settings/settings-tab.tsx +5 -2
- package/src/web/lib/api-mcp.ts +38 -0
- package/src/web/lib/csv-parser.ts +134 -0
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
- package/dist/web/assets/browser-tab-DAvH4mv0.js +0 -1
- package/dist/web/assets/channel-w7yboq56.js +0 -1
- package/dist/web/assets/chat-tab-WEBXxGgN.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
- package/dist/web/assets/clone-BSi6cgDh.js +0 -1
- package/dist/web/assets/code-editor-B5sg_uJQ.js +0 -1
- package/dist/web/assets/database-viewer-CwtyWCkE.js +0 -1
- package/dist/web/assets/diff-viewer-CzE5M-Wd.js +0 -4
- package/dist/web/assets/dist-T0Vhi0Mh.js +0 -16
- package/dist/web/assets/git-graph-6yxCeeN9.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
- package/dist/web/assets/index-DE8b9u8F.css +0 -2
- package/dist/web/assets/index-wuWZBO9y.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
- package/dist/web/assets/input-Brjz2Vv-.js +0 -41
- package/dist/web/assets/keybindings-store-mkBHnWN1.js +0 -1
- package/dist/web/assets/markdown-renderer-CxWxvrzT.js +0 -69
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
- package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
- package/dist/web/assets/settings-store-Bbhg_ptG.js +0 -2
- package/dist/web/assets/settings-tab-BoBXlVHe.js +0 -1
- package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
- package/dist/web/assets/use-monaco-theme-vwto-Vlf.js +0 -11
- /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-BGFCBI0e.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DbesTfa7.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-Cce3efmT.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-B8gtcn7T.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-Bbu770d9.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{math-DwgHI-Cu.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-DZF-JdEe.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-Bf_JiD2A.js} +0 -0
- /package/dist/web/assets/{react-BGf7KNLk.js → react-SKk5z-bm.js} +0 -0
- /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-BoSBNdA_.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{table-Yo02WRH-.js → table-CQVQM2SB.js} +0 -0
- /package/dist/web/assets/{tag-CaC1ng2E.js → tag-Q2dZiSPX.js} +0 -0
- /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.js} +0 -0
package/docs/project-roadmap.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PPM Project Roadmap
|
|
2
2
|
|
|
3
|
-
**Last Updated:** March
|
|
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.
|
|
59
|
+
**Theme:** Multi-provider AI (Claude + Cursor) + extension system. Ship a focused release, expand providers later.
|
|
60
60
|
|
|
61
|
-
|
|
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
|
-
|
|
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 (
|
|
70
|
-
-
|
|
71
|
-
|
|
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 — "
|
|
89
|
+
### v0.10.0 — "Enhanced Workflow" (Q3 2026)
|
|
83
90
|
|
|
84
|
-
**Theme:**
|
|
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
|
-
| **
|
|
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
|
|
143
|
-
| **v0.10** |
|
|
144
|
-
| **
|
|
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** —
|
|
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 |
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
//
|
|
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
|
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
|
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
|
+
});
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -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 —
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
593
|
-
|
|
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
|
};
|