@hienlh/ppm 0.8.84 → 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 -179
- 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-D5TyPXKq.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 -37
- 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 +0 -7
- 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-83kPKHv_.js +0 -1
- package/dist/web/assets/channel-wrd-NHWf.js +0 -1
- package/dist/web/assets/chat-tab-a7D9FgDq.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-D1JIPO6X.js +0 -2
- package/dist/web/assets/csv-preview-DLqYtXxt.js +0 -10
- package/dist/web/assets/database-viewer-Cv-ZkGs3.js +0 -1
- package/dist/web/assets/diff-viewer-DKva_V0e.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-H7KrfMtT.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
- package/dist/web/assets/index-BWcr-pMn.css +0 -2
- package/dist/web/assets/index-Myr2-Cdd.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-BR7vgqrv.js +0 -1
- package/dist/web/assets/lib-BQ34Db2e.js +0 -4
- package/dist/web/assets/markdown-renderer-A6JNS40T.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-BIdyO-DA.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-BJDnUMoy.js +0 -1
- package/dist/web/assets/sqlite-viewer-BkdmedKC.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-Cu_X7Dj3.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/hooks/use-voice-input.ts +0 -111
- package/src/web/lib/csv-parser.ts +0 -134
- 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
|
@@ -69,14 +69,9 @@ 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
|
-
|
|
78
72
|
class AccountService {
|
|
79
73
|
private pendingStates = new Map<string, { verifier: string; createdAt: number }>();
|
|
74
|
+
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
80
75
|
|
|
81
76
|
private toAccount(row: AccountRow): Account {
|
|
82
77
|
let profileData: OAuthProfileData | null = null;
|
|
@@ -535,7 +530,6 @@ class AccountService {
|
|
|
535
530
|
client_id: OAUTH_CLIENT_ID,
|
|
536
531
|
refresh_token: account.refreshToken,
|
|
537
532
|
}),
|
|
538
|
-
signal: AbortSignal.timeout(15_000),
|
|
539
533
|
});
|
|
540
534
|
if (!res.ok) {
|
|
541
535
|
const errorBody = await res.text().catch(() => "");
|
|
@@ -623,13 +617,13 @@ class AccountService {
|
|
|
623
617
|
if (!row.id || !row.access_token) continue;
|
|
624
618
|
const hasRefresh = !!row.refresh_token && row.refresh_token !== "";
|
|
625
619
|
|
|
626
|
-
// Duplicate handling:
|
|
620
|
+
// Duplicate handling: if existing account has no refresh token but import does, upgrade it
|
|
627
621
|
const existingById = getAccountById(row.id);
|
|
628
622
|
const existingByEmail = row.email ? this.list().find((a) => a.email === row.email) : null;
|
|
629
623
|
const existing = existingById ?? (existingByEmail ? getAccountById(existingByEmail.id) : null);
|
|
630
624
|
if (existing) {
|
|
631
|
-
if (hasRefresh) {
|
|
632
|
-
//
|
|
625
|
+
if (hasRefresh && !this.hasRefreshToken(existing.id)) {
|
|
626
|
+
// Upgrade: import has refresh token, existing doesn't → update tokens
|
|
633
627
|
let accessToken = row.access_token;
|
|
634
628
|
if (!looksEncrypted(accessToken)) accessToken = encrypt(accessToken);
|
|
635
629
|
const refreshToken = looksEncrypted(row.refresh_token) ? row.refresh_token : encrypt(row.refresh_token);
|
|
@@ -641,9 +635,9 @@ class AccountService {
|
|
|
641
635
|
});
|
|
642
636
|
imported++;
|
|
643
637
|
fullTransferIds.push(existing.id);
|
|
644
|
-
console.log(`[accounts]
|
|
638
|
+
console.log(`[accounts] Upgraded ${row.email ?? existing.id} with refresh token from import`);
|
|
645
639
|
}
|
|
646
|
-
continue; // skip if
|
|
640
|
+
continue; // skip if existing already has refresh token or import doesn't
|
|
647
641
|
}
|
|
648
642
|
|
|
649
643
|
// New account — insert
|
|
@@ -694,7 +688,7 @@ class AccountService {
|
|
|
694
688
|
// ---------------------------------------------------------------------------
|
|
695
689
|
|
|
696
690
|
startAutoRefresh(): void {
|
|
697
|
-
if (
|
|
691
|
+
if (this.refreshTimer) return;
|
|
698
692
|
const CHECK_INTERVAL_MS = 5 * 60_000;
|
|
699
693
|
const REFRESH_BUFFER_S = 5 * 60;
|
|
700
694
|
|
|
@@ -734,20 +728,20 @@ class AccountService {
|
|
|
734
728
|
// Run immediately on startup, then every 5 minutes
|
|
735
729
|
refreshExpiring().catch(() => {});
|
|
736
730
|
cleanupExpiredTemporary();
|
|
737
|
-
|
|
731
|
+
this.refreshTimer = setInterval(() => {
|
|
738
732
|
refreshExpiring().catch(() => {});
|
|
739
733
|
cleanupExpiredTemporary();
|
|
740
734
|
}, CHECK_INTERVAL_MS);
|
|
741
735
|
|
|
742
|
-
if (typeof
|
|
743
|
-
(
|
|
736
|
+
if (typeof this.refreshTimer === "object" && this.refreshTimer !== null && "unref" in this.refreshTimer) {
|
|
737
|
+
(this.refreshTimer as NodeJS.Timeout).unref();
|
|
744
738
|
}
|
|
745
739
|
}
|
|
746
740
|
|
|
747
741
|
stopAutoRefresh(): void {
|
|
748
|
-
if (
|
|
749
|
-
clearInterval(
|
|
750
|
-
|
|
742
|
+
if (this.refreshTimer) {
|
|
743
|
+
clearInterval(this.refreshTimer);
|
|
744
|
+
this.refreshTimer = null;
|
|
751
745
|
}
|
|
752
746
|
}
|
|
753
747
|
}
|
|
@@ -47,19 +47,10 @@ const POLL_INTERVAL = 300_000; // 5min
|
|
|
47
47
|
const ACCOUNT_STAGGER_MS = 1_000; // 1s between accounts
|
|
48
48
|
|
|
49
49
|
let inMemoryCostUsd = 0;
|
|
50
|
-
|
|
51
|
-
// Survive Bun --hot reloads: module-level vars reset on reload, globalThis persists.
|
|
52
|
-
// Without this, each hot-reload creates a NEW polling timer without clearing the old one,
|
|
53
|
-
// leading to N concurrent timers after N reloads (observed: 221 timers → 38k 429 errors/day).
|
|
54
|
-
const HOT_KEY = "__PPM_USAGE_POLL__" as const;
|
|
55
|
-
const hotState = ((globalThis as any)[HOT_KEY] ??= {
|
|
56
|
-
pollTimer: null as ReturnType<typeof setTimeout> | null,
|
|
57
|
-
inflightPoll: null as Promise<void> | null,
|
|
58
|
-
}) as { pollTimer: ReturnType<typeof setTimeout> | null; inflightPoll: Promise<void> | null };
|
|
50
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
59
51
|
|
|
60
52
|
// Per-token cooldown map: token prefix → earliest allowed fetch time
|
|
61
53
|
const tokenCooldowns = new Map<string, number>();
|
|
62
|
-
const MIN_COOLDOWN_MS = 60_000; // floor: at least 60s cooldown on 429
|
|
63
54
|
|
|
64
55
|
// Legacy: Keychain token cache for users without accounts in DB
|
|
65
56
|
let tokenCache: { token: string; timestamp: number } | null = null;
|
|
@@ -122,10 +113,9 @@ async function fetchUsageForToken(token: string): Promise<ClaudeUsage> {
|
|
|
122
113
|
});
|
|
123
114
|
if (res.status === 429) {
|
|
124
115
|
const retryAfter = parseInt(res.headers.get("retry-after") ?? "60", 10);
|
|
125
|
-
const cooldownMs = Math.max(retryAfter * 1000, MIN_COOLDOWN_MS);
|
|
126
116
|
const cooldownKey = token.substring(0, 20);
|
|
127
|
-
tokenCooldowns.set(cooldownKey, Date.now() +
|
|
128
|
-
throw new Error(`Usage API 429 — cooldown ${
|
|
117
|
+
tokenCooldowns.set(cooldownKey, Date.now() + retryAfter * 1000);
|
|
118
|
+
throw new Error(`Usage API 429 — cooldown ${retryAfter}s`);
|
|
129
119
|
}
|
|
130
120
|
if (!res.ok) throw new Error(`Usage API returned ${res.status}`);
|
|
131
121
|
const raw = (await res.json()) as Record<string, any>;
|
|
@@ -238,7 +228,7 @@ async function fetchLegacySingleAccount(): Promise<void> {
|
|
|
238
228
|
} catch {}
|
|
239
229
|
}
|
|
240
230
|
|
|
241
|
-
async function
|
|
231
|
+
async function pollOnce(): Promise<void> {
|
|
242
232
|
try {
|
|
243
233
|
const hasAccounts = accountService.list().length > 0;
|
|
244
234
|
if (hasAccounts) {
|
|
@@ -251,18 +241,6 @@ async function pollOnceInternal(): Promise<void> {
|
|
|
251
241
|
}
|
|
252
242
|
}
|
|
253
243
|
|
|
254
|
-
/** Deduped: concurrent callers share a single in-flight fetch */
|
|
255
|
-
async function pollOnce(): Promise<void> {
|
|
256
|
-
if (hotState.inflightPoll) return hotState.inflightPoll;
|
|
257
|
-
const thisPoll = pollOnceInternal().finally(() => {
|
|
258
|
-
// Only clear if still the current poll — prevents a stale .finally() from
|
|
259
|
-
// clearing a newer poll after timeout handler force-nulled inflightPoll.
|
|
260
|
-
if (hotState.inflightPoll === thisPoll) hotState.inflightPoll = null;
|
|
261
|
-
});
|
|
262
|
-
hotState.inflightPoll = thisPoll;
|
|
263
|
-
return thisPoll;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
244
|
// ---------------------------------------------------------------------------
|
|
267
245
|
// Public API
|
|
268
246
|
// ---------------------------------------------------------------------------
|
|
@@ -319,26 +297,20 @@ export function getCachedUsage(): ClaudeUsage & { activeAccountId?: string; acti
|
|
|
319
297
|
}
|
|
320
298
|
|
|
321
299
|
export function startUsagePolling(): void {
|
|
322
|
-
if (
|
|
323
|
-
|
|
300
|
+
if (pollTimer) return;
|
|
301
|
+
// Use recursive setTimeout instead of setInterval to prevent overlap
|
|
302
|
+
// and ensure polling continues even if a single iteration errors
|
|
324
303
|
const scheduleNext = () => {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const result = await Promise.race([
|
|
328
|
-
pollOnce().then(() => "done" as const),
|
|
329
|
-
timeout,
|
|
330
|
-
]).catch(() => "error" as const);
|
|
331
|
-
// If the poll timed out, force-clear inflightPoll so next scheduled poll
|
|
332
|
-
// starts a fresh fetch instead of reusing the stale hanging promise.
|
|
333
|
-
if (result === "timeout") hotState.inflightPoll = null;
|
|
304
|
+
pollTimer = setTimeout(async () => {
|
|
305
|
+
await pollOnce();
|
|
334
306
|
scheduleNext();
|
|
335
307
|
}, POLL_INTERVAL);
|
|
336
308
|
};
|
|
337
|
-
pollOnce().then(scheduleNext
|
|
309
|
+
pollOnce().then(scheduleNext);
|
|
338
310
|
}
|
|
339
311
|
|
|
340
312
|
export function stopUsagePolling(): void {
|
|
341
|
-
if (
|
|
313
|
+
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
|
342
314
|
}
|
|
343
315
|
|
|
344
316
|
export function updateFromSdkEvent(_rateLimitType?: string, _utilization?: number, costUsd?: number): void {
|
|
@@ -349,12 +321,3 @@ export async function refreshUsageNow(): Promise<ClaudeUsage & { activeAccountId
|
|
|
349
321
|
await pollOnce();
|
|
350
322
|
return getCachedUsage();
|
|
351
323
|
}
|
|
352
|
-
|
|
353
|
-
/** @internal Test-only: reset module-level state between tests */
|
|
354
|
-
export function _resetForTesting(): void {
|
|
355
|
-
inMemoryCostUsd = 0;
|
|
356
|
-
if (hotState.pollTimer) { clearTimeout(hotState.pollTimer); hotState.pollTimer = null; }
|
|
357
|
-
tokenCooldowns.clear();
|
|
358
|
-
hotState.inflightPoll = null;
|
|
359
|
-
tokenCache = null;
|
|
360
|
-
}
|
|
@@ -362,16 +362,12 @@ export async function sendHeartbeat(tunnelUrl: string): Promise<boolean> {
|
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
|
|
366
|
-
const CLOUD_HOT_KEY = "__PPM_CLOUD_HEARTBEAT__" as const;
|
|
367
|
-
const cloudHotState = ((globalThis as any)[CLOUD_HOT_KEY] ??= {
|
|
368
|
-
heartbeatTimer: null as ReturnType<typeof setInterval> | null,
|
|
369
|
-
}) as { heartbeatTimer: ReturnType<typeof setInterval> | null };
|
|
365
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
370
366
|
|
|
371
367
|
/** Start periodic heartbeat (call once after tunnel URL is obtained) */
|
|
372
368
|
export function startHeartbeat(tunnelUrl: string): void {
|
|
373
369
|
// Clear any existing heartbeat to prevent duplicates on restart
|
|
374
|
-
if (
|
|
370
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
375
371
|
|
|
376
372
|
// Initial heartbeat immediately
|
|
377
373
|
sendHeartbeat(tunnelUrl).then((ok) => {
|
|
@@ -380,16 +376,16 @@ export function startHeartbeat(tunnelUrl: string): void {
|
|
|
380
376
|
});
|
|
381
377
|
|
|
382
378
|
// Periodic heartbeat every 5 minutes
|
|
383
|
-
|
|
379
|
+
heartbeatTimer = setInterval(() => {
|
|
384
380
|
sendHeartbeat(tunnelUrl).catch(() => {});
|
|
385
381
|
}, HEARTBEAT_INTERVAL_MS);
|
|
386
382
|
}
|
|
387
383
|
|
|
388
384
|
/** Stop periodic heartbeat */
|
|
389
385
|
export function stopHeartbeat(): void {
|
|
390
|
-
if (
|
|
391
|
-
clearInterval(
|
|
392
|
-
|
|
386
|
+
if (heartbeatTimer) {
|
|
387
|
+
clearInterval(heartbeatTimer);
|
|
388
|
+
heartbeatTimer = null;
|
|
393
389
|
}
|
|
394
390
|
}
|
|
395
391
|
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { mkdirSync, existsSync } from "node:fs";
|
|
5
5
|
|
|
6
6
|
const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
|
|
7
|
-
const CURRENT_SCHEMA_VERSION =
|
|
7
|
+
const CURRENT_SCHEMA_VERSION = 5;
|
|
8
8
|
|
|
9
9
|
let db: Database | null = null;
|
|
10
10
|
let dbProfile: string | null = null;
|
|
@@ -228,65 +228,6 @@ function runMigrations(database: Database): void {
|
|
|
228
228
|
}
|
|
229
229
|
database.exec(`PRAGMA user_version = 7`);
|
|
230
230
|
}
|
|
231
|
-
|
|
232
|
-
if (current < 8) {
|
|
233
|
-
database.exec(`
|
|
234
|
-
CREATE TABLE IF NOT EXISTS session_titles (
|
|
235
|
-
session_id TEXT PRIMARY KEY,
|
|
236
|
-
title TEXT NOT NULL,
|
|
237
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
PRAGMA user_version = 8;
|
|
241
|
-
`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (current < 9) {
|
|
245
|
-
database.exec(`
|
|
246
|
-
CREATE TABLE IF NOT EXISTS session_pins (
|
|
247
|
-
session_id TEXT PRIMARY KEY,
|
|
248
|
-
pinned_at TEXT DEFAULT (datetime('now'))
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
PRAGMA user_version = 9;
|
|
252
|
-
`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (current < 10) {
|
|
256
|
-
database.exec(`
|
|
257
|
-
CREATE TABLE IF NOT EXISTS workspace_state (
|
|
258
|
-
project_name TEXT PRIMARY KEY,
|
|
259
|
-
layout_json TEXT NOT NULL,
|
|
260
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
PRAGMA user_version = 10;
|
|
264
|
-
`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
// Workspace helpers
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
export interface WorkspaceRow {
|
|
273
|
-
project_name: string;
|
|
274
|
-
layout_json: string;
|
|
275
|
-
updated_at: string;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function getWorkspace(projectName: string): WorkspaceRow | null {
|
|
279
|
-
return getDb().query(
|
|
280
|
-
"SELECT project_name, layout_json, updated_at FROM workspace_state WHERE project_name = ?",
|
|
281
|
-
).get(projectName) as WorkspaceRow | null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function setWorkspace(projectName: string, layoutJson: string): string {
|
|
285
|
-
const now = new Date().toISOString();
|
|
286
|
-
getDb().query(
|
|
287
|
-
"INSERT INTO workspace_state (project_name, layout_json, updated_at) VALUES (?, ?, ?) ON CONFLICT(project_name) DO UPDATE SET layout_json = excluded.layout_json, updated_at = excluded.updated_at",
|
|
288
|
-
).run(projectName, layoutJson, now);
|
|
289
|
-
return now;
|
|
290
231
|
}
|
|
291
232
|
|
|
292
233
|
// ---------------------------------------------------------------------------
|
|
@@ -370,52 +311,6 @@ export function getAllSessionMappings(): Record<string, string> {
|
|
|
370
311
|
return result;
|
|
371
312
|
}
|
|
372
313
|
|
|
373
|
-
// ---------------------------------------------------------------------------
|
|
374
|
-
// Session title helpers (user-set titles persisted in PPM DB)
|
|
375
|
-
// ---------------------------------------------------------------------------
|
|
376
|
-
|
|
377
|
-
export function getSessionTitle(sessionId: string): string | null {
|
|
378
|
-
const row = getDb().query("SELECT title FROM session_titles WHERE session_id = ?").get(sessionId) as { title: string } | null;
|
|
379
|
-
return row?.title ?? null;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export function setSessionTitle(sessionId: string, title: string): void {
|
|
383
|
-
getDb().query(
|
|
384
|
-
"INSERT INTO session_titles (session_id, title, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(session_id) DO UPDATE SET title = excluded.title, updated_at = excluded.updated_at",
|
|
385
|
-
).run(sessionId, title);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** Bulk-fetch DB titles for a list of session IDs. Returns map of id → title. */
|
|
389
|
-
export function getSessionTitles(sessionIds: string[]): Record<string, string> {
|
|
390
|
-
if (sessionIds.length === 0) return {};
|
|
391
|
-
const placeholders = sessionIds.map(() => "?").join(", ");
|
|
392
|
-
const rows = getDb().query(
|
|
393
|
-
`SELECT session_id, title FROM session_titles WHERE session_id IN (${placeholders})`,
|
|
394
|
-
).all(...sessionIds) as { session_id: string; title: string }[];
|
|
395
|
-
const result: Record<string, string> = {};
|
|
396
|
-
for (const r of rows) result[r.session_id] = r.title;
|
|
397
|
-
return result;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// ---------------------------------------------------------------------------
|
|
401
|
-
// Session pin helpers
|
|
402
|
-
// ---------------------------------------------------------------------------
|
|
403
|
-
|
|
404
|
-
export function pinSession(sessionId: string): void {
|
|
405
|
-
getDb().query(
|
|
406
|
-
"INSERT INTO session_pins (session_id, pinned_at) VALUES (?, datetime('now')) ON CONFLICT(session_id) DO UPDATE SET pinned_at = datetime('now')",
|
|
407
|
-
).run(sessionId);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export function unpinSession(sessionId: string): void {
|
|
411
|
-
getDb().query("DELETE FROM session_pins WHERE session_id = ?").run(sessionId);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
export function getPinnedSessionIds(): Set<string> {
|
|
415
|
-
const rows = getDb().query("SELECT session_id FROM session_pins ORDER BY pinned_at DESC").all() as { session_id: string }[];
|
|
416
|
-
return new Set(rows.map((r) => r.session_id));
|
|
417
|
-
}
|
|
418
|
-
|
|
419
314
|
// ---------------------------------------------------------------------------
|
|
420
315
|
// Push subscription helpers
|
|
421
316
|
// ---------------------------------------------------------------------------
|
|
@@ -546,13 +441,13 @@ export function insertLimitSnapshot(data: Omit<LimitSnapshotRow, "id" | "recorde
|
|
|
546
441
|
|
|
547
442
|
export function getLatestLimitSnapshot(): LimitSnapshotRow | null {
|
|
548
443
|
return getDb().query(
|
|
549
|
-
"SELECT * FROM claude_limit_snapshots ORDER BY recorded_at DESC
|
|
444
|
+
"SELECT * FROM claude_limit_snapshots ORDER BY recorded_at DESC LIMIT 1",
|
|
550
445
|
).get() as LimitSnapshotRow | null;
|
|
551
446
|
}
|
|
552
447
|
|
|
553
448
|
export function getLatestSnapshotForAccount(accountId: string): LimitSnapshotRow | null {
|
|
554
449
|
return getDb().query(
|
|
555
|
-
"SELECT * FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC
|
|
450
|
+
"SELECT * FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC LIMIT 1",
|
|
556
451
|
).get(accountId) as LimitSnapshotRow | null;
|
|
557
452
|
}
|
|
558
453
|
|
|
@@ -560,17 +455,17 @@ export function getAllLatestSnapshots(): LimitSnapshotRow[] {
|
|
|
560
455
|
return getDb().query(
|
|
561
456
|
`SELECT s.* FROM claude_limit_snapshots s
|
|
562
457
|
INNER JOIN (
|
|
563
|
-
SELECT account_id, MAX(
|
|
458
|
+
SELECT account_id, MAX(recorded_at) as max_recorded
|
|
564
459
|
FROM claude_limit_snapshots WHERE account_id IS NOT NULL
|
|
565
460
|
GROUP BY account_id
|
|
566
|
-
) latest ON s.
|
|
461
|
+
) latest ON s.account_id = latest.account_id AND s.recorded_at = latest.max_recorded`,
|
|
567
462
|
).all() as LimitSnapshotRow[];
|
|
568
463
|
}
|
|
569
464
|
|
|
570
465
|
export function touchSnapshotTimestamp(accountId: string): void {
|
|
571
466
|
getDb().query(
|
|
572
467
|
`UPDATE claude_limit_snapshots SET recorded_at = datetime('now')
|
|
573
|
-
WHERE id = (SELECT id FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC
|
|
468
|
+
WHERE id = (SELECT id FROM claude_limit_snapshots WHERE account_id = ? ORDER BY recorded_at DESC LIMIT 1)`,
|
|
574
469
|
).run(accountId);
|
|
575
470
|
}
|
|
576
471
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Subprocess } from "bun";
|
|
2
|
+
import { ensureCloudflared } from "./cloudflared.service.ts";
|
|
3
|
+
|
|
4
|
+
const TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
5
|
+
const decoder = new TextDecoder();
|
|
6
|
+
|
|
7
|
+
interface PortTunnel {
|
|
8
|
+
port: number;
|
|
9
|
+
url: string;
|
|
10
|
+
process: Subprocess;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manages cloudflare Quick Tunnels for arbitrary local ports.
|
|
15
|
+
* Each port gets its own cloudflared process and public URL.
|
|
16
|
+
*/
|
|
17
|
+
class PortTunnelService {
|
|
18
|
+
private tunnels = new Map<number, PortTunnel>();
|
|
19
|
+
|
|
20
|
+
/** Start a tunnel for a given port. Returns the public URL. */
|
|
21
|
+
async start(port: number): Promise<string> {
|
|
22
|
+
const existing = this.tunnels.get(port);
|
|
23
|
+
if (existing) return existing.url;
|
|
24
|
+
|
|
25
|
+
const bin = await ensureCloudflared();
|
|
26
|
+
const proc = Bun.spawn(
|
|
27
|
+
[bin, "tunnel", "--url", `http://127.0.0.1:${port}`],
|
|
28
|
+
{ stderr: "pipe", stdout: "ignore", stdin: "ignore" },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const reader = proc.stderr.getReader();
|
|
32
|
+
const url = await new Promise<string>((resolve, reject) => {
|
|
33
|
+
const timeout = setTimeout(() => {
|
|
34
|
+
proc.kill();
|
|
35
|
+
reject(new Error("Tunnel timed out after 30s"));
|
|
36
|
+
}, 30_000);
|
|
37
|
+
|
|
38
|
+
let buffer = "";
|
|
39
|
+
let found = false;
|
|
40
|
+
const read = async () => {
|
|
41
|
+
try {
|
|
42
|
+
while (true) {
|
|
43
|
+
const { done, value } = await reader.read();
|
|
44
|
+
if (done) break;
|
|
45
|
+
if (found) continue;
|
|
46
|
+
buffer += decoder.decode(value, { stream: true });
|
|
47
|
+
const match = buffer.match(TUNNEL_URL_REGEX);
|
|
48
|
+
if (match) {
|
|
49
|
+
found = true;
|
|
50
|
+
buffer = "";
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
resolve(match[0]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!found) {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
reject(new Error("cloudflared exited without providing tunnel URL"));
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (!found) { clearTimeout(timeout); reject(err); }
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
read();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.tunnels.set(port, { port, url, process: proc });
|
|
67
|
+
console.log(`[port-tunnel] Started tunnel for port ${port} → ${url}`);
|
|
68
|
+
return url;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Stop a tunnel for a given port */
|
|
72
|
+
stop(port: number): boolean {
|
|
73
|
+
const tunnel = this.tunnels.get(port);
|
|
74
|
+
if (!tunnel) return false;
|
|
75
|
+
try { tunnel.process.kill(); } catch {}
|
|
76
|
+
this.tunnels.delete(port);
|
|
77
|
+
console.log(`[port-tunnel] Stopped tunnel for port ${port}`);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Get tunnel URL for a port (null if not running) */
|
|
82
|
+
getUrl(port: number): string | null {
|
|
83
|
+
return this.tunnels.get(port)?.url ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** List all active port tunnels */
|
|
87
|
+
list(): { port: number; url: string }[] {
|
|
88
|
+
return Array.from(this.tunnels.values()).map(({ port, url }) => ({ port, url }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Stop all tunnels (cleanup) */
|
|
92
|
+
stopAll(): void {
|
|
93
|
+
for (const [port] of this.tunnels) this.stop(port);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const portTunnelService = new PortTunnelService();
|
|
@@ -60,6 +60,12 @@ class ProxyService {
|
|
|
60
60
|
|
|
61
61
|
// Ensure token is fresh for OAuth accounts
|
|
62
62
|
let token = account.accessToken;
|
|
63
|
+
if (!token) {
|
|
64
|
+
return new Response(
|
|
65
|
+
JSON.stringify({ type: "error", error: { type: "authentication_error", message: "Account has no access token (decryption may have failed)" } }),
|
|
66
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
67
|
+
);
|
|
68
|
+
}
|
|
63
69
|
if (token.startsWith("sk-ant-oat")) {
|
|
64
70
|
const fresh = await accountService.ensureFreshToken(account.id);
|
|
65
71
|
if (fresh) token = fresh.accessToken;
|
|
@@ -108,14 +114,22 @@ class ProxyService {
|
|
|
108
114
|
} else if (upstream.status === 401) {
|
|
109
115
|
accountSelector.onAuthError(account.id);
|
|
110
116
|
console.log(`[proxy] 401 from Anthropic — account ${account.email ?? account.id} auth error`);
|
|
117
|
+
} else if (upstream.status >= 400) {
|
|
118
|
+
console.log(`[proxy] ${upstream.status} from Anthropic — account ${account.email ?? account.id} (OAuth=${token.startsWith("sk-ant-oat")})`);
|
|
111
119
|
} else if (upstream.status >= 200 && upstream.status < 300) {
|
|
112
120
|
accountSelector.onSuccess(account.id);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
// Stream response back as-is (preserves SSE for streaming)
|
|
116
124
|
const responseHeaders = new Headers();
|
|
117
|
-
// Forward
|
|
118
|
-
for (const key of [
|
|
125
|
+
// Forward all relevant response headers from Anthropic
|
|
126
|
+
for (const key of [
|
|
127
|
+
"content-type", "x-request-id", "request-id",
|
|
128
|
+
"anthropic-ratelimit-requests-limit", "anthropic-ratelimit-requests-remaining",
|
|
129
|
+
"anthropic-ratelimit-requests-reset", "anthropic-ratelimit-tokens-limit",
|
|
130
|
+
"anthropic-ratelimit-tokens-remaining", "anthropic-ratelimit-tokens-reset",
|
|
131
|
+
"retry-after",
|
|
132
|
+
]) {
|
|
119
133
|
const val = upstream.headers.get(key);
|
|
120
134
|
if (val) responseHeaders.set(key, val);
|
|
121
135
|
}
|
|
@@ -127,9 +141,10 @@ class ProxyService {
|
|
|
127
141
|
headers: responseHeaders,
|
|
128
142
|
});
|
|
129
143
|
} catch (e) {
|
|
130
|
-
|
|
144
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
145
|
+
console.error(`[proxy] Error forwarding to Anthropic:`, msg);
|
|
131
146
|
return new Response(
|
|
132
|
-
JSON.stringify({ type: "error", error: { type: "api_error", message:
|
|
147
|
+
JSON.stringify({ type: "error", error: { type: "api_error", message: msg || "Unknown proxy error" } }),
|
|
133
148
|
{ status: 502, headers: { "Content-Type": "application/json" } },
|
|
134
149
|
);
|
|
135
150
|
}
|