@hienlh/ppm 0.8.85 → 0.8.86

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.
Files changed (224) hide show
  1. package/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
  2. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
  3. package/CHANGELOG.md +5 -187
  4. package/bun.lock +0 -5
  5. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-5eBmZ_lt.js} +1 -1
  6. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-DimLlN0y.js} +1 -1
  7. package/dist/web/assets/api-settings-CFw-lh5k.js +1 -0
  8. package/dist/web/assets/{arc-BAOivWpI.js → arc-D4SasZrA.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-nv0WbM7d.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-C1XvYrb8.js} +1 -1
  12. package/dist/web/assets/browser-tab-CmsL5eny.js +1 -0
  13. package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW-CygDrbWJ.js} +1 -1
  14. package/dist/web/assets/channel-DmKoFTd_.js +1 -0
  15. package/dist/web/assets/chat-tab-CFWsf13Z.js +7 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C2FDgsgT.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-jF4w6cat.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-BVCECZFi.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-DXTbeu5d.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BaZqOsTs.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Bky2tcH7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Cp4BK9A8.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-BosFEH7G.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-H5Gbjsbr.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-CWerSUwS.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-FvwP7jUy.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-D1PI_ORP.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C7Vzk_AI.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-BceYBGYX.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-WPtzgxql.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-DlHXDeLY.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-Ci_Prygb.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-CO0zMN-z.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-IAEEzfpM.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-BLXalOgc.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dx1Ri_p2.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-m9pPGKn7.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-B_08ExbI.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-DqSOVcYe.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +1 -0
  45. package/dist/web/assets/clone-CijCFRT5.js +1 -0
  46. package/dist/web/assets/code-editor-H_dAh_fJ.js +1 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-DlL82QHu.js} +1 -1
  48. package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BmVoh2At.js} +1 -1
  49. package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-sDrRW9MQ.js} +1 -1
  50. package/dist/web/assets/database-viewer-DBzsgEJ8.js +1 -0
  51. package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-ChnAhgni.js} +1 -1
  52. package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-DW1J1uJd.js} +1 -1
  53. package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-CQ32hyG_.js} +1 -1
  54. package/dist/web/assets/diff-viewer-DzS-OnAR.js +4 -0
  55. package/dist/web/assets/dist-0Va_2L7G.js +16 -0
  56. package/dist/web/assets/dist-D9irYETY.js +41 -0
  57. package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-6CHo6nOw.js} +1 -1
  58. package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-DroDiNT0.js} +1 -1
  59. package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-DP0QBh8w.js} +1 -1
  60. package/dist/web/assets/git-graph-D3C7F8o3.js +1 -0
  61. package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +1 -0
  62. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js} +1 -1
  63. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CQBb2thr.js} +1 -1
  64. package/dist/web/assets/index-CIkjfera.js +31 -0
  65. package/dist/web/assets/index-WKLuYsBY.css +2 -0
  66. package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +1 -0
  67. package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +2 -0
  68. package/dist/web/assets/input-CGp1nFIg.js +1 -0
  69. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B4kqZBtn.js} +1 -1
  70. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-46yibrV5.js} +1 -1
  71. package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-BcmRwjK-.js} +1 -1
  72. package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-B619K53y.js} +1 -1
  73. package/dist/web/assets/keybindings-store-BdaoLwSo.js +1 -0
  74. package/dist/web/assets/{line-B78g-52T.js → line-1gcO63_w.js} +1 -1
  75. package/dist/web/assets/{linear-DP4mkX3m.js → linear-DfRqDoVd.js} +1 -1
  76. package/dist/web/assets/markdown-renderer-DH49Zag7.js +69 -0
  77. package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-XtjZQOeM.js} +2 -2
  78. package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-CifOFo_q.js} +1 -1
  79. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-BJYw-iDX.js} +1 -1
  80. package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +1 -0
  81. package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +1 -0
  82. package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-BuHUh_fO.js} +1 -1
  83. package/dist/web/assets/postgres-viewer-B9FYk8sD.js +1 -0
  84. package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-Bau_hj6Z.js} +1 -1
  85. package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +1 -0
  86. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-Cq2b-uwp.js} +1 -1
  87. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js} +1 -1
  88. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-qPxiTUcS.js} +1 -1
  89. package/dist/web/assets/settings-store-DWXGVHsE.js +2 -0
  90. package/dist/web/assets/settings-tab-D-q8pd-5.js +1 -0
  91. package/dist/web/assets/sqlite-viewer-CDqcTePw.js +1 -0
  92. package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-Dulj2oa8.js} +1 -1
  93. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +1 -0
  94. package/dist/web/assets/tab-store-BPeiymiH.js +1 -0
  95. package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-wKgpSPAT.js} +2 -2
  96. package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-BWyDnCYq.js} +1 -1
  97. package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +1 -0
  98. package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +11 -0
  99. package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-B9Iv2bNV.js} +1 -1
  100. package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-ChXcMzBQ.js} +1 -1
  101. package/dist/web/index.html +11 -12
  102. package/dist/web/sw.js +1 -1
  103. package/docs/code-standards.md +7 -232
  104. package/docs/codebase-summary.md +3 -9
  105. package/docs/design-guidelines.md +0 -21
  106. package/docs/project-changelog.md +1 -115
  107. package/docs/project-roadmap.md +19 -41
  108. package/docs/system-architecture.md +15 -212
  109. package/package.json +2 -3
  110. package/src/cli/commands/autostart.ts +1 -1
  111. package/src/cli/commands/restart.ts +1 -9
  112. package/src/cli/commands/status.ts +0 -19
  113. package/src/index.ts +3 -2
  114. package/src/providers/claude-agent-sdk.ts +31 -94
  115. package/src/providers/mock-provider.ts +1 -6
  116. package/src/server/index.ts +166 -38
  117. package/src/server/routes/chat.ts +3 -52
  118. package/src/server/routes/project-scoped.ts +0 -2
  119. package/src/server/routes/proxy.ts +53 -46
  120. package/src/server/routes/tunnel.ts +32 -0
  121. package/src/server/ws/chat.ts +146 -207
  122. package/src/services/account-selector.service.ts +8 -16
  123. package/src/services/account.service.ts +13 -19
  124. package/src/services/claude-usage.service.ts +11 -48
  125. package/src/services/cloud.service.ts +6 -10
  126. package/src/services/db.service.ts +6 -111
  127. package/src/services/port-tunnel.service.ts +97 -0
  128. package/src/services/proxy.service.ts +19 -4
  129. package/src/services/supervisor.ts +25 -285
  130. package/src/types/api.ts +1 -9
  131. package/src/types/chat.ts +1 -3
  132. package/src/web/app.tsx +35 -41
  133. package/src/web/components/browser/browser-tab.tsx +97 -106
  134. package/src/web/components/chat/chat-history-bar.tsx +6 -72
  135. package/src/web/components/chat/chat-tab.tsx +16 -32
  136. package/src/web/components/chat/message-input.tsx +13 -107
  137. package/src/web/components/chat/message-list.tsx +15 -27
  138. package/src/web/components/chat/session-picker.tsx +31 -78
  139. package/src/web/components/chat/usage-badge.tsx +1 -11
  140. package/src/web/components/editor/code-editor.tsx +26 -36
  141. package/src/web/components/layout/command-palette.tsx +1 -3
  142. package/src/web/components/layout/editor-panel.tsx +18 -162
  143. package/src/web/components/layout/panel-layout.tsx +1 -17
  144. package/src/web/components/settings/proxy-settings-section.tsx +42 -40
  145. package/src/web/hooks/use-chat.ts +201 -211
  146. package/src/web/hooks/use-global-keybindings.ts +2 -25
  147. package/src/web/hooks/use-server-reload.ts +0 -9
  148. package/src/web/hooks/use-url-sync.ts +21 -173
  149. package/src/web/stores/keybindings-store.ts +0 -1
  150. package/src/web/stores/panel-store.ts +19 -73
  151. package/src/web/stores/panel-utils.ts +3 -145
  152. package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
  153. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  154. package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
  155. package/dist/web/assets/browser-tab-DaHGm_0i.js +0 -1
  156. package/dist/web/assets/channel-wrd-NHWf.js +0 -1
  157. package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
  158. package/dist/web/assets/chevron-right-DeV0ehiG.js +0 -1
  159. package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
  160. package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
  161. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  162. package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
  163. package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
  164. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  165. package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
  166. package/dist/web/assets/csv-preview-DLqYtXxt.js +0 -10
  167. package/dist/web/assets/database-viewer-DXk79Nel.js +0 -1
  168. package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
  169. package/dist/web/assets/dist-DylI9XxN.js +0 -13
  170. package/dist/web/assets/dist-lF8CoYII.js +0 -41
  171. package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
  172. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  173. package/dist/web/assets/index-CgQXpBb_.css +0 -2
  174. package/dist/web/assets/index-DEeeRoka.js +0 -37
  175. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  176. package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
  177. package/dist/web/assets/input-BglMT33g.js +0 -1
  178. package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
  179. package/dist/web/assets/lib-BQ34Db2e.js +0 -4
  180. package/dist/web/assets/markdown-renderer-Brj8_LQM.js +0 -69
  181. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  182. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  183. package/dist/web/assets/postgres-viewer-CwkTGmqy.js +0 -1
  184. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  185. package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
  186. package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
  187. package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +0 -1
  188. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
  189. package/dist/web/assets/tab-store-BJw7OCmy.js +0 -1
  190. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  191. package/dist/web/assets/use-monaco-theme-CNzekTN3.js +0 -11
  192. package/docs/streaming-input-guide.md +0 -267
  193. package/snapshot-state.md +0 -1526
  194. package/src/server/routes/browser-preview.ts +0 -159
  195. package/src/server/routes/workspace.ts +0 -35
  196. package/src/services/cloud-ws.service.ts +0 -227
  197. package/src/web/components/chat/account-rotation-settings.tsx +0 -163
  198. package/src/web/components/chat/chat-welcome.tsx +0 -148
  199. package/src/web/components/editor/csv-preview.tsx +0 -228
  200. package/src/web/components/editor/editor-breadcrumb.tsx +0 -216
  201. package/src/web/components/editor/editor-toolbar.tsx +0 -74
  202. package/src/web/components/shared/connection-lost-overlay.tsx +0 -89
  203. package/src/web/hooks/use-voice-input.ts +0 -111
  204. package/src/web/lib/csv-parser.ts +0 -134
  205. package/src/web/stores/connection-store.ts +0 -39
  206. package/test-tokens.mjs +0 -212
  207. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-DOElml5u.js} +0 -0
  208. /package/dist/web/assets/{array-B9UHiPd-.js → array-CYkMkqnU.js} +0 -0
  209. /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-ChOTgl3e.js} +0 -0
  210. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-HeHO0VhB.js} +0 -0
  211. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-Beh6XjaL.js} +0 -0
  212. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-BUYzeuKe.js} +0 -0
  213. /package/dist/web/assets/{init-DlZdxViB.js → init-Rr1s_RiX.js} +0 -0
  214. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BB-mzMLb.js} +0 -0
  215. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-CKoArbIw.js} +0 -0
  216. /package/dist/web/assets/{math-069Z4SuC.js → math-B7b0HgJF.js} +0 -0
  217. /package/dist/web/assets/{path-6uRLdFF7.js → path-BAQ3hXlG.js} +0 -0
  218. /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-DeiOTZKJ.js} +0 -0
  219. /package/dist/web/assets/{react-ER-4DN55.js → react-Dev-wu-s.js} +0 -0
  220. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-Dwml_la6.js} +0 -0
  221. /package/dist/web/assets/{src-BqX54PbV.js → src-B_cC68fH.js} +0 -0
  222. /package/dist/web/assets/{table-C7X5UAEI.js → table-COiJDPRA.js} +0 -0
  223. /package/dist/web/assets/{tag-CCtdV063.js → tag-LMq02LfE.js} +0 -0
  224. /package/dist/web/assets/{utils-BNytJOb1.js → utils-btZ8C8-R.js} +0 -0
@@ -13,8 +13,7 @@ import { postgresRoutes } from "./routes/postgres.ts";
13
13
  import { databaseRoutes } from "./routes/database.ts";
14
14
  import { fsBrowseRoutes } from "./routes/fs-browse.ts";
15
15
  import { accountsRoutes } from "./routes/accounts.ts";
16
- import { proxyRoutes } from "./routes/proxy.ts";
17
- import { browserPreviewRoutes } from "./routes/browser-preview.ts";
16
+ import { proxyRoutes, handleProxyRequest } from "./routes/proxy.ts";
18
17
  import { initAdapters } from "../services/database/init-adapters.ts";
19
18
  import { terminalWebSocket } from "./ws/terminal.ts";
20
19
  import { chatWebSocket } from "./ws/chat.ts";
@@ -22,10 +21,6 @@ import { ok, err } from "../types/api.ts";
22
21
 
23
22
  /** Tee console.log/error to ~/.ppm/ppm.log while preserving terminal output */
24
23
  async function setupLogFile() {
25
- // Guard: prevent re-wrapping console on hot-reload (bun --hot re-executes the module)
26
- if ((globalThis as any).__PPM_LOG_SETUP__) return;
27
- (globalThis as any).__PPM_LOG_SETUP__ = true;
28
-
29
24
  const { resolve } = await import("node:path");
30
25
  const { homedir } = await import("node:os");
31
26
  const { appendFileSync, mkdirSync, existsSync } = await import("node:fs");
@@ -131,9 +126,6 @@ app.route("/proxy", proxyRoutes);
131
126
  app.use("/api/*", authMiddleware);
132
127
  app.get("/api/auth/check", (c) => c.json(ok(true)));
133
128
 
134
- // Browser preview reverse proxy — proxies to localhost:<port> for iframe embedding
135
- app.route("/api/preview", browserPreviewRoutes);
136
-
137
129
  // Filesystem operations (browse, list, read, write) — consolidated in fs-browse route
138
130
  app.route("/api/fs", fsBrowseRoutes);
139
131
 
@@ -160,39 +152,47 @@ app.route("/", staticRoutes);
160
152
 
161
153
  export async function startServer(options: {
162
154
  port?: string;
155
+ foreground?: boolean;
156
+ daemon?: boolean; // compat, ignored (daemon is now default)
163
157
  share?: boolean;
164
158
  config?: string;
165
159
  profile?: string;
166
160
  }) {
167
- // Tunnel always enabled — cloudflared shares the server publicly
168
- options.share = true;
169
-
170
161
  // Load config
171
162
  configService.load(options.config);
172
163
  const port = parseInt(options.port ?? String(configService.get("port")), 10);
173
164
  const host = configService.get("host");
174
165
 
166
+ // Setup log file (both foreground and daemon modes)
175
167
  await setupLogFile();
176
168
 
177
- // Check if port is already in use before spawning supervisor
178
- const portInUse = await new Promise<boolean>((resolve) => {
179
- const net = require("node:net") as typeof import("node:net");
180
- const tester = net.createServer()
181
- .once("error", (err: NodeJS.ErrnoException) => {
182
- resolve(err.code === "EADDRINUSE");
183
- })
184
- .once("listening", () => {
185
- tester.close(() => resolve(false));
186
- })
187
- .listen(port, host);
188
- });
189
- if (portInUse) {
190
- console.error(`\n ✗ Port ${port} is already in use.`);
191
- console.error(` Run 'ppm stop' first or use a different port with --port.\n`);
192
- process.exit(1);
169
+ // Check if port is already in use before starting.
170
+ // Skip in hot-reload mode — Bun.serve() replaces the previous server on the same port,
171
+ // but a net.createServer() probe would see it as "in use" and exit prematurely.
172
+ // globalThis persists across bun --hot reloads, so we use a flag set after first start.
173
+ const isHotReload = !!(globalThis as any).__PPM_SERVER_STARTED__;
174
+ if (!isHotReload) {
175
+ const portInUse = await new Promise<boolean>((resolve) => {
176
+ const net = require("node:net") as typeof import("node:net");
177
+ const tester = net.createServer()
178
+ .once("error", (err: NodeJS.ErrnoException) => {
179
+ resolve(err.code === "EADDRINUSE");
180
+ })
181
+ .once("listening", () => {
182
+ tester.close(() => resolve(false));
183
+ })
184
+ .listen(port, host);
185
+ });
186
+ if (portInUse) {
187
+ console.error(`\n ✗ Port ${port} is already in use.`);
188
+ console.error(` Run 'ppm stop' first or use a different port with --port.\n`);
189
+ process.exit(1);
190
+ }
193
191
  }
194
192
 
195
- {
193
+ const isDaemon = !options.foreground;
194
+
195
+ if (isDaemon) {
196
196
  const { resolve } = await import("node:path");
197
197
  const { homedir } = await import("node:os");
198
198
  const { writeFileSync, readFileSync, mkdirSync, existsSync, openSync } = await import("node:fs");
@@ -258,6 +258,7 @@ export async function startServer(options: {
258
258
  if (isNaN(supervisorPid)) {
259
259
  console.error(" ✗ Failed to start supervisor on Windows.");
260
260
  console.error(` ${result.stderr.toString().trim()}`);
261
+ console.error(" Try: ppm start -f (foreground mode)");
261
262
  process.exit(1);
262
263
  }
263
264
  } else {
@@ -282,6 +283,7 @@ export async function startServer(options: {
282
283
  try { process.kill(supervisorPid, 0); } catch {
283
284
  console.error(" ✗ Supervisor exited immediately after start.");
284
285
  console.error(" Check logs: ppm logs");
286
+ console.error(" Or try: ppm start -f (foreground mode)");
285
287
  process.exit(1);
286
288
  }
287
289
  // Check if server PID appeared in status.json
@@ -337,6 +339,133 @@ export async function startServer(options: {
337
339
 
338
340
  process.exit(0);
339
341
  }
342
+
343
+ // Foreground mode — with WebSocket support
344
+ const server = Bun.serve({
345
+ port,
346
+ hostname: host,
347
+ async fetch(req, server) {
348
+ const url = new URL(req.url);
349
+
350
+ // Proxy: handle before Hono to avoid SPA catch-all conflict
351
+ if (url.pathname.startsWith("/proxy")) {
352
+ const proxyRes = await handleProxyRequest(req);
353
+ if (proxyRes) return proxyRes;
354
+ }
355
+
356
+ // WebSocket upgrade: /ws/project/:projectName/terminal/:id
357
+ if (url.pathname.startsWith("/ws/project/")) {
358
+ const parts = url.pathname.split("/");
359
+ const projectName = parts[3] ?? "";
360
+ const wsType = parts[4] ?? "";
361
+ const id = parts[5] ?? "";
362
+
363
+ if (wsType === "terminal") {
364
+ const upgraded = server.upgrade(req, {
365
+ data: { type: "terminal", id, projectName },
366
+ });
367
+ if (upgraded) return undefined;
368
+ return new Response("WebSocket upgrade failed", { status: 400 });
369
+ }
370
+
371
+ if (wsType === "chat") {
372
+ const sessionId = id;
373
+ const upgraded = server.upgrade(req, {
374
+ data: { type: "chat", sessionId, projectName },
375
+ });
376
+ if (upgraded) return undefined;
377
+ return new Response("WebSocket upgrade failed", { status: 400 });
378
+ }
379
+ }
380
+
381
+ return app.fetch(req, server);
382
+ },
383
+ websocket: {
384
+ idleTimeout: 960,
385
+ sendPong: true,
386
+ perMessageDeflate: false, // Disable compression — Cloudflare tunnels can mangle compressed frames
387
+ open(ws: any) {
388
+ if (ws.data?.type === "health") {
389
+ ws.send(JSON.stringify({ type: "health", status: "ok" }));
390
+ } else if (ws.data?.type === "chat") chatWebSocket.open(ws);
391
+ else terminalWebSocket.open(ws);
392
+ },
393
+ message(ws: any, msg: any) {
394
+ if (ws.data?.type === "health") {
395
+ // Respond to ping with pong
396
+ ws.send(JSON.stringify({ type: "health", status: "ok" }));
397
+ } else if (ws.data?.type === "chat") chatWebSocket.message(ws, msg);
398
+ else terminalWebSocket.message(ws, msg);
399
+ },
400
+ close(ws: any) {
401
+ if (ws.data?.type === "health") return;
402
+ if (ws.data?.type === "chat") chatWebSocket.close(ws);
403
+ else terminalWebSocket.close(ws);
404
+ },
405
+ } as Parameters<typeof Bun.serve>[0] extends { websocket?: infer W } ? W : never,
406
+ });
407
+
408
+ // Mark server as started — survives bun --hot reloads (globalThis persists)
409
+ (globalThis as any).__PPM_SERVER_STARTED__ = true;
410
+
411
+ // Start background usage polling
412
+ import("../services/claude-usage.service.ts").then(({ startUsagePolling }) => startUsagePolling()).catch(() => {});
413
+
414
+ // Start background account token refresh
415
+ import("../services/account.service.ts").then(({ accountService }) => accountService.startAutoRefresh()).catch(() => {});
416
+
417
+ console.log(`\n PPM ready\n`);
418
+ console.log(` ➜ Local: http://localhost:${server.port}/`);
419
+
420
+ const { networkInterfaces } = await import("node:os");
421
+ const nets = networkInterfaces();
422
+ for (const name of Object.keys(nets)) {
423
+ for (const net of nets[name] ?? []) {
424
+ if (net.family === "IPv4" && !net.internal) {
425
+ console.log(` ➜ Network: http://${net.address}:${server.port}/`);
426
+ }
427
+ }
428
+ }
429
+
430
+ // Share tunnel in foreground mode
431
+ if (options.share) {
432
+ try {
433
+ const { tunnelService } = await import("../services/tunnel.service.ts");
434
+ console.log("\n Starting share tunnel...");
435
+ const shareUrl = await tunnelService.startTunnel(server.port!);
436
+ console.log(` ➜ Share: ${shareUrl}`);
437
+ if (!configService.get("auth").enabled) {
438
+ console.log(`\n ⚠ Warning: auth is disabled — your IDE is publicly accessible!`);
439
+ console.log(` Enable auth: run 'ppm config set auth.enabled true' or restart without --share.`);
440
+ }
441
+ const qr = await import("qrcode-terminal");
442
+ console.log();
443
+ qr.generate(shareUrl, { small: true });
444
+ } catch (err: unknown) {
445
+ const msg = err instanceof Error ? err.message : String(err);
446
+ console.error(` ✗ Share failed: ${msg}`);
447
+ }
448
+ }
449
+
450
+ console.log(`\n Auth: ${configService.get("auth").enabled ? "enabled" : "disabled"}`);
451
+ if (configService.get("auth").enabled) {
452
+ console.log(` Token: ${configService.get("auth").token}`);
453
+ }
454
+ console.log();
455
+
456
+ // Graceful shutdown — stop server + tunnel + DB on exit
457
+ const shutdown = () => {
458
+ try { server.stop(true); } catch {}
459
+ try {
460
+ import("../services/tunnel.service.ts").then(({ tunnelService }) => tunnelService.stopTunnel()).catch(() => {});
461
+ } catch {}
462
+ try {
463
+ import("../services/db.service.ts").then(({ closeDb }) => closeDb()).catch(() => {});
464
+ } catch {}
465
+ };
466
+ process.on("SIGINT", () => { shutdown(); process.exit(0); });
467
+ process.on("SIGTERM", () => { shutdown(); process.exit(0); });
468
+ process.on("exit", shutdown);
340
469
  }
341
470
 
342
471
  // Internal entry point for daemon child process
@@ -360,16 +489,12 @@ if (process.argv.includes("__serve__")) {
360
489
 
361
490
  // Sync externally-started tunnel URL + PID into tunnelService
362
491
  // so GET /api/tunnel reflects the correct state and Share button doesn't start a duplicate.
363
- // Also write server version to status.json so supervisor heartbeat reports the actual running version.
364
492
  try {
365
493
  const { resolve: r } = await import("node:path");
366
494
  const { homedir: h } = await import("node:os");
367
- const { readFileSync: rf, writeFileSync: wf } = await import("node:fs");
495
+ const { readFileSync: rf } = await import("node:fs");
368
496
  const statusFile = r(h(), ".ppm", "status.json");
369
497
  const status = JSON.parse(rf(statusFile, "utf-8"));
370
- // Write running server version — source of truth for heartbeat
371
- status.serverVersion = VERSION;
372
- wf(statusFile, JSON.stringify(status));
373
498
  if (status.shareUrl) {
374
499
  const { tunnelService } = await import("../services/tunnel.service.ts");
375
500
  tunnelService.setExternalUrl(status.shareUrl);
@@ -380,9 +505,15 @@ if (process.argv.includes("__serve__")) {
380
505
  Bun.serve({
381
506
  port,
382
507
  hostname: host,
383
- fetch(req, server) {
508
+ async fetch(req, server) {
384
509
  const url = new URL(req.url);
385
510
 
511
+ // Proxy: handle before Hono to avoid SPA catch-all conflict
512
+ if (url.pathname.startsWith("/proxy")) {
513
+ const proxyRes = await handleProxyRequest(req);
514
+ if (proxyRes) return proxyRes;
515
+ }
516
+
386
517
  if (url.pathname === "/ws/health") {
387
518
  const upgraded = server.upgrade(req, { data: { type: "health" } });
388
519
  if (upgraded) return undefined;
@@ -437,8 +568,5 @@ if (process.argv.includes("__serve__")) {
437
568
  // Start background account token refresh in daemon child
438
569
  import("../services/account.service.ts").then(({ accountService }) => accountService.startAutoRefresh()).catch(() => {});
439
570
 
440
- // Start background usage limit polling (every 5 min)
441
- import("../services/claude-usage.service.ts").then(({ startUsagePolling }) => startUsagePolling()).catch(() => {});
442
-
443
571
  console.log(`Server child ready on port ${port}`);
444
572
  }
@@ -8,7 +8,7 @@ import { renameSession as sdkRenameSession } from "@anthropic-ai/claude-agent-sd
8
8
  import { listSlashItems } from "../../services/slash-items.service.ts";
9
9
  import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
10
10
  import { getSessionLog } from "../../services/session-log.service.ts";
11
- import { getSessionMapping, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession } from "../../services/db.service.ts";
11
+ import { getSessionMapping } 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 } };
@@ -63,16 +63,7 @@ chatRoutes.get("/sessions", async (c) => {
63
63
  const projectPath = c.get("projectPath");
64
64
  const providerId = c.req.query("providerId");
65
65
  const sessions = await chatService.listSessions(providerId, projectPath);
66
- // Enrich with pin status
67
- const pinnedIds = getPinnedSessionIds();
68
- const enriched = sessions.map((s) => ({ ...s, pinned: pinnedIds.has(s.id) }));
69
- // Sort: pinned first (by pinned_at implicit via Set order), then unpinned by createdAt
70
- enriched.sort((a, b) => {
71
- if (a.pinned && !b.pinned) return -1;
72
- if (!a.pinned && b.pinned) return 1;
73
- return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
74
- });
75
- return c.json(ok(enriched));
66
+ return c.json(ok(sessions));
76
67
  } catch (e) {
77
68
  return c.json(err((e as Error).message), 500);
78
69
  }
@@ -129,9 +120,7 @@ chatRoutes.patch("/sessions/:id", async (c) => {
129
120
  // Resolve PPM UUID → SDK session ID if mapped
130
121
  const sdkId = getSessionMapping(id) ?? id;
131
122
  const projectPath = c.get("projectPath");
132
- // Persist to PPM DB (authoritative source for user-set titles)
133
- setSessionTitle(sdkId, title);
134
- // Also persist to SDK so Claude Code CLI sees the custom title
123
+ // Persist to SDK so Claude Code CLI also sees the custom title
135
124
  await sdkRenameSession(sdkId, title, { dir: projectPath });
136
125
  // Also update in-memory session
137
126
  const session = chatService.getSession(id);
@@ -142,28 +131,6 @@ chatRoutes.patch("/sessions/:id", async (c) => {
142
131
  }
143
132
  });
144
133
 
145
- /** PUT /chat/sessions/:id/pin — pin a session */
146
- chatRoutes.put("/sessions/:id/pin", (c) => {
147
- try {
148
- const id = c.req.param("id");
149
- pinSession(id);
150
- return c.json(ok({ id, pinned: true }));
151
- } catch (e) {
152
- return c.json(err((e as Error).message), 500);
153
- }
154
- });
155
-
156
- /** DELETE /chat/sessions/:id/pin — unpin a session */
157
- chatRoutes.delete("/sessions/:id/pin", (c) => {
158
- try {
159
- const id = c.req.param("id");
160
- unpinSession(id);
161
- return c.json(ok({ id, pinned: false }));
162
- } catch (e) {
163
- return c.json(err((e as Error).message), 500);
164
- }
165
- });
166
-
167
134
  /** POST /chat/sessions/:id/fork — fork session into a new one (for rewind/branch) */
168
135
  chatRoutes.post("/sessions/:id/fork", async (c) => {
169
136
  try {
@@ -200,22 +167,6 @@ chatRoutes.get("/sessions/:id/logs", (c) => {
200
167
  }
201
168
  });
202
169
 
203
- /** GET /chat/sessions/:id/debug — session debug info (IDs, JSONL path) */
204
- chatRoutes.get("/sessions/:id/debug", (c) => {
205
- const ppmId = c.req.param("id");
206
- const sdkId = getSessionMapping(ppmId) ?? ppmId;
207
- const projectName = c.req.query("project") ?? "";
208
- // Resolve JSONL path: ~/.claude/projects/<encoded-cwd>/<sdkId>.jsonl
209
- const homedir = process.env.HOME ?? process.env.USERPROFILE ?? "";
210
- const provider = providerRegistry.get("claude") as any;
211
- const projectPath = provider?.activeSessions?.get(ppmId)?.projectPath ?? "";
212
- const encodedCwd = projectPath ? projectPath.replace(/\//g, "-") : "";
213
- const jsonlDir = encodedCwd ? resolve(homedir, ".claude", "projects", encodedCwd) : "";
214
- const jsonlPath = jsonlDir ? resolve(jsonlDir, `${sdkId}.jsonl`) : "";
215
- const jsonlExists = jsonlPath ? existsSync(jsonlPath) : false;
216
- return c.json(ok({ ppmSessionId: ppmId, sdkSessionId: sdkId, jsonlPath: jsonlExists ? jsonlPath : null, jsonlDir, projectPath }));
217
- });
218
-
219
170
  /** POST /chat/upload — upload files for chat attachments, returns server-side paths */
220
171
  chatRoutes.post("/upload", async (c) => {
221
172
  try {
@@ -4,7 +4,6 @@ import { chatRoutes } from "./chat.ts";
4
4
  import { gitRoutes } from "./git.ts";
5
5
  import { fileRoutes } from "./files.ts";
6
6
  import { sqliteRoutes } from "./sqlite.ts";
7
- import { workspaceRoutes } from "./workspace.ts";
8
7
 
9
8
  type Env = { Variables: { projectPath: string; projectName: string } };
10
9
 
@@ -28,4 +27,3 @@ projectScopedRouter.route("/chat", chatRoutes);
28
27
  projectScopedRouter.route("/git", gitRoutes);
29
28
  projectScopedRouter.route("/files", fileRoutes);
30
29
  projectScopedRouter.route("/sqlite", sqliteRoutes);
31
- projectScopedRouter.route("/workspace", workspaceRoutes);
@@ -1,79 +1,86 @@
1
1
  import { Hono } from "hono";
2
2
  import { proxyService } from "../../services/proxy.service.ts";
3
- import { ok, err } from "../../types/api.ts";
4
3
 
5
4
  /**
6
5
  * Proxy routes — Anthropic-compatible API proxy.
7
- * External tools (opencode, cursor, etc.) send requests here
6
+ * External tools (Claude Code CLI, OpenCode, Cursor, etc.) send requests here
8
7
  * and PPM forwards them to Anthropic using account rotation.
9
8
  *
10
- * Mounted at /proxy — so /proxy/v1/messages maps to Anthropic's POST /v1/messages.
9
+ * Mounted at /proxy — all paths are forwarded to api.anthropic.com.
11
10
  * Uses its own auth (proxy auth key), NOT PPM's auth middleware.
11
+ *
12
+ * Usage with Claude Code CLI:
13
+ * ANTHROPIC_BASE_URL=http://host:port/proxy
14
+ * ANTHROPIC_API_KEY=<proxy-auth-key>
12
15
  */
13
- export const proxyRoutes = new Hono();
14
16
 
15
- /** Validate proxy auth key from Authorization header */
17
+ /** Validate proxy auth key from Authorization or x-api-key header */
16
18
  function validateProxyAuth(authHeader: string | undefined): boolean {
17
19
  if (!authHeader) return false;
18
20
  const key = proxyService.getAuthKey();
19
21
  if (!key) return false;
20
- // Accept both "Bearer <key>" and raw "<key>" (x-api-key style)
21
22
  const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
22
23
  return token === key;
23
24
  }
24
25
 
25
- /** CORS preflight for external tools */
26
- proxyRoutes.options("/*", (c) => {
27
- return new Response(null, {
28
- status: 204,
29
- headers: {
30
- "Access-Control-Allow-Origin": "*",
31
- "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
32
- "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta",
33
- "Access-Control-Max-Age": "86400",
34
- },
35
- });
36
- });
26
+ const CORS_HEADERS = {
27
+ "Access-Control-Allow-Origin": "*",
28
+ "Access-Control-Allow-Methods": "POST, GET, PUT, DELETE, PATCH, OPTIONS",
29
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta",
30
+ "Access-Control-Max-Age": "86400",
31
+ };
37
32
 
38
- /** POST /proxy/v1/messages — Anthropic Messages API proxy */
39
- proxyRoutes.post("/v1/messages", async (c) => {
40
- if (!proxyService.isEnabled()) {
41
- return c.json({ type: "error", error: { type: "api_error", message: "Proxy is disabled" } }, 503);
42
- }
33
+ /**
34
+ * Standalone proxy request handler — called directly from Bun.serve fetch,
35
+ * bypassing Hono routing to avoid SPA catch-all conflicts.
36
+ * Returns a Response for /proxy/* requests, or null if path doesn't match.
37
+ */
38
+ export async function handleProxyRequest(req: Request): Promise<Response | null> {
39
+ const url = new URL(req.url);
40
+ if (!url.pathname.startsWith("/proxy/") && url.pathname !== "/proxy") return null;
43
41
 
44
- // Auth check — accept both Authorization and x-api-key headers
45
- const authHeader = c.req.header("authorization") || c.req.header("x-api-key");
46
- if (!validateProxyAuth(authHeader)) {
47
- return c.json({ type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } }, 401);
42
+ // CORS preflight
43
+ if (req.method === "OPTIONS") {
44
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
48
45
  }
49
46
 
50
- const body = await c.req.text();
51
- const headers: Record<string, string> = {};
52
- for (const key of ["anthropic-version", "anthropic-beta", "content-type"]) {
53
- const val = c.req.header(key);
54
- if (val) headers[key] = val;
55
- }
56
-
57
- return proxyService.forward("/v1/messages", "POST", headers, body);
58
- });
59
-
60
- /** POST /proxy/v1/messages/count_tokens — token counting proxy */
61
- proxyRoutes.post("/v1/messages/count_tokens", async (c) => {
62
47
  if (!proxyService.isEnabled()) {
63
- return c.json({ type: "error", error: { type: "api_error", message: "Proxy is disabled" } }, 503);
48
+ return Response.json(
49
+ { type: "error", error: { type: "api_error", message: "Proxy is disabled" } },
50
+ { status: 503, headers: { "Access-Control-Allow-Origin": "*" } },
51
+ );
64
52
  }
65
53
 
66
- const authHeader = c.req.header("authorization") || c.req.header("x-api-key");
54
+ // Auth check
55
+ const authHeader = req.headers.get("authorization") || req.headers.get("x-api-key") || undefined;
67
56
  if (!validateProxyAuth(authHeader)) {
68
- return c.json({ type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } }, 401);
57
+ return Response.json(
58
+ { type: "error", error: { type: "authentication_error", message: "Invalid proxy auth key" } },
59
+ { status: 401, headers: { "Access-Control-Allow-Origin": "*" } },
60
+ );
69
61
  }
70
62
 
71
- const body = await c.req.text();
63
+ // Strip /proxy prefix to get the Anthropic API path
64
+ const path = url.pathname.replace(/^\/proxy/, "") || "/";
65
+ const method = req.method;
66
+
67
+ // Collect relevant headers to forward
72
68
  const headers: Record<string, string> = {};
73
- for (const key of ["anthropic-version", "anthropic-beta", "content-type"]) {
74
- const val = c.req.header(key);
69
+ for (const key of ["anthropic-version", "anthropic-beta", "content-type", "accept"]) {
70
+ const val = req.headers.get(key);
75
71
  if (val) headers[key] = val;
76
72
  }
77
73
 
78
- return proxyService.forward("/v1/messages/count_tokens", "POST", headers, body);
74
+ // Read body for methods that have one
75
+ const body = ["POST", "PUT", "PATCH"].includes(method) ? await req.text() : null;
76
+
77
+ return proxyService.forward(path, method, headers, body);
78
+ }
79
+
80
+ // Keep Hono sub-router for backward compat with app.route() + tests
81
+ export const proxyRoutes = new Hono();
82
+ proxyRoutes.all("/*", async (c) => {
83
+ const res = await handleProxyRequest(c.req.raw);
84
+ if (res) return res;
85
+ return c.notFound();
79
86
  });
@@ -1,5 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import { tunnelService } from "../../services/tunnel.service.ts";
3
+ import { portTunnelService } from "../../services/port-tunnel.service.ts";
3
4
  import { configService } from "../../services/config.service.ts";
4
5
  import { getLocalIp } from "../../lib/network-utils.ts";
5
6
  import { ok, err } from "../../types/api.ts";
@@ -36,3 +37,34 @@ tunnelRoutes.post("/stop", (c) => {
36
37
  tunnelService.stopTunnel();
37
38
  return c.json(ok({ stopped: true }));
38
39
  });
40
+
41
+ // --- Port-specific tunnels (for browser tab preview) ---
42
+
43
+ /** GET /api/tunnel/ports — list all active port tunnels */
44
+ tunnelRoutes.get("/ports", (c) => {
45
+ return c.json(ok(portTunnelService.list()));
46
+ });
47
+
48
+ /** POST /api/tunnel/port/start — start tunnel for a specific port */
49
+ tunnelRoutes.post("/port/start", async (c) => {
50
+ const body = await c.req.json<{ port: number }>();
51
+ const port = Number(body.port);
52
+ if (!port || port < 1 || port > 65535) {
53
+ return c.json(err("Invalid port number"), 400);
54
+ }
55
+ try {
56
+ const url = await portTunnelService.start(port);
57
+ return c.json(ok({ port, url }));
58
+ } catch (e) {
59
+ return c.json(err((e as Error).message), 500);
60
+ }
61
+ });
62
+
63
+ /** POST /api/tunnel/port/stop — stop tunnel for a specific port */
64
+ tunnelRoutes.post("/port/stop", async (c) => {
65
+ const body = await c.req.json<{ port: number }>();
66
+ const port = Number(body.port);
67
+ if (!port) return c.json(err("Invalid port number"), 400);
68
+ const stopped = portTunnelService.stop(port);
69
+ return c.json(ok({ stopped }));
70
+ });