@thevinci/web 1.0.0
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/README.md +197 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +4887 -0
- package/bin/cli.test.js +64 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +528 -0
- package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
- package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
- package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
- package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
- package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
- package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
- package/dist/assets/es-4o9ciP61.js +15 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-DLTDToSP.css +1 -0
- package/dist/assets/index-DgiFEKGN.js +1 -0
- package/dist/assets/ko-B20imCHE.js +15 -0
- package/dist/assets/main-BV3KOtdA.css +1 -0
- package/dist/assets/main-CDKJj0sH.js +226 -0
- package/dist/assets/main-LC-PSNVM.js +2 -0
- package/dist/assets/miniChat-CQUiG_cr.js +2 -0
- package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
- package/dist/assets/pl-DQJ7LSzj.js +15 -0
- package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
- package/dist/assets/uk-BNFxOlO4.js +15 -0
- package/dist/assets/vendor--DBfsbEis.css +1 -0
- package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
- package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
- package/dist/assets/worker-CbT6TVo7.js +155 -0
- package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +528 -0
- package/dist/index.html +607 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.png +0 -0
- package/dist/logo-dark-512x512.svg +528 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.png +0 -0
- package/dist/logo-light-512x512.svg +528 -0
- package/dist/mini-chat.html +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +21 -0
- package/dist/sw.js +1 -0
- package/package.json +118 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +528 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +528 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.png +0 -0
- package/public/logo-dark-512x512.svg +528 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.png +0 -0
- package/public/logo-light-512x512.svg +528 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +21 -0
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/index.d.ts +39 -0
- package/server/index.js +1311 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/event-stream/DOCUMENTATION.md +61 -0
- package/server/lib/event-stream/directory-ws-bridge.js +185 -0
- package/server/lib/event-stream/global-hub.js +158 -0
- package/server/lib/event-stream/global-hub.test.js +140 -0
- package/server/lib/event-stream/global-ws-bridge.js +206 -0
- package/server/lib/event-stream/index.js +25 -0
- package/server/lib/event-stream/protocol.js +131 -0
- package/server/lib/event-stream/protocol.test.js +182 -0
- package/server/lib/event-stream/runtime.js +180 -0
- package/server/lib/event-stream/runtime.test.js +512 -0
- package/server/lib/event-stream/upstream-reader.js +226 -0
- package/server/lib/event-stream/upstream-reader.test.js +276 -0
- package/server/lib/fs/DOCUMENTATION.md +36 -0
- package/server/lib/fs/routes.js +1040 -0
- package/server/lib/fs/search.js +238 -0
- package/server/lib/git/DOCUMENTATION.md +152 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +112 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/routes.js +972 -0
- package/server/lib/git/service.js +3432 -0
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/github/DOCUMENTATION.md +171 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +519 -0
- package/server/lib/github/repo/fork-detection.js +102 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/github/routes.js +1560 -0
- package/server/lib/magic-prompts/routes.js +63 -0
- package/server/lib/magic-prompts/runtime.js +119 -0
- package/server/lib/notifications/DOCUMENTATION.md +122 -0
- package/server/lib/notifications/emitter-runtime.js +102 -0
- package/server/lib/notifications/index.js +4 -0
- package/server/lib/notifications/message.js +52 -0
- package/server/lib/notifications/message.test.js +34 -0
- package/server/lib/notifications/push-runtime.js +304 -0
- package/server/lib/notifications/routes.js +315 -0
- package/server/lib/notifications/runtime.js +566 -0
- package/server/lib/notifications/template-runtime.js +349 -0
- package/server/lib/notifications/template-runtime.test.js +26 -0
- package/server/lib/opencode/DOCUMENTATION.md +362 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth-state-runtime.js +88 -0
- package/server/lib/opencode/auth.js +83 -0
- package/server/lib/opencode/bootstrap-runtime.js +131 -0
- package/server/lib/opencode/cli-entry-runtime.js +43 -0
- package/server/lib/opencode/cli-options.js +128 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/config-entity-routes.js +370 -0
- package/server/lib/opencode/core-routes.js +500 -0
- package/server/lib/opencode/core-routes.test.js +26 -0
- package/server/lib/opencode/env-config.js +74 -0
- package/server/lib/opencode/env-keys.js +68 -0
- package/server/lib/opencode/env-runtime.js +1162 -0
- package/server/lib/opencode/env-runtime.test.js +116 -0
- package/server/lib/opencode/feature-routes-runtime.js +244 -0
- package/server/lib/opencode/hmr-state-runtime.js +85 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/lifecycle.js +1019 -0
- package/server/lib/opencode/lifecycle.test.js +240 -0
- package/server/lib/opencode/mcp.js +278 -0
- package/server/lib/opencode/network-runtime.js +104 -0
- package/server/lib/opencode/network-runtime.test.js +37 -0
- package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
- package/server/lib/opencode/path-utils.js +100 -0
- package/server/lib/opencode/path-utils.test.js +71 -0
- package/server/lib/opencode/project-directory-runtime.js +124 -0
- package/server/lib/opencode/project-icon-routes.js +399 -0
- package/server/lib/opencode/project-icon-routes.test.js +107 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/proxy.js +445 -0
- package/server/lib/opencode/pwa-manifest-routes.js +257 -0
- package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
- package/server/lib/opencode/routes.js +541 -0
- package/server/lib/opencode/server-startup-runtime.js +156 -0
- package/server/lib/opencode/server-utils-runtime.js +168 -0
- package/server/lib/opencode/server-utils-runtime.test.js +135 -0
- package/server/lib/opencode/session-runtime.js +356 -0
- package/server/lib/opencode/session-runtime.test.js +151 -0
- package/server/lib/opencode/settings-helpers.js +770 -0
- package/server/lib/opencode/settings-helpers.test.js +109 -0
- package/server/lib/opencode/settings-normalization-runtime.js +428 -0
- package/server/lib/opencode/settings-runtime.js +826 -0
- package/server/lib/opencode/settings-runtime.test.js +85 -0
- package/server/lib/opencode/shared.js +615 -0
- package/server/lib/opencode/shutdown-runtime.js +139 -0
- package/server/lib/opencode/shutdown-runtime.test.js +58 -0
- package/server/lib/opencode/skill-routes.js +701 -0
- package/server/lib/opencode/skills.js +548 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
- package/server/lib/opencode/static-routes-runtime.js +65 -0
- package/server/lib/opencode/theme-runtime.js +167 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
- package/server/lib/opencode/vinci-routes.js +76 -0
- package/server/lib/opencode/watcher.js +115 -0
- package/server/lib/opencode/watcher.test.js +239 -0
- package/server/lib/preview/proxy-runtime.js +1333 -0
- package/server/lib/preview/proxy-runtime.test.js +144 -0
- package/server/lib/projects/project-config.js +567 -0
- package/server/lib/projects/project-config.test.js +175 -0
- package/server/lib/projects/project-id.js +13 -0
- package/server/lib/quota/DOCUMENTATION.md +58 -0
- package/server/lib/quota/index.js +25 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +168 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/quota/routes.js +27 -0
- package/server/lib/quota/utils/auth.js +50 -0
- package/server/lib/quota/utils/formatters.js +85 -0
- package/server/lib/quota/utils/formatters.test.js +54 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
- package/server/lib/scheduled-tasks/routes.js +235 -0
- package/server/lib/scheduled-tasks/runtime.js +773 -0
- package/server/lib/scheduled-tasks/runtime.test.js +100 -0
- package/server/lib/security/request-security.js +115 -0
- package/server/lib/session-folders/routes.js +63 -0
- package/server/lib/session-folders/routes.test.js +102 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +87 -0
- package/server/lib/terminal/DOCUMENTATION.md +76 -0
- package/server/lib/terminal/index.js +31 -0
- package/server/lib/terminal/output-replay-buffer.js +78 -0
- package/server/lib/terminal/output-replay-buffer.test.js +75 -0
- package/server/lib/terminal/runtime.js +850 -0
- package/server/lib/terminal/runtime.test.js +96 -0
- package/server/lib/terminal/terminal-ws-protocol.js +68 -0
- package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
- package/server/lib/text/DOCUMENTATION.md +35 -0
- package/server/lib/text/summarization.js +138 -0
- package/server/lib/text/summarization.test.js +34 -0
- package/server/lib/tts/DOCUMENTATION.md +146 -0
- package/server/lib/tts/base-url.js +62 -0
- package/server/lib/tts/capability-runtime.js +31 -0
- package/server/lib/tts/index.js +19 -0
- package/server/lib/tts/routes.js +261 -0
- package/server/lib/tts/routes.test.js +53 -0
- package/server/lib/tts/service.js +178 -0
- package/server/lib/tts/stt.js +75 -0
- package/server/lib/tunnels/DOCUMENTATION.md +18 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/managed-config.js +201 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/routes.js +605 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
- package/server/lib/ui-auth/ui-auth.js +673 -0
- package/server/lib/ui-auth/ui-passkeys.js +545 -0
- package/server/opencode-proxy.test.js +151 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/server/sse-routes.test.js +152 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
2
|
+
import {
|
|
3
|
+
getAuthEntry,
|
|
4
|
+
normalizeAuthEntry,
|
|
5
|
+
buildResult,
|
|
6
|
+
toUsageWindow,
|
|
7
|
+
toNumber,
|
|
8
|
+
formatMoney
|
|
9
|
+
} from '../utils/index.js';
|
|
10
|
+
|
|
11
|
+
export const providerId = 'openrouter';
|
|
12
|
+
export const providerName = 'OpenRouter';
|
|
13
|
+
export const aliases = ['openrouter'];
|
|
14
|
+
|
|
15
|
+
export const isConfigured = () => {
|
|
16
|
+
const auth = readAuthFile();
|
|
17
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
18
|
+
return Boolean(entry?.key || entry?.token);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const fetchQuota = async () => {
|
|
22
|
+
const auth = readAuthFile();
|
|
23
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
24
|
+
const apiKey = entry?.key ?? entry?.token;
|
|
25
|
+
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
return buildResult({
|
|
28
|
+
providerId,
|
|
29
|
+
providerName,
|
|
30
|
+
ok: false,
|
|
31
|
+
configured: false,
|
|
32
|
+
error: 'Not configured'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch('https://openrouter.ai/api/v1/credits', {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${apiKey}`,
|
|
41
|
+
'Content-Type': 'application/json'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
return buildResult({
|
|
47
|
+
providerId,
|
|
48
|
+
providerName,
|
|
49
|
+
ok: false,
|
|
50
|
+
configured: true,
|
|
51
|
+
error: `API error: ${response.status}`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const payload = await response.json();
|
|
56
|
+
const credits = payload?.data ?? {};
|
|
57
|
+
const totalCredits = toNumber(credits.total_credits);
|
|
58
|
+
const totalUsage = toNumber(credits.total_usage);
|
|
59
|
+
const remaining = totalCredits !== null && totalUsage !== null
|
|
60
|
+
? Math.max(0, totalCredits - totalUsage)
|
|
61
|
+
: null;
|
|
62
|
+
let valueLabel = null;
|
|
63
|
+
if (remaining !== null && totalUsage !== null) {
|
|
64
|
+
valueLabel = `$${formatMoney(remaining)} left · $${formatMoney(totalUsage)} spent`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const windows = {
|
|
68
|
+
credits: toUsageWindow({
|
|
69
|
+
usedPercent: null,
|
|
70
|
+
windowSeconds: null,
|
|
71
|
+
resetAt: null,
|
|
72
|
+
valueLabel
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return buildResult({
|
|
77
|
+
providerId,
|
|
78
|
+
providerName,
|
|
79
|
+
ok: true,
|
|
80
|
+
configured: true,
|
|
81
|
+
usage: { windows }
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return buildResult({
|
|
85
|
+
providerId,
|
|
86
|
+
providerName,
|
|
87
|
+
ok: false,
|
|
88
|
+
configured: true,
|
|
89
|
+
error: error instanceof Error ? error.message : 'Request failed'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
2
|
+
import {
|
|
3
|
+
getAuthEntry,
|
|
4
|
+
normalizeAuthEntry,
|
|
5
|
+
buildResult,
|
|
6
|
+
toUsageWindow,
|
|
7
|
+
toNumber,
|
|
8
|
+
toTimestamp,
|
|
9
|
+
resolveWindowSeconds,
|
|
10
|
+
resolveWindowLabel,
|
|
11
|
+
normalizeTimestamp
|
|
12
|
+
} from '../utils/index.js';
|
|
13
|
+
|
|
14
|
+
export const providerId = 'zai-coding-plan';
|
|
15
|
+
export const providerName = 'z.ai';
|
|
16
|
+
export const aliases = ['zai-coding-plan', 'zai', 'z.ai'];
|
|
17
|
+
|
|
18
|
+
export const isConfigured = () => {
|
|
19
|
+
const auth = readAuthFile();
|
|
20
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
21
|
+
return Boolean(entry?.key || entry?.token);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const fetchQuota = async () => {
|
|
25
|
+
const auth = readAuthFile();
|
|
26
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
27
|
+
const apiKey = entry?.key ?? entry?.token;
|
|
28
|
+
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
return buildResult({
|
|
31
|
+
providerId,
|
|
32
|
+
providerName,
|
|
33
|
+
ok: false,
|
|
34
|
+
configured: false,
|
|
35
|
+
error: 'Not configured'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch('https://api.z.ai/api/monitor/usage/quota/limit', {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${apiKey}`,
|
|
44
|
+
'Content-Type': 'application/json'
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
return buildResult({
|
|
50
|
+
providerId,
|
|
51
|
+
providerName,
|
|
52
|
+
ok: false,
|
|
53
|
+
configured: true,
|
|
54
|
+
error: `API error: ${response.status}`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const payload = await response.json();
|
|
59
|
+
const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
|
|
60
|
+
const tokensLimit = limits.find((limit) => limit?.type === 'TOKENS_LIMIT');
|
|
61
|
+
const windowSeconds = resolveWindowSeconds(tokensLimit);
|
|
62
|
+
const windowLabel = resolveWindowLabel(windowSeconds);
|
|
63
|
+
const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null;
|
|
64
|
+
const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null;
|
|
65
|
+
|
|
66
|
+
const windows = {};
|
|
67
|
+
if (tokensLimit) {
|
|
68
|
+
windows[windowLabel] = toUsageWindow({
|
|
69
|
+
usedPercent,
|
|
70
|
+
windowSeconds,
|
|
71
|
+
resetAt
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return buildResult({
|
|
76
|
+
providerId,
|
|
77
|
+
providerName,
|
|
78
|
+
ok: true,
|
|
79
|
+
configured: true,
|
|
80
|
+
usage: { windows }
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return buildResult({
|
|
84
|
+
providerId,
|
|
85
|
+
providerName,
|
|
86
|
+
ok: false,
|
|
87
|
+
configured: true,
|
|
88
|
+
error: error instanceof Error ? error.message : 'Request failed'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zhipu AI Coding Plan quota fetch
|
|
3
|
+
*
|
|
4
|
+
* API: https://open.bigmodel.cn/api/monitor/usage/quota/limit
|
|
5
|
+
*
|
|
6
|
+
* Response limits:
|
|
7
|
+
* - TOKENS_LIMIT: Token usage (5-hour rolling window)
|
|
8
|
+
* - TIME_LIMIT: MCP tools usage (monthly window)
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} TokensLimit
|
|
11
|
+
* @property {string} type - 'TOKENS_LIMIT'
|
|
12
|
+
* @property {number} [unit]
|
|
13
|
+
* @property {number} [number]
|
|
14
|
+
* @property {number} [nextResetTime]
|
|
15
|
+
* @property {number} [percentage]
|
|
16
|
+
*
|
|
17
|
+
* @typedef {Object} McpToolsTimeLimit
|
|
18
|
+
* @property {string} type - 'TIME_LIMIT'
|
|
19
|
+
* @property {number} [unit]
|
|
20
|
+
* @property {number} [number]
|
|
21
|
+
* @property {number} [usage]
|
|
22
|
+
* @property {number} [currentValue]
|
|
23
|
+
* @property {number} [remaining]
|
|
24
|
+
* @property {number} [percentage]
|
|
25
|
+
* @property {number} [nextResetTime]
|
|
26
|
+
* @property {Array<{modelCode: string, usage: number}>} [usageDetails]
|
|
27
|
+
*/
|
|
28
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
29
|
+
import {
|
|
30
|
+
getAuthEntry,
|
|
31
|
+
normalizeAuthEntry,
|
|
32
|
+
buildResult,
|
|
33
|
+
toUsageWindow,
|
|
34
|
+
resolveWindowSeconds,
|
|
35
|
+
normalizeTimestamp
|
|
36
|
+
} from '../utils/index.js';
|
|
37
|
+
|
|
38
|
+
export const providerId = 'zhipuai-coding-plan';
|
|
39
|
+
export const providerName = 'Zhipu AI Coding Plan';
|
|
40
|
+
export const aliases = ['zhipuai-coding-plan'];
|
|
41
|
+
|
|
42
|
+
export const isConfigured = () => {
|
|
43
|
+
const auth = readAuthFile();
|
|
44
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
45
|
+
return Boolean(entry?.key || entry?.token);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const fetchQuota = async () => {
|
|
49
|
+
const auth = readAuthFile();
|
|
50
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
51
|
+
const apiKey = entry?.key ?? entry?.token;
|
|
52
|
+
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
return buildResult({
|
|
55
|
+
providerId,
|
|
56
|
+
providerName,
|
|
57
|
+
ok: false,
|
|
58
|
+
configured: false,
|
|
59
|
+
error: 'Not configured'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch('https://open.bigmodel.cn/api/monitor/usage/quota/limit', {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${apiKey}`,
|
|
68
|
+
'Content-Type': 'application/json'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
return buildResult({
|
|
74
|
+
providerId,
|
|
75
|
+
providerName,
|
|
76
|
+
ok: false,
|
|
77
|
+
configured: true,
|
|
78
|
+
error: `API error: ${response.status}`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const payload = await response.json();
|
|
83
|
+
const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
|
|
84
|
+
|
|
85
|
+
const tokensLimit = limits.find((limit) => limit?.type === 'TOKENS_LIMIT');
|
|
86
|
+
const mcpToolsTimeLimit = limits.find((limit) => limit?.type === 'TIME_LIMIT');
|
|
87
|
+
|
|
88
|
+
const windows = {};
|
|
89
|
+
|
|
90
|
+
// Handle TOKENS_LIMIT (5-hour window for token usage)
|
|
91
|
+
if (tokensLimit) {
|
|
92
|
+
const windowSeconds = resolveWindowSeconds(tokensLimit);
|
|
93
|
+
const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null;
|
|
94
|
+
const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null;
|
|
95
|
+
|
|
96
|
+
windows['Tokens'] = toUsageWindow({
|
|
97
|
+
usedPercent,
|
|
98
|
+
windowSeconds,
|
|
99
|
+
resetAt
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle TIME_LIMIT (MCP tools monthly window)
|
|
104
|
+
if (mcpToolsTimeLimit) {
|
|
105
|
+
// TIME_LIMIT unit=5 means 1 month (30 days)
|
|
106
|
+
const monthSeconds = 30 * 24 * 60 * 60;
|
|
107
|
+
const resetAt = mcpToolsTimeLimit?.nextResetTime ? normalizeTimestamp(mcpToolsTimeLimit.nextResetTime) : null;
|
|
108
|
+
const usedPercent = typeof mcpToolsTimeLimit?.percentage === 'number' ? mcpToolsTimeLimit.percentage : null;
|
|
109
|
+
|
|
110
|
+
windows['MCP Tools'] = toUsageWindow({
|
|
111
|
+
usedPercent,
|
|
112
|
+
windowSeconds: monthSeconds,
|
|
113
|
+
resetAt
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return buildResult({
|
|
118
|
+
providerId,
|
|
119
|
+
providerName,
|
|
120
|
+
ok: true,
|
|
121
|
+
configured: true,
|
|
122
|
+
usage: { windows }
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return buildResult({
|
|
126
|
+
providerId,
|
|
127
|
+
providerName,
|
|
128
|
+
ok: false,
|
|
129
|
+
configured: true,
|
|
130
|
+
error: error instanceof Error ? error.message : 'Request failed'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
2
|
+
import { readConfigLayers } from '../../opencode/shared.js';
|
|
3
|
+
import {
|
|
4
|
+
getAuthEntry,
|
|
5
|
+
normalizeAuthEntry,
|
|
6
|
+
buildResult,
|
|
7
|
+
toUsageWindow,
|
|
8
|
+
toNumber,
|
|
9
|
+
toTimestamp,
|
|
10
|
+
resolveWindowSeconds,
|
|
11
|
+
resolveWindowLabel,
|
|
12
|
+
normalizeTimestamp
|
|
13
|
+
} from '../utils/index.js';
|
|
14
|
+
|
|
15
|
+
export const providerId = 'zhipuai-coding-plan';
|
|
16
|
+
export const providerName = 'ZhipuAI';
|
|
17
|
+
export const aliases = ['zhipuai-coding-plan', 'zhipuai', 'zhipu'];
|
|
18
|
+
|
|
19
|
+
function getApiKey() {
|
|
20
|
+
const auth = readAuthFile();
|
|
21
|
+
const oldEntry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
22
|
+
const apiKeyFromOld = oldEntry?.key ?? oldEntry?.token;
|
|
23
|
+
|
|
24
|
+
if (apiKeyFromOld) {
|
|
25
|
+
return apiKeyFromOld;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const layers = readConfigLayers();
|
|
30
|
+
const { mergedConfig } = layers;
|
|
31
|
+
|
|
32
|
+
for (const alias of aliases) {
|
|
33
|
+
const providerConfig = mergedConfig?.provider?.[alias];
|
|
34
|
+
if (providerConfig?.options?.apiKey) {
|
|
35
|
+
return providerConfig.options.apiKey;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// Ignore read errors
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const isConfigured = () => {
|
|
46
|
+
return Boolean(getApiKey());
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const fetchQuota = async () => {
|
|
50
|
+
const apiKey = getApiKey();
|
|
51
|
+
|
|
52
|
+
if (!apiKey) {
|
|
53
|
+
return buildResult({
|
|
54
|
+
providerId,
|
|
55
|
+
providerName,
|
|
56
|
+
ok: false,
|
|
57
|
+
configured: false,
|
|
58
|
+
error: 'Not configured'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch('https://open.bigmodel.cn/api/monitor/usage/quota/limit', {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${apiKey}`,
|
|
67
|
+
'Content-Type': 'application/json'
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
return buildResult({
|
|
73
|
+
providerId,
|
|
74
|
+
providerName,
|
|
75
|
+
ok: false,
|
|
76
|
+
configured: true,
|
|
77
|
+
error: `API error: ${response.status}`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const payload = await response.json();
|
|
82
|
+
const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
|
|
83
|
+
const tokensLimit = limits.find((limit) => limit?.type === 'TOKENS_LIMIT');
|
|
84
|
+
const windowSeconds = resolveWindowSeconds(tokensLimit);
|
|
85
|
+
const windowLabel = resolveWindowLabel(windowSeconds);
|
|
86
|
+
const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null;
|
|
87
|
+
const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null;
|
|
88
|
+
|
|
89
|
+
const windows = {};
|
|
90
|
+
if (tokensLimit) {
|
|
91
|
+
windows[windowLabel] = toUsageWindow({
|
|
92
|
+
usedPercent,
|
|
93
|
+
windowSeconds,
|
|
94
|
+
resetAt
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return buildResult({
|
|
99
|
+
providerId,
|
|
100
|
+
providerName,
|
|
101
|
+
ok: true,
|
|
102
|
+
configured: true,
|
|
103
|
+
usage: { windows }
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return buildResult({
|
|
107
|
+
providerId,
|
|
108
|
+
providerName,
|
|
109
|
+
ok: false,
|
|
110
|
+
configured: true,
|
|
111
|
+
error: error instanceof Error ? error.message : 'Request failed'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function registerQuotaRoutes(app, { getQuotaProviders }) {
|
|
2
|
+
app.get('/api/quota/providers', async (_req, res) => {
|
|
3
|
+
try {
|
|
4
|
+
const { listConfiguredQuotaProviders } = await getQuotaProviders();
|
|
5
|
+
const providers = listConfiguredQuotaProviders();
|
|
6
|
+
res.json({ providers });
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error('Failed to list quota providers:', error);
|
|
9
|
+
res.status(500).json({ error: error.message || 'Failed to list quota providers' });
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.get('/api/quota/:providerId', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const { providerId } = req.params;
|
|
16
|
+
if (!providerId) {
|
|
17
|
+
return res.status(400).json({ error: 'Provider ID is required' });
|
|
18
|
+
}
|
|
19
|
+
const { fetchQuotaForProvider } = await getQuotaProviders();
|
|
20
|
+
const result = await fetchQuotaForProvider(providerId);
|
|
21
|
+
res.json(result);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Failed to fetch quota:', error);
|
|
24
|
+
res.status(500).json({ error: error.message || 'Failed to fetch quota' });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const OPENCODE_CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR
|
|
6
|
+
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
|
|
7
|
+
: path.join(os.homedir(), '.vinci', '.opencode');
|
|
8
|
+
const OPENCODE_DATA_DIR = process.env.OPENCODE_DATA_DIR
|
|
9
|
+
? path.resolve(process.env.OPENCODE_DATA_DIR)
|
|
10
|
+
: path.join(os.homedir(), '.vinci', '.opencode', 'data');
|
|
11
|
+
|
|
12
|
+
export const ANTIGRAVITY_ACCOUNTS_PATHS = [
|
|
13
|
+
path.join(OPENCODE_CONFIG_DIR, 'antigravity-accounts.json'),
|
|
14
|
+
path.join(OPENCODE_DATA_DIR, 'antigravity-accounts.json')
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const readJsonFile = (filePath) => {
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
if (!trimmed) return null;
|
|
25
|
+
return JSON.parse(trimmed);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn(`Failed to read JSON file: ${filePath}`, error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getAuthEntry = (auth, aliases) => {
|
|
33
|
+
for (const alias of aliases) {
|
|
34
|
+
if (auth[alias]) {
|
|
35
|
+
return auth[alias];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const normalizeAuthEntry = (entry) => {
|
|
42
|
+
if (!entry) return null;
|
|
43
|
+
if (typeof entry === 'string') {
|
|
44
|
+
return { token: entry };
|
|
45
|
+
}
|
|
46
|
+
if (typeof entry === 'object') {
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const formatResetTime = (timestamp) => {
|
|
2
|
+
try {
|
|
3
|
+
const resetDate = new Date(timestamp);
|
|
4
|
+
if (!Number.isFinite(resetDate.getTime())) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const isToday = resetDate.toDateString() === now.toDateString();
|
|
10
|
+
|
|
11
|
+
if (isToday) {
|
|
12
|
+
return resetDate.toLocaleTimeString(undefined, {
|
|
13
|
+
hour: 'numeric',
|
|
14
|
+
minute: '2-digit'
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return resetDate.toLocaleString(undefined, {
|
|
19
|
+
month: 'short',
|
|
20
|
+
day: 'numeric',
|
|
21
|
+
weekday: 'short',
|
|
22
|
+
hour: 'numeric',
|
|
23
|
+
minute: '2-digit'
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const hasResetTimestamp = (resetAt) => resetAt !== null && resetAt !== undefined && resetAt !== '';
|
|
31
|
+
|
|
32
|
+
export const calculateResetAfterSeconds = (resetAt) => {
|
|
33
|
+
if (!hasResetTimestamp(resetAt)) return null;
|
|
34
|
+
const resetAtTime = new Date(resetAt).getTime();
|
|
35
|
+
if (!Number.isFinite(resetAtTime)) return null;
|
|
36
|
+
const delta = Math.floor((resetAtTime - Date.now()) / 1000);
|
|
37
|
+
return delta < 0 ? 0 : delta;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const toUsageWindow = ({ usedPercent, windowSeconds, resetAt, valueLabel }) => {
|
|
41
|
+
const resetAfterSeconds = calculateResetAfterSeconds(resetAt);
|
|
42
|
+
const resetFormatted = hasResetTimestamp(resetAt) ? formatResetTime(resetAt) : null;
|
|
43
|
+
const hasFiniteUsedPercent = typeof usedPercent === 'number' && Number.isFinite(usedPercent);
|
|
44
|
+
return {
|
|
45
|
+
usedPercent,
|
|
46
|
+
remainingPercent: hasFiniteUsedPercent ? Math.max(0, 100 - usedPercent) : null,
|
|
47
|
+
windowSeconds: windowSeconds ?? null,
|
|
48
|
+
resetAfterSeconds,
|
|
49
|
+
resetAt,
|
|
50
|
+
resetAtFormatted: resetFormatted,
|
|
51
|
+
resetAfterFormatted: resetFormatted,
|
|
52
|
+
...(valueLabel ? { valueLabel } : {})
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const buildResult = ({ providerId, providerName, ok, configured, usage, error }) => ({
|
|
57
|
+
providerId,
|
|
58
|
+
providerName,
|
|
59
|
+
ok,
|
|
60
|
+
configured,
|
|
61
|
+
usage: usage ?? null,
|
|
62
|
+
...(error ? { error } : {}),
|
|
63
|
+
fetchedAt: Date.now()
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const durationToLabel = (duration, unit) => {
|
|
67
|
+
if (!duration || !unit) return 'limit';
|
|
68
|
+
if (unit === 'TIME_UNIT_MINUTE') return `${duration}m`;
|
|
69
|
+
if (unit === 'TIME_UNIT_HOUR') return `${duration}h`;
|
|
70
|
+
if (unit === 'TIME_UNIT_DAY') return `${duration}d`;
|
|
71
|
+
return 'limit';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const durationToSeconds = (duration, unit) => {
|
|
75
|
+
if (!duration || !unit) return null;
|
|
76
|
+
if (unit === 'TIME_UNIT_MINUTE') return duration * 60;
|
|
77
|
+
if (unit === 'TIME_UNIT_HOUR') return duration * 3600;
|
|
78
|
+
if (unit === 'TIME_UNIT_DAY') return duration * 86400;
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const formatMoney = (value) => {
|
|
83
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
|
|
84
|
+
return value.toFixed(2);
|
|
85
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { calculateResetAfterSeconds, formatResetTime, toUsageWindow } from './formatters.js';
|
|
4
|
+
|
|
5
|
+
describe('formatResetTime', () => {
|
|
6
|
+
it('returns null for invalid timestamps', () => {
|
|
7
|
+
expect(formatResetTime('not-a-date')).toBeNull();
|
|
8
|
+
expect(formatResetTime(NaN)).toBeNull();
|
|
9
|
+
expect(formatResetTime(Infinity)).toBeNull();
|
|
10
|
+
expect(formatResetTime(-Infinity)).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('calculateResetAfterSeconds', () => {
|
|
15
|
+
it('accepts an epoch reset timestamp', () => {
|
|
16
|
+
expect(calculateResetAfterSeconds(0)).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns null for invalid timestamps', () => {
|
|
20
|
+
expect(calculateResetAfterSeconds('not-a-date')).toBeNull();
|
|
21
|
+
expect(calculateResetAfterSeconds(NaN)).toBeNull();
|
|
22
|
+
expect(calculateResetAfterSeconds(Infinity)).toBeNull();
|
|
23
|
+
expect(calculateResetAfterSeconds(-Infinity)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('toUsageWindow', () => {
|
|
28
|
+
it('formats epoch reset timestamps', () => {
|
|
29
|
+
const usageWindow = toUsageWindow({ resetAt: 0 });
|
|
30
|
+
|
|
31
|
+
expect(usageWindow.resetAfterSeconds).toBe(0);
|
|
32
|
+
expect(usageWindow.resetAtFormatted).toBe(formatResetTime(0));
|
|
33
|
+
expect(usageWindow.resetAfterFormatted).toBe(formatResetTime(0));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not derive remaining percent from missing usage', () => {
|
|
37
|
+
expect(toUsageWindow({ usedPercent: undefined }).remainingPercent).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not derive remaining percent from non-finite usage', () => {
|
|
41
|
+
expect(toUsageWindow({ usedPercent: NaN }).remainingPercent).toBeNull();
|
|
42
|
+
expect(toUsageWindow({ usedPercent: Infinity }).remainingPercent).toBeNull();
|
|
43
|
+
expect(toUsageWindow({ usedPercent: -Infinity }).remainingPercent).toBeNull();
|
|
44
|
+
expect(toUsageWindow({ usedPercent: null }).remainingPercent).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('derives remaining percent from a valid usage value', () => {
|
|
48
|
+
expect(toUsageWindow({ usedPercent: 60 }).remainingPercent).toBe(40);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('clamps remaining percent to zero when usage exceeds 100', () => {
|
|
52
|
+
expect(toUsageWindow({ usedPercent: 110 }).remainingPercent).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
});
|