@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.
- package/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +5 -187
- package/bun.lock +0 -5
- package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-5eBmZ_lt.js} +1 -1
- package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-DimLlN0y.js} +1 -1
- package/dist/web/assets/api-settings-CFw-lh5k.js +1 -0
- package/dist/web/assets/{arc-BAOivWpI.js → arc-D4SasZrA.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-nv0WbM7d.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-C1XvYrb8.js} +1 -1
- package/dist/web/assets/browser-tab-CmsL5eny.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW-CygDrbWJ.js} +1 -1
- package/dist/web/assets/channel-DmKoFTd_.js +1 -0
- package/dist/web/assets/chat-tab-CFWsf13Z.js +7 -0
- package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C2FDgsgT.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-jF4w6cat.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-BVCECZFi.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-DXTbeu5d.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BaZqOsTs.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Bky2tcH7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Cp4BK9A8.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-BosFEH7G.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-H5Gbjsbr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-CWerSUwS.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-FvwP7jUy.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-D1PI_ORP.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C7Vzk_AI.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-BceYBGYX.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-WPtzgxql.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-DlHXDeLY.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-Ci_Prygb.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-CO0zMN-z.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-IAEEzfpM.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-BLXalOgc.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dx1Ri_p2.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-m9pPGKn7.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-B_08ExbI.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-DqSOVcYe.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +1 -0
- package/dist/web/assets/clone-CijCFRT5.js +1 -0
- package/dist/web/assets/code-editor-H_dAh_fJ.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-DlL82QHu.js} +1 -1
- package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BmVoh2At.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-sDrRW9MQ.js} +1 -1
- package/dist/web/assets/database-viewer-DBzsgEJ8.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-ChnAhgni.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-DW1J1uJd.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-CQ32hyG_.js} +1 -1
- package/dist/web/assets/diff-viewer-DzS-OnAR.js +4 -0
- package/dist/web/assets/dist-0Va_2L7G.js +16 -0
- package/dist/web/assets/dist-D9irYETY.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-6CHo6nOw.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-DroDiNT0.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-DP0QBh8w.js} +1 -1
- package/dist/web/assets/git-graph-D3C7F8o3.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js} +1 -1
- package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CQBb2thr.js} +1 -1
- package/dist/web/assets/index-CIkjfera.js +31 -0
- package/dist/web/assets/index-WKLuYsBY.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +2 -0
- package/dist/web/assets/input-CGp1nFIg.js +1 -0
- package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B4kqZBtn.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-46yibrV5.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-BcmRwjK-.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-B619K53y.js} +1 -1
- package/dist/web/assets/keybindings-store-BdaoLwSo.js +1 -0
- package/dist/web/assets/{line-B78g-52T.js → line-1gcO63_w.js} +1 -1
- package/dist/web/assets/{linear-DP4mkX3m.js → linear-DfRqDoVd.js} +1 -1
- package/dist/web/assets/markdown-renderer-DH49Zag7.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-XtjZQOeM.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-CifOFo_q.js} +1 -1
- package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-BJYw-iDX.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-BuHUh_fO.js} +1 -1
- package/dist/web/assets/postgres-viewer-B9FYk8sD.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-Bau_hj6Z.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-Cq2b-uwp.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-qPxiTUcS.js} +1 -1
- package/dist/web/assets/settings-store-DWXGVHsE.js +2 -0
- package/dist/web/assets/settings-tab-D-q8pd-5.js +1 -0
- package/dist/web/assets/sqlite-viewer-CDqcTePw.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-Dulj2oa8.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +1 -0
- package/dist/web/assets/tab-store-BPeiymiH.js +1 -0
- package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-wKgpSPAT.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-BWyDnCYq.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +1 -0
- package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-B9Iv2bNV.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-ChXcMzBQ.js} +1 -1
- package/dist/web/index.html +11 -12
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +7 -232
- package/docs/codebase-summary.md +3 -9
- package/docs/design-guidelines.md +0 -21
- package/docs/project-changelog.md +1 -115
- package/docs/project-roadmap.md +19 -41
- package/docs/system-architecture.md +15 -212
- package/package.json +2 -3
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/restart.ts +1 -9
- package/src/cli/commands/status.ts +0 -19
- package/src/index.ts +3 -2
- package/src/providers/claude-agent-sdk.ts +31 -94
- package/src/providers/mock-provider.ts +1 -6
- package/src/server/index.ts +166 -38
- package/src/server/routes/chat.ts +3 -52
- package/src/server/routes/project-scoped.ts +0 -2
- package/src/server/routes/proxy.ts +53 -46
- package/src/server/routes/tunnel.ts +32 -0
- package/src/server/ws/chat.ts +146 -207
- package/src/services/account-selector.service.ts +8 -16
- package/src/services/account.service.ts +13 -19
- package/src/services/claude-usage.service.ts +11 -48
- package/src/services/cloud.service.ts +6 -10
- package/src/services/db.service.ts +6 -111
- package/src/services/port-tunnel.service.ts +97 -0
- package/src/services/proxy.service.ts +19 -4
- package/src/services/supervisor.ts +25 -285
- package/src/types/api.ts +1 -9
- package/src/types/chat.ts +1 -3
- package/src/web/app.tsx +35 -41
- package/src/web/components/browser/browser-tab.tsx +97 -106
- package/src/web/components/chat/chat-history-bar.tsx +6 -72
- package/src/web/components/chat/chat-tab.tsx +16 -32
- package/src/web/components/chat/message-input.tsx +13 -107
- package/src/web/components/chat/message-list.tsx +15 -27
- package/src/web/components/chat/session-picker.tsx +31 -78
- package/src/web/components/chat/usage-badge.tsx +1 -11
- package/src/web/components/editor/code-editor.tsx +26 -36
- package/src/web/components/layout/command-palette.tsx +1 -3
- package/src/web/components/layout/editor-panel.tsx +18 -162
- package/src/web/components/layout/panel-layout.tsx +1 -17
- package/src/web/components/settings/proxy-settings-section.tsx +42 -40
- package/src/web/hooks/use-chat.ts +201 -211
- package/src/web/hooks/use-global-keybindings.ts +2 -25
- package/src/web/hooks/use-server-reload.ts +0 -9
- package/src/web/hooks/use-url-sync.ts +21 -173
- package/src/web/stores/keybindings-store.ts +0 -1
- package/src/web/stores/panel-store.ts +19 -73
- package/src/web/stores/panel-utils.ts +3 -145
- package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-DaHGm_0i.js +0 -1
- package/dist/web/assets/channel-wrd-NHWf.js +0 -1
- package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
- package/dist/web/assets/chevron-right-DeV0ehiG.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
- package/dist/web/assets/clone-LRxlvnMj.js +0 -1
- package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
- package/dist/web/assets/csv-preview-DLqYtXxt.js +0 -10
- package/dist/web/assets/database-viewer-DXk79Nel.js +0 -1
- package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
- package/dist/web/assets/dist-DylI9XxN.js +0 -13
- package/dist/web/assets/dist-lF8CoYII.js +0 -41
- package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
- package/dist/web/assets/index-CgQXpBb_.css +0 -2
- package/dist/web/assets/index-DEeeRoka.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
- package/dist/web/assets/input-BglMT33g.js +0 -1
- package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
- package/dist/web/assets/lib-BQ34Db2e.js +0 -4
- package/dist/web/assets/markdown-renderer-Brj8_LQM.js +0 -69
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
- package/dist/web/assets/postgres-viewer-CwkTGmqy.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
- package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
- package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
- package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
- package/dist/web/assets/tab-store-BJw7OCmy.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
- package/dist/web/assets/use-monaco-theme-CNzekTN3.js +0 -11
- package/docs/streaming-input-guide.md +0 -267
- package/snapshot-state.md +0 -1526
- package/src/server/routes/browser-preview.ts +0 -159
- package/src/server/routes/workspace.ts +0 -35
- package/src/services/cloud-ws.service.ts +0 -227
- package/src/web/components/chat/account-rotation-settings.tsx +0 -163
- package/src/web/components/chat/chat-welcome.tsx +0 -148
- package/src/web/components/editor/csv-preview.tsx +0 -228
- package/src/web/components/editor/editor-breadcrumb.tsx +0 -216
- package/src/web/components/editor/editor-toolbar.tsx +0 -74
- package/src/web/components/shared/connection-lost-overlay.tsx +0 -89
- package/src/web/hooks/use-voice-input.ts +0 -111
- package/src/web/lib/csv-parser.ts +0 -134
- package/src/web/stores/connection-store.ts +0 -39
- package/test-tokens.mjs +0 -212
- /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-DOElml5u.js} +0 -0
- /package/dist/web/assets/{array-B9UHiPd-.js → array-CYkMkqnU.js} +0 -0
- /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-ChOTgl3e.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-HeHO0VhB.js} +0 -0
- /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-Beh6XjaL.js} +0 -0
- /package/dist/web/assets/{dist-CSJdAyA9.js → dist-BUYzeuKe.js} +0 -0
- /package/dist/web/assets/{init-DlZdxViB.js → init-Rr1s_RiX.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BB-mzMLb.js} +0 -0
- /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-CKoArbIw.js} +0 -0
- /package/dist/web/assets/{math-069Z4SuC.js → math-B7b0HgJF.js} +0 -0
- /package/dist/web/assets/{path-6uRLdFF7.js → path-BAQ3hXlG.js} +0 -0
- /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-DeiOTZKJ.js} +0 -0
- /package/dist/web/assets/{react-ER-4DN55.js → react-Dev-wu-s.js} +0 -0
- /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-Dwml_la6.js} +0 -0
- /package/dist/web/assets/{src-BqX54PbV.js → src-B_cC68fH.js} +0 -0
- /package/dist/web/assets/{table-C7X5UAEI.js → table-COiJDPRA.js} +0 -0
- /package/dist/web/assets/{tag-CCtdV063.js → tag-LMq02LfE.js} +0 -0
- /package/dist/web/assets/{utils-BNytJOb1.js → utils-btZ8C8-R.js} +0 -0
package/src/server/index.ts
CHANGED
|
@@ -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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 —
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
54
|
+
// Auth check
|
|
55
|
+
const authHeader = req.headers.get("authorization") || req.headers.get("x-api-key") || undefined;
|
|
67
56
|
if (!validateProxyAuth(authHeader)) {
|
|
68
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
});
|