@hienlh/ppm 0.9.0-beta.7 → 0.9.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/bun.lock +5 -0
- package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-3Xe18azI.js} +1 -1
- package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-Yy35llnn.js} +1 -1
- package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CEMxVMCV.js} +1 -1
- package/dist/web/assets/{arc-C2Qaz-ch.js → arc-B9n1Gvb5.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-DqAZP_F6.js} +1 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-h3cDF2vI.js} +1 -1
- package/dist/web/assets/{browser-tab-DAvH4mv0.js → browser-tab-D1Zua62g.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
- package/dist/web/assets/channel-C2fMafck.js +1 -0
- package/dist/web/assets/chat-tab-BnD27Vp9.js +7 -0
- package/dist/web/assets/chevron-right-CHnjJt4E.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-C3aZvW7B.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-tDjHsAUs.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-C7Gry6md.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-av5aeHLq.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +1 -0
- package/dist/web/assets/clone-B2hUek6n.js +1 -0
- package/dist/web/assets/code-editor-DGRg8stf.js +2 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-qudEiMCT.js} +1 -1
- package/dist/web/assets/csv-preview-DUbHtTAS.js +10 -0
- package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-BFcnKyBF.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
- package/dist/web/assets/database-viewer-DxCXZQcE.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
- package/dist/web/assets/diff-viewer-C1sDJG35.js +4 -0
- package/dist/web/assets/dist-CALwEtco.js +41 -0
- package/dist/web/assets/dist-DGDPTxs1.js +13 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
- package/dist/web/assets/git-graph-BDn-EiGE.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
- package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-Duh_bWLa.js} +1 -1
- package/dist/web/assets/index-Bun94AK3.js +37 -0
- package/dist/web/assets/index-Db8uky1a.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +2 -0
- package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-B9L-Ge-H.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
- package/dist/web/assets/keybindings-store-COmK4Dte.js +1 -0
- package/dist/web/assets/lib-BeaDXEkP.js +4 -0
- package/dist/web/assets/{line-DBLLF7lH.js → line-B75-Rx70.js} +1 -1
- package/dist/web/assets/{linear-BLFWatDe.js → linear-Bcjv9FQt.js} +1 -1
- package/dist/web/assets/markdown-renderer-VIZB1GXE.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-8u2leTXI.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
- package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-LFEjVtwQ.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
- package/dist/web/assets/postgres-viewer-CvQZ8gkh.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-CdjGIDfw.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +1 -0
- package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
- package/dist/web/assets/settings-tab-RCnvZ29H.js +1 -0
- package/dist/web/assets/sqlite-viewer-CEEm2W4C.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C4EMl6jf.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +1 -0
- package/dist/web/assets/{tab-store-DcIBZTD4.js → tab-store-Bjh6bXFP.js} +1 -1
- package/dist/web/assets/{terminal-tab-CAZtLK6i.js → terminal-tab-XhKfb4ei.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-A4PN_Efm.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +1 -0
- package/dist/web/assets/use-monaco-theme-0p0-84jJ.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
- package/dist/web/index.html +11 -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 +2 -2
- 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/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/account.service.ts +13 -8
- package/src/services/claude-usage.service.ts +37 -18
- package/src/services/cloud.service.ts +10 -6
- 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 +216 -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/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-BKIT_Qeg.js} +0 -0
- /package/dist/web/assets/{array-BGFCBI0e.js → array-DqLCdDFv.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-CWPXKqbJ.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-CrJzLgRD.js} +0 -0
- /package/dist/web/assets/{dist-Cce3efmT.js → dist-Cep75xXf.js} +0 -0
- /package/dist/web/assets/{init-B8gtcn7T.js → init-C0r9Gk5G.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
- /package/dist/web/assets/{katex-Bbu770d9.js → katex-DzXRfQ_m.js} +0 -0
- /package/dist/web/assets/{math-DwgHI-Cu.js → math-y9zN1W-N.js} +0 -0
- /package/dist/web/assets/{path-DZF-JdEe.js → path-DIKpVbHL.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-nHaDi0Kw.js} +0 -0
- /package/dist/web/assets/{src-BoSBNdA_.js → src-Dw4QhedI.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-DMiycH3O.js} +0 -0
|
@@ -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.9",
|
|
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);
|
|
@@ -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
|
};
|
|
@@ -57,8 +57,14 @@ class AccountSelectorService {
|
|
|
57
57
|
// Clear expired cooldowns
|
|
58
58
|
for (const acc of allAccounts) {
|
|
59
59
|
if (acc.status === "cooldown" && acc.cooldownUntil && acc.cooldownUntil <= now) {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
try {
|
|
61
|
+
accountService.setEnabled(acc.id);
|
|
62
|
+
this.retryCounts.delete(acc.id);
|
|
63
|
+
} catch {
|
|
64
|
+
// Account expired or cannot be re-enabled — disable it
|
|
65
|
+
accountService.setDisabled(acc.id);
|
|
66
|
+
this.retryCounts.delete(acc.id);
|
|
67
|
+
}
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -69,9 +69,14 @@ const OAUTH_TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
|
|
|
69
69
|
const OAUTH_SCOPE = "org:create_api_key user:profile user:inference";
|
|
70
70
|
const OAUTH_PLATFORM_REDIRECT = "https://platform.claude.com/oauth/code/callback";
|
|
71
71
|
|
|
72
|
+
// Survive Bun --hot reloads: persist timer ref across module re-evaluations
|
|
73
|
+
const ACCT_HOT_KEY = "__PPM_ACCT_REFRESH__" as const;
|
|
74
|
+
const acctHotState = ((globalThis as any)[ACCT_HOT_KEY] ??= {
|
|
75
|
+
refreshTimer: null as ReturnType<typeof setInterval> | null,
|
|
76
|
+
}) as { refreshTimer: ReturnType<typeof setInterval> | null };
|
|
77
|
+
|
|
72
78
|
class AccountService {
|
|
73
79
|
private pendingStates = new Map<string, { verifier: string; createdAt: number }>();
|
|
74
|
-
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
75
80
|
|
|
76
81
|
private toAccount(row: AccountRow): Account {
|
|
77
82
|
let profileData: OAuthProfileData | null = null;
|
|
@@ -689,7 +694,7 @@ class AccountService {
|
|
|
689
694
|
// ---------------------------------------------------------------------------
|
|
690
695
|
|
|
691
696
|
startAutoRefresh(): void {
|
|
692
|
-
if (
|
|
697
|
+
if (acctHotState.refreshTimer) return;
|
|
693
698
|
const CHECK_INTERVAL_MS = 5 * 60_000;
|
|
694
699
|
const REFRESH_BUFFER_S = 5 * 60;
|
|
695
700
|
|
|
@@ -729,20 +734,20 @@ class AccountService {
|
|
|
729
734
|
// Run immediately on startup, then every 5 minutes
|
|
730
735
|
refreshExpiring().catch(() => {});
|
|
731
736
|
cleanupExpiredTemporary();
|
|
732
|
-
|
|
737
|
+
acctHotState.refreshTimer = setInterval(() => {
|
|
733
738
|
refreshExpiring().catch(() => {});
|
|
734
739
|
cleanupExpiredTemporary();
|
|
735
740
|
}, CHECK_INTERVAL_MS);
|
|
736
741
|
|
|
737
|
-
if (typeof
|
|
738
|
-
(
|
|
742
|
+
if (typeof acctHotState.refreshTimer === "object" && acctHotState.refreshTimer !== null && "unref" in acctHotState.refreshTimer) {
|
|
743
|
+
(acctHotState.refreshTimer as NodeJS.Timeout).unref();
|
|
739
744
|
}
|
|
740
745
|
}
|
|
741
746
|
|
|
742
747
|
stopAutoRefresh(): void {
|
|
743
|
-
if (
|
|
744
|
-
clearInterval(
|
|
745
|
-
|
|
748
|
+
if (acctHotState.refreshTimer) {
|
|
749
|
+
clearInterval(acctHotState.refreshTimer);
|
|
750
|
+
acctHotState.refreshTimer = null;
|
|
746
751
|
}
|
|
747
752
|
}
|
|
748
753
|
}
|