├── .cursorignore ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── images └── icon.png ├── package.json ├── src ├── extension.ts ├── handlers │ ├── notifications.ts │ └── statusBar.ts ├── interfaces │ ├── i18n.ts │ └── types.ts ├── locales │ ├── en.json │ ├── ja.json │ ├── kk.json │ ├── ko.json │ ├── ru.json │ └── zh.json ├── services │ ├── api.ts │ ├── database.ts │ ├── github.ts │ └── team.ts └── utils │ ├── cooldown.ts │ ├── currency.ts │ ├── i18n.ts │ ├── logger.ts │ ├── progressBars.ts │ ├── report.ts │ └── updateStats.ts └── tsconfig.json /.cursorignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | .DS_Store 6 | .env 7 | *.log 8 | .gitignore 9 | .git/ 10 | package-lock.json 11 | LICENSE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | .DS_Store 6 | .env 7 | *.log 8 | builds/ 9 | .git/ 10 | *.ps1 11 | user-cache.json 12 | currency-rates.json 13 | .cursorignore 14 | package-lock.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "ms-vscode.extension-test-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | !src/locales/** 5 | !out/locales/** 6 | .gitignore 7 | .yarnrc 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | 13 | builds/** 14 | .git/** 15 | .github/** 16 | **/eslint.config.mjs 17 | 18 | # Development files 19 | .cursorignore 20 | package-lock.json 21 | **/test/** 22 | **/*.test.js 23 | **/*.spec.js 24 | **/.npmignore 25 | scripts/** 26 | 27 | # Node modules to exclude (keeping only necessary ones) 28 | node_modules/sql.js/** 29 | !node_modules/sql.js/package.json 30 | !node_modules/sql.js/dist/sql-wasm.js 31 | !node_modules/sql.js/dist/sql-wasm.wasm 32 | 33 | # Keep minimal axios files 34 | !node_modules/axios/dist/axios.cjs 35 | !node_modules/axios/index.js 36 | !node_modules/axios/package.json 37 | 38 | # Keep minimal jsonwebtoken files 39 | !node_modules/jsonwebtoken/index.js 40 | !node_modules/jsonwebtoken/lib/** 41 | !node_modules/jsonwebtoken/package.json 42 | 43 | # Keep minimal semver files 44 | !node_modules/semver/index.js 45 | !node_modules/semver/package.json 46 | !node_modules/semver/classes/** 47 | !node_modules/semver/functions/** 48 | !node_modules/semver/internal/** 49 | !node_modules/semver/ranges/** 50 | 51 | # Exclude unnecessary files from dependencies 52 | node_modules/**/LICENSE* 53 | node_modules/**/README* 54 | node_modules/**/CHANGELOG* 55 | node_modules/**/.travis.yml 56 | node_modules/**/.eslintrc* 57 | node_modules/**/test/** 58 | node_modules/**/tests/** 59 | node_modules/**/docs/** 60 | node_modules/**/example/** 61 | node_modules/**/examples/** 62 | node_modules/**/coverage/** 63 | node_modules/**/.github/** 64 | node_modules/**/.git/** 65 | node_modules/**/bench/** 66 | node_modules/**/benchmark/** 67 | node_modules/@types/ 68 | node_modules/typescript/ 69 | 70 | # Keep only the necessary image 71 | images/**/*.?(png|gif|jpg|jpeg|webp) 72 | !images/icon.png 73 | 74 | # Exclude any VSIX files 75 | **/*.vsix 76 | 77 | *.ps1 78 | user-cache.json 79 | currency-rates.json 80 | 81 | # exclude any md files 82 | **/*.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "cursor-stats" extension will be documented in this file. 4 | 5 | ## [1.1.4] - 2025-06-06 6 | 7 | ### Added 8 | - 🌍 **Internationalization (i18n) Support**: Extension interface now supports multiple languages 9 | - English (Default) 10 | - 中文 (Chinese) 11 | - 한국어 (Korean) 12 | - 🔧 **Language Selection**: New command "Cursor Stats: Select Language" for easy language switching 13 | 14 | ### Changed 15 | - 🌐 All UI elements, notifications, and messages are now translatable 16 | - 📈 Status bar and tooltips adapt to selected language 17 | - 💰 Currency names are now localized based on selected language 18 | - 🔄 Automatic interface updates when language is changed 19 | 20 | ### Fixed 21 | - 🐛 Fixed undefined requests handling in team usage extraction 22 | - 🎯 Improved usage-based pricing period calculations for active months 23 | - 🔧 Better error handling for localization edge cases 24 | 25 | ## Upcoming Features 26 | 27 | ### Planned 28 | 29 | - Session based request tracking 30 | - Visual analytics with graphs for historical request usage 31 | - Project-specific request usage monitoring 32 | - Dedicated activity bar section for enhanced statistics view 33 | - Smart API error handling with exponential backoff 34 | - Automatic retry reduction during API outages 35 | - Intelligent refresh rate adjustment 36 | - User-friendly error notifications 37 | - Customization features: 38 | - Configurable quota display options 39 | - Hide/show specific model statistics 40 | - Customizable status bar information 41 | 42 | ## [1.1.3] - 2024-07-27 43 | 44 | ### Reverted 45 | - ⏪ Reverted the SQLite library change introduced in `v1.1.2` (from `node-sqlite3-wasm` back to `sql.js`). This addresses critical token retrieval errors experienced by some users, particularly on Macos. 46 | 47 | ### Fixed 48 | - 🐛 Fixed an issue where some users could not retrieve their access token after the `v1.1.2` update due to the new SQLite library. 49 | 50 | ## [1.1.2] - 2025-05-23 51 | 52 | ### Added 53 | - ✨ Enhanced spending alert notifications to trigger for each multiple of the configured threshold, providing more granular warnings. 54 | - 📊 Added "Daily Remaining" feature: Shows estimated fast requests remaining per day in the tooltip. 55 | - Includes new settings: `cursorStats.showDailyRemaining` and `cursorStats.excludeWeekends`. 56 | - 🚀 Changelog Display on Update: The extension now automatically shows the changelog in a webview panel when it's updated to a new version. 57 | 58 | ### Changed 59 | - 🔧 Switched from `sql.js` to `node-sqlite3-wasm` for SQLite database handling. This change addresses potential issues with large database files (over 2GB) and improves cross-platform compatibility by using a more robust WASM-based SQLite implementation. 60 | - 💅 Improved tooltip formatting for usage-based pricing details, including better alignment and padding for costs and model names. 61 | - 🛠 Refined detection and notification logic for unknown AI models in usage data. 62 | - ⚙️ Spending alert notifications are now reset and re-evaluated if the `spendingAlertThreshold` setting is changed. 63 | 64 | ### Fixed 65 | - 🐛 Corrected an issue where mid-month payment amounts in tooltips might not always reflect the user's selected currency. 66 | - 🐞 Addressed potential minor inaccuracies in progress bar calculations when "exclude weekends" was enabled. 67 | 68 | ## [1.1.1] - 2025-05-01 69 | 70 | ### Added 71 | - 🎨 Added a new setting `cursorStats.statusBarColorThresholds` to allow customizing status bar text color based on usage percentage thresholds. 72 | 73 | ### Changed 74 | - ✨ Improved usage-based pricing calculations to more accurately reflect costs, particularly by excluding mid-month payment credits from total cost and usage percentage calculations. 75 | - 📊 Modified the tooltip display to filter out the mid-month payment item from the detailed list and clarify the unpaid amount calculation. 76 | - 🔎 Enhanced detection and display of potentially unknown models in the usage details tooltip. 77 | - 🧹 Removed minor redundant log statements. 78 | 79 | ## [1.1.0] - 2025-04-22 80 | 81 | ### Added 82 | - 🌍 Multi-currency support 83 | - 📊 Progress bar visualization for usage and period tracking 84 | - 📝 Diagnostic report generation for troubleshooting 85 | - 🎨 Enhanced tooltips with improved formatting and clarity 86 | - ⚙️ Custom database path configuration option 87 | - 🔄 Improved model detection and notifications 88 | - 📈 Better usage statistics presentation 89 | - 💡 Smart progress bars with configurable thresholds 90 | 91 | ### Changed 92 | - Refactored currency handling with real-time conversion 93 | - Enhanced tooltip display with centered alignment 94 | - Improved error handling and notifications 95 | - Updated usage tracking for new AI models 96 | - Enhanced status bar information display 97 | - Better organization of configuration options 98 | - Optimized performance for currency conversions 99 | 100 | ### Fixed 101 | - Currency display formatting issues 102 | - Progress bar threshold calculations 103 | - Model detection accuracy 104 | - Usage percentage calculations 105 | - Status bar update timing 106 | - Configuration change handling 107 | 108 | ## [1.0.9] - 2025-02-15 109 | 110 | ### Added 111 | - Team usage support with per-user statistics tracking 112 | - Enhanced cooldown mechanism for API request management 113 | - Improved window focus handling for better performance 114 | - Smart interval management for status updates 115 | - Comprehensive error handling with detailed logging 116 | 117 | ### Changed 118 | - Refactored extension activation and update logic 119 | - Enhanced team usage API with team ID support 120 | - Improved notification system with better timing 121 | - Updated status bar refresh mechanism 122 | - Optimized performance during window focus changes 123 | 124 | ### Fixed 125 | - Window focus handling during cooldown periods 126 | - Status bar update timing issues 127 | - Team usage data synchronization 128 | - API request throttling and cooldown logic 129 | - Memory usage optimization 130 | 131 | ## [1.0.8] - 2025-02-08 132 | 133 | ### Added 134 | - Spending alert notifications with configurable dollar thresholds 135 | - UI extension host compatibility for remote development 136 | - New `spendingAlertThreshold` configuration option 137 | - Multi-threshold alert system (6 default percentage thresholds) 138 | 139 | ### Changed 140 | - Configuration structure now uses array-based format 141 | - Increased default refresh interval from 30 to 60 seconds 142 | - Raised minimum refresh interval from 5 to 10 seconds 143 | - Added "scope" property to all configuration settings 144 | - Updated notification sorting logic to handle more thresholds 145 | 146 | ### Removed 147 | - Legacy VSIX package file from repository 148 | 149 | ## [1.0.7] - 2025-02-07 150 | 151 | ### Added 152 | - Comprehensive GitHub release notes viewer with markdown support 153 | - Enhanced release check functionality with detailed release information 154 | - Support for release assets and source code download links 155 | - Integrated marked library for markdown rendering 156 | - Improved WSL database path handling 157 | 158 | ### Changed 159 | - Updated status bar color scheme for better visual consistency 160 | - Refactored Windows username utility into database service 161 | - Enhanced usage cost display formatting 162 | - Improved release information structure with additional metadata 163 | - Updated package dependencies to latest versions 164 | 165 | ### Removed 166 | - Redundant Windows username utility module 167 | 168 | ## [1.0.6] - 2025-02-06 169 | 170 | ### Added 171 | 172 | - Window focus-aware refresh intervals 173 | - Mid-month payment tracking and notifications 174 | - Stripe integration for unpaid invoice handling 175 | - New "Show Total Requests" configuration option 176 | - Additional logging categories for debugging 177 | - Unpaid invoice warnings with billing portal integration 178 | 179 | ### Changed 180 | 181 | - Improved notification sorting logic (highest thresholds first) 182 | - Refactored usage-based pricing data structure 183 | - Enhanced status bar tooltip formatting 184 | - Updated payment period calculations to exclude mid-month payments 185 | - Modified status bar text display for total requests 186 | 187 | ### Fixed 188 | 189 | - Notification clearing logic when usage drops below thresholds 190 | - Window focus change handling during updates 191 | - Error handling for Stripe session URL retrieval 192 | - Configuration change detection for total requests display 193 | 194 | ## [1.0.5-beta.15] - 2025-02-6 195 | 196 | ### Added 197 | 198 | - Configurable refresh interval setting (minimum 5 seconds) 199 | - SQL.js database support for improved cross-platform compatibility 200 | - New database error handling and logging mechanisms 201 | 202 | ### Changed 203 | 204 | - Replaced SQLite3 with SQL.js to eliminate native dependencies 205 | - Complete database handling refactor for better reliability 206 | - Updated package dependencies and lockfile 207 | - Improved installation process by removing postinstall script 208 | 209 | ### Removed 210 | 211 | - SQLite3 native dependency and related build scripts 212 | - Database connection pooling and monitoring logic 213 | - Unused dependencies and development files 214 | 215 | ### Fixed 216 | 217 | - Potential installation issues on ARM architectures 218 | - Database file path resolution logic 219 | - Memory leaks in database handling 220 | - Error handling during token retrieval 221 | 222 | ## [1.0.5-beta.3] - 2024-02-14 223 | 224 | ### Added 225 | 226 | - Support for new startOfMonth field in API response 227 | - Smart notification system with configurable thresholds 228 | - Optional status bar colors with configuration 229 | - Support for Cursor nightly version database paths 230 | - Enhanced tooltip with compact and modern layout 231 | - Improved settings accessibility features 232 | 233 | ### Changed 234 | 235 | - Improved handling of usage-based pricing billing cycle (3rd/4th day) 236 | - Enhanced error handling and API response processing 237 | - Better startup behavior for notifications and status bar 238 | - Refined settings navigation and accessibility 239 | - Updated tooltip design with better organization 240 | 241 | ### Fixed 242 | 243 | - Startup notification and status bar visibility issues 244 | - Double notifications on startup 245 | - Settings button functionality when no file is open 246 | - Status bar visibility during notifications 247 | 248 | ### Known Issues 249 | 250 | - macOS support is currently not available (still working on it) 251 | 252 | ## [1.0.4] - 2025-02-02 253 | 254 | ### Added 255 | 256 | - WSL (Windows Subsystem for Linux) support for database path 257 | - Dynamic status bar colors based on usage percentage 258 | - Interactive buttons in tooltip for quick actions: 259 | - Refresh Statistics 260 | - Open Settings 261 | - Usage Based Pricing Settings 262 | - Debug logging system with configuration option 263 | - Improved tooltip formatting and alignment 264 | - Combined Period and Last Updated into a single line 265 | - Left-aligned Period and right-aligned Last Updated 266 | - Moved Total Cost to Current Usage line 267 | - Centered section titles 268 | - Added action buttons section 269 | - Dynamic status bar improvements: 270 | - Usage-based color theming 271 | - Visual status indicators 272 | - Custom color scheme support 273 | 274 | ### Changed 275 | 276 | - Enhanced error handling for database connections 277 | - Improved WSL detection and path resolution 278 | - Better visual organization of usage information 279 | - Refined status bar color transitions based on usage levels 280 | - Added detailed logging for debugging purposes 281 | - Improved command handling and user interaction 282 | 283 | ## [1.0.2] - 2025-02-02 284 | 285 | ### Added 286 | 287 | - Click-to-settings functionality in status bar 288 | - Automatic token retrieval from local database 289 | 290 | ### Changed 291 | 292 | - Enhanced tooltip UI with better formatting and icons 293 | - Improved separator styling for better readability 294 | - Updated status bar icons for better visual consistency 295 | 296 | ## [1.0.1] - 2025-02-02 297 | 298 | ### Added 299 | 300 | - Extension icon for better visibility in VS Code Marketplace 301 | - Improved tooltip formatting for status bar items 302 | - Better alignment of list items in the display 303 | 304 | ## [1.0.0] - 2025-02-02 305 | 306 | ### Added 307 | 308 | - Initial release 309 | - Status bar integration showing Cursor usage statistics 310 | - Session token management 311 | - Real-time statistics updates 312 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cursor Stats 2 | 3 |
4 | 5 | > A powerful Cursor extension that provides real-time monitoring of your Cursor subscription usage, 6 | > 7 | > including fast requests and usage-based pricing information. 8 | 9 | #### [Features](#section-features) • [Screenshots](#section-screenshots) • [Configuration](#section-configuration) • [Commands](#section-commands) • [Installation](#section-install) • [Support](#-support) 10 | 11 | [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/Dwtexe.cursor-stats.svg?style=flat-square&label=VS%20Code%20Marketplace&logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=Dwtexe.cursor-stats) [![Downloads](https://img.shields.io/visual-studio-marketplace/d/Dwtexe.cursor-stats.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=Dwtexe.cursor-stats) [![Rating](https://img.shields.io/visual-studio-marketplace/r/Dwtexe.cursor-stats.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=Dwtexe.cursor-stats) 12 | 13 |
14 | 15 |
16 |

✨ Features

17 | 18 | #### Core Features 19 | 20 | - 🚀 Real-time usage monitoring 21 | - 👥 Team usage tracking 22 | - 📊 Premium request analytics 23 | - 💰 Usage-based pricing insights 24 | - 🔄 Smart cooldown system 25 | - 🔔 Intelligent notifications 26 | - 💸 Spending alerts 27 | - 💳 Mid-month payment tracking 28 | 29 | #### Advanced Features 30 | 31 | - 🎨 Customizable status bar 32 | - 📈 Progress bar visualization 33 | - 🌍 Multi-currency support 34 | - 📝 Diagnostic reporting 35 | - ⚡ Command palette integration 36 | - 🌙 Cursor Nightly version support 37 | - 🔄 GitHub release updates 38 | - 🔒 Secure token management 39 | 40 | #### 🔜 Upcoming Features 41 | 42 | - 📊 Session-based request tracking 43 | - 📈 Visual analytics dashboard 44 | - 🎯 Project-specific monitoring 45 | - 🎨 Enhanced statistics view 46 | - ⚙️ Advanced customization options 47 | 48 |
49 |
50 |

📸 Screenshots

51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
Default UICustom Currency
Progress BarsSettings
69 |
70 | 71 |
72 |

⚙️ Configuration

73 | 74 | | Setting | Description | Default | 75 | |---------|-------------|---------| 76 | | `cursorStats.enableLogging` | Enable detailed logging | `true` | 77 | | `cursorStats.enableStatusBarColors` | Toggle colored status bar | `true` | 78 | | `cursorStats.statusBarColorThresholds` | Customize status bar text color based on usage percentage | `Array of 14 color thresholds` | 79 | | `cursorStats.enableAlerts` | Enable usage alerts | `true` | 80 | | `cursorStats.usageAlertThresholds` | Percentage thresholds for usage alerts | `[10, 30, 50, 75, 90, 100]` | 81 | | `cursorStats.showTotalRequests` | Show sum of all requests instead of only fast requests | `false` | 82 | | `cursorStats.refreshInterval` | Update frequency (seconds) | `60` | 83 | | `cursorStats.spendingAlertThreshold` | Spending alert threshold (in your selected currency) | `1` | 84 | | `cursorStats.currency` | Custom currency conversion | `USD` | 85 | | `cursorStats.showProgressBars` | Enable progress visualization | `false` | 86 | | `cursorStats.progressBarLength` | Progress bar length (for progress visualization) | `10` | 87 | | `cursorStats.progressBarWarningThreshold` | Percentage threshold for progress bar warning (yellow) | `50` | 88 | | `cursorStats.progressBarCriticalThreshold` | Percentage threshold for progress bar critical (red) | `75` | 89 | | `cursorStats.customDatabasePath` | Custom path to Cursor database | `""` | 90 | | `cursorStats.excludeWeekends` | Exclude weekends from period progress and daily calculations | `false` | 91 | | `cursorStats.showDailyRemaining` | Show estimated fast requests remaining per day | `false` | 92 | | `cursorStats.language` | Language for extension interface and messages | `en` | 93 | 94 |
95 | 96 |
97 |

🔧 Commands

98 | 99 | | Command | Description | 100 | |---------|-------------| 101 | | `cursor-stats.refreshStats` | Manually refresh statistics | 102 | | `cursor-stats.openSettings` | Open extension settings | 103 | | `cursor-stats.setLimit` | Configure usage-based pricing settings | 104 | | `cursor-stats.selectCurrency` | Change display currency | 105 | | `cursor-stats.selectLanguage` | Select language for extension interface | 106 | | `cursor-stats.createReport` | Generate diagnostic report | 107 | 108 |
109 | 110 |
111 |

🚀 Installation

112 | 113 | #### VS Code Marketplace 114 | 115 | 1. Open VS Code 116 | 2. Press `Ctrl+P` / `⌘P` 117 | 3. Run `ext install Dwtexe.cursor-stats` 118 | 119 | Or install directly from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=Dwtexe.cursor-stats) 120 | 121 | #### Manual Installation 122 | 123 | 1. Download the latest `.vsix` from [Releases](https://github.com/Dwtexe/cursor-stats/releases) 124 | 2. Open Cursor 125 | 3. Press `Ctrl+Shift+P` / `⌘⇧P` 126 | 4. Run `Install from VSIX` 127 | 5. Select the downloaded file 128 | 129 |
130 | 131 | ## 🤝 Contributing 132 | 133 | 1. Fork the repository 134 | 2. Create a feature branch 135 | 3. Make your changes 136 | 4. Submit a pull request 137 | 138 | ## 💬 Support 139 | 140 | - 🐛 [Report Issues](https://github.com/Dwtexe/cursor-stats/issues) 141 | - 💡 [Feature Requests](https://github.com/Dwtexe/cursor-stats/issues/new) 142 | 143 | ## 💝 Donations 144 | 145 | If you find this extension helpful, consider supporting its development: 146 | 147 |
148 | Click to view donation options 149 | 150 | ### Buy Me A Coffee 151 | 152 | Buy Me A Coffee 153 | 154 | ### Binance 155 | 156 | - **ID**: `39070620` 157 | 158 | ### USDT 159 | 160 | - **Multi-Chain** (BEP20/ERC20/Arbitrum One/Optimism): 161 | 162 | ``` 163 | 0x88bfb527158387f8f74c5a96a0468615d06f3899 164 | ``` 165 | 166 | - **TRC20**: 167 | 168 | ``` 169 | TPTnapCanmrsfcMVAyn4YiC6dLP8Wx1Czb 170 | ``` 171 | 172 |
173 | 174 | ## 📄 License 175 | 176 | [MIT](LICENSE) © Dwtexe 177 | 178 | --- 179 | 180 |
181 | 182 | Made with ❤️ by [Dwtexe](https://github.com/Dwtexe) 183 | 184 |
185 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "import", 20 | format: ["camelCase", "PascalCase"], 21 | }], 22 | 23 | curly: "warn", 24 | eqeqeq: "warn", 25 | "no-throw-literal": "warn", 26 | semi: "warn", 27 | }, 28 | }]; -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dwtexe/cursor-stats/6560caa1f8408afb56767a5b08f3aa426e14d90a/images/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cursor-stats", 3 | "displayName": "Cursor Stats", 4 | "description": "A Cursor extension for monitoring usage.", 5 | "version": "1.1.4", 6 | "publisher": "Dwtexe", 7 | "icon": "images/icon.png", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Dwtexe/cursor-stats" 11 | }, 12 | "engines": { 13 | "vscode": "^1.96.0" 14 | }, 15 | "categories": [ 16 | "Other", 17 | "Visualization" 18 | ], 19 | "keywords": [ 20 | "cursor", 21 | "cursor.com", 22 | "cursor-ide", 23 | "statistics", 24 | "monitoring", 25 | "usage", 26 | "analytics" 27 | ], 28 | "activationEvents": [ 29 | "onStartupFinished" 30 | ], 31 | "main": "./out/extension.js", 32 | "contributes": { 33 | "commands": [ 34 | { 35 | "command": "cursor-stats.refreshStats", 36 | "title": "Cursor Stats: Refresh Statistics", 37 | "icon": "$(sync)" 38 | }, 39 | { 40 | "command": "cursor-stats.openSettings", 41 | "title": "Cursor Stats: Open Settings", 42 | "icon": "$(gear)" 43 | }, 44 | { 45 | "command": "cursor-stats.setLimit", 46 | "title": "Cursor Stats: Usage Based Pricing Settings", 47 | "icon": "$(dollar)" 48 | }, 49 | { 50 | "command": "cursor-stats.selectCurrency", 51 | "title": "Cursor Stats: Select Display Currency" 52 | }, 53 | { 54 | "command": "cursor-stats.createReport", 55 | "title": "Cursor Stats: Generate Diagnostic Report", 56 | "icon": "$(notebook)" 57 | }, 58 | { 59 | "command": "cursor-stats.selectLanguage", 60 | "title": "Cursor Stats: Select Language" 61 | } 62 | ], 63 | "configuration": [ 64 | { 65 | "title": "Cursor Stats", 66 | "properties": { 67 | "cursorStats.enableLogging": { 68 | "type": "boolean", 69 | "default": true, 70 | "description": "Enable detailed logging for debugging purposes.", 71 | "scope": "window" 72 | }, 73 | "cursorStats.enableStatusBarColors": { 74 | "type": "boolean", 75 | "default": true, 76 | "description": "Enable colored status bar based on usage percentage.", 77 | "scope": "window" 78 | }, 79 | "cursorStats.statusBarColorThresholds": { 80 | "type": "array", 81 | "description": "Customize status bar text color based on usage percentage. Define thresholds (min percentage) and colors (theme ID or hex). Defaults replicate original behavior.", 82 | "scope": "window", 83 | "default": [ 84 | { 85 | "percentage": 95, 86 | "color": "#CC0000" 87 | }, 88 | { 89 | "percentage": 90, 90 | "color": "#FF3333" 91 | }, 92 | { 93 | "percentage": 85, 94 | "color": "#FF4D4D" 95 | }, 96 | { 97 | "percentage": 80, 98 | "color": "#FF6600" 99 | }, 100 | { 101 | "percentage": 75, 102 | "color": "#FF8800" 103 | }, 104 | { 105 | "percentage": 70, 106 | "color": "#FFAA00" 107 | }, 108 | { 109 | "percentage": 65, 110 | "color": "#FFCC00" 111 | }, 112 | { 113 | "percentage": 60, 114 | "color": "#FFE066" 115 | }, 116 | { 117 | "percentage": 50, 118 | "color": "#DCE775" 119 | }, 120 | { 121 | "percentage": 40, 122 | "color": "#66BB6A" 123 | }, 124 | { 125 | "percentage": 30, 126 | "color": "#81C784" 127 | }, 128 | { 129 | "percentage": 20, 130 | "color": "#B3E6B3" 131 | }, 132 | { 133 | "percentage": 10, 134 | "color": "#E8F5E9" 135 | }, 136 | { 137 | "percentage": 0, 138 | "color": "#FFFFFF" 139 | } 140 | ], 141 | "items": { 142 | "type": "object", 143 | "required": [ 144 | "percentage", 145 | "color" 146 | ], 147 | "properties": { 148 | "percentage": { 149 | "type": "number", 150 | "description": "Minimum percentage threshold (0-100).", 151 | "minimum": 0, 152 | "maximum": 100 153 | }, 154 | "color": { 155 | "type": "string", 156 | "description": "Color to use. Can be a theme color ID (e.g., 'charts.red', 'statusBarItem.foreground') or a hex color code (e.g., '#FF0000')." 157 | } 158 | } 159 | } 160 | }, 161 | "cursorStats.enableAlerts": { 162 | "type": "boolean", 163 | "default": true, 164 | "description": "Enable usage alert notifications.", 165 | "scope": "window" 166 | }, 167 | "cursorStats.usageAlertThresholds": { 168 | "type": "array", 169 | "default": [ 170 | 10, 171 | 30, 172 | 50, 173 | 75, 174 | 90, 175 | 100 176 | ], 177 | "description": "Percentage thresholds at which to show usage alerts.", 178 | "items": { 179 | "type": "number", 180 | "minimum": 0, 181 | "maximum": 100 182 | }, 183 | "scope": "window" 184 | }, 185 | "cursorStats.refreshInterval": { 186 | "type": "number", 187 | "default": 60, 188 | "minimum": 10, 189 | "description": "How often to refresh the stats (in seconds). Minimum 10 seconds.", 190 | "scope": "window" 191 | }, 192 | "cursorStats.showTotalRequests": { 193 | "type": "boolean", 194 | "default": false, 195 | "description": "Show total requests (fast requests + usage-based requests) in the status bar.", 196 | "scope": "window" 197 | }, 198 | "cursorStats.spendingAlertThreshold": { 199 | "type": "number", 200 | "default": 1, 201 | "minimum": 0, 202 | "description": "Dollar amount threshold for spending notifications (0 to disable, any positive amount in dollars).", 203 | "scope": "window" 204 | }, 205 | "cursorStats.currency": { 206 | "type": "string", 207 | "default": "USD", 208 | "enum": [ 209 | "USD", 210 | "EUR", 211 | "GBP", 212 | "JPY", 213 | "AUD", 214 | "CAD", 215 | "CHF", 216 | "CNY", 217 | "INR", 218 | "MXN", 219 | "BRL", 220 | "RUB", 221 | "KRW", 222 | "SGD", 223 | "NZD", 224 | "TRY", 225 | "ZAR", 226 | "SEK", 227 | "NOK", 228 | "DKK", 229 | "HKD", 230 | "TWD", 231 | "PHP", 232 | "THB", 233 | "IDR", 234 | "VND", 235 | "ILS", 236 | "AED", 237 | "SAR", 238 | "MYR", 239 | "PLN", 240 | "CZK", 241 | "HUF", 242 | "RON", 243 | "BGN", 244 | "HRK", 245 | "EGP", 246 | "QAR", 247 | "KWD", 248 | "MAD" 249 | ], 250 | "enumDescriptions": [ 251 | "US Dollar", 252 | "Euro", 253 | "British Pound", 254 | "Japanese Yen", 255 | "Australian Dollar", 256 | "Canadian Dollar", 257 | "Swiss Franc", 258 | "Chinese Yuan", 259 | "Indian Rupee", 260 | "Mexican Peso", 261 | "Brazilian Real", 262 | "Russian Ruble", 263 | "South Korean Won", 264 | "Singapore Dollar", 265 | "New Zealand Dollar", 266 | "Turkish Lira", 267 | "South African Rand", 268 | "Swedish Krona", 269 | "Norwegian Krone", 270 | "Danish Krone", 271 | "Hong Kong Dollar", 272 | "Taiwan Dollar", 273 | "Philippine Peso", 274 | "Thai Baht", 275 | "Indonesian Rupiah", 276 | "Vietnamese Dong", 277 | "Israeli Shekel", 278 | "UAE Dirham", 279 | "Saudi Riyal", 280 | "Malaysian Ringgit", 281 | "Polish Złoty", 282 | "Czech Koruna", 283 | "Hungarian Forint", 284 | "Romanian Leu", 285 | "Bulgarian Lev", 286 | "Croatian Kuna", 287 | "Egyptian Pound", 288 | "Qatari Riyal", 289 | "Kuwaiti Dinar", 290 | "Moroccan Dirham" 291 | ], 292 | "description": "Currency to display monetary values in (default: USD)" 293 | }, 294 | "cursorStats.showProgressBars": { 295 | "type": "boolean", 296 | "default": false, 297 | "description": "Show emoji-based progress bars in the tooltip for premium requests and usage-based pricing.", 298 | "scope": "window" 299 | }, 300 | "cursorStats.progressBarLength": { 301 | "type": "number", 302 | "default": 10, 303 | "minimum": 5, 304 | "maximum": 20, 305 | "description": "Length of the progress bars (number of characters).", 306 | "scope": "window" 307 | }, 308 | "cursorStats.progressBarWarningThreshold": { 309 | "type": "number", 310 | "default": 50, 311 | "minimum": 0, 312 | "maximum": 100, 313 | "description": "Percentage threshold at which progress bars turn yellow (warning).", 314 | "scope": "window" 315 | }, 316 | "cursorStats.progressBarCriticalThreshold": { 317 | "type": "number", 318 | "default": 75, 319 | "minimum": 0, 320 | "maximum": 100, 321 | "description": "Percentage threshold at which progress bars turn red (critical).", 322 | "scope": "window" 323 | }, 324 | "cursorStats.customDatabasePath": { 325 | "type": "string", 326 | "default": "", 327 | "description": "Custom path to the Cursor database file. Leave empty to use default location.", 328 | "scope": "window" 329 | }, 330 | "cursorStats.excludeWeekends": { 331 | "type": "boolean", 332 | "default": false, 333 | "description": "Exclude weekends from period progress calculations and daily remaining requests.", 334 | "scope": "window" 335 | }, 336 | "cursorStats.showDailyRemaining": { 337 | "type": "boolean", 338 | "default": false, 339 | "description": "Show estimated fast requests remaining per day in the tooltip.", 340 | "scope": "window" 341 | }, 342 | "cursorStats.language": { 343 | "type": "string", 344 | "default": "en", 345 | "enum": [ 346 | "en", 347 | "zh", 348 | "ko" 349 | ], 350 | "enumDescriptions": [ 351 | "English", 352 | "中文 (Chinese)", 353 | "한국어 (Korean)" 354 | ], 355 | "description": "Language for the extension interface and messages." 356 | } 357 | } 358 | } 359 | ] 360 | }, 361 | "scripts": { 362 | "vscode:prepublish": "npm run compile", 363 | "compile": "tsc -p ./ && npm run copy-locales", 364 | "copy-locales": "node -e \"const fs=require('fs'),path=require('path'); const src='src/locales',dest='out/locales'; if(fs.existsSync(src)){fs.mkdirSync(dest,{recursive:true}); fs.readdirSync(src).forEach(file=>fs.copyFileSync(path.join(src,file),path.join(dest,file))); console.log('Locales copied to out/locales');}\"", 365 | "watch": "tsc -watch -p ./", 366 | "pretest": "npm run compile && npm run lint", 367 | "lint": "eslint src --ext ts", 368 | "test": "vscode-test" 369 | }, 370 | "devDependencies": { 371 | "@types/jsonwebtoken": "9.0.9", 372 | "@types/mocha": "10.0.10", 373 | "@types/node": "22.15.20", 374 | "@types/semver": "7.7.0", 375 | "@types/vscode": "1.96.0", 376 | "@typescript-eslint/eslint-plugin": "8.32.1", 377 | "@typescript-eslint/parser": "8.32.1", 378 | "@vscode/test-cli": "^0.0.10", 379 | "eslint": "9.27.0", 380 | "typescript": "5.8.3", 381 | "@types/sql.js": "1.4.9" 382 | }, 383 | "dependencies": { 384 | "axios": "1.9.0", 385 | "glob": "^11.0.2", 386 | "jsonwebtoken": "^9.0.2", 387 | "lru-cache": "^11.1.0", 388 | "marked": "15.0.12", 389 | "semver": "7.7.2", 390 | "sql.js": "1.13.0" 391 | }, 392 | "extensionKind": [ 393 | "ui" 394 | ] 395 | } 396 | -------------------------------------------------------------------------------- /src/handlers/notifications.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { log } from '../utils/logger'; 3 | import { convertAndFormatCurrency } from '../utils/currency'; 4 | import { UsageInfo } from '../interfaces/types'; 5 | import { t } from '../utils/i18n'; 6 | 7 | // Track which thresholds have been notified in the current session 8 | const notifiedPremiumThresholds = new Set(); 9 | const notifiedUsageBasedThresholds = new Set(); 10 | const notifiedSpendingThresholds = new Set(); 11 | let isNotificationInProgress = false; 12 | let unpaidInvoiceNotifiedThisSession = false; 13 | let isSpendingCheckInitialRun = true; // New state variable for spending checks 14 | 15 | // Reset notification tracking 16 | export function resetNotifications() { 17 | notifiedPremiumThresholds.clear(); 18 | notifiedUsageBasedThresholds.clear(); 19 | notifiedSpendingThresholds.clear(); 20 | isNotificationInProgress = false; 21 | unpaidInvoiceNotifiedThisSession = false; 22 | isSpendingCheckInitialRun = true; // Reset this flag as well 23 | log('[Notifications] Reset notification tracking, including spending check initial run flag.'); 24 | } 25 | 26 | export async function checkAndNotifySpending(totalSpent: number) { 27 | if (isNotificationInProgress) { 28 | return; 29 | } 30 | 31 | const config = vscode.workspace.getConfiguration('cursorStats'); 32 | const spendingThreshold = config.get('spendingAlertThreshold', 1); 33 | 34 | // If threshold is 0 or less, spending notifications are disabled 35 | if (spendingThreshold <= 0) { 36 | log('[Notifications] Spending alerts disabled (threshold <= 0).'); 37 | return; 38 | } 39 | 40 | try { 41 | isNotificationInProgress = true; 42 | if (isSpendingCheckInitialRun) { 43 | // On the initial run (or after a reset), prime the notifiedSpendingThresholds 44 | // by adding all multiples of spendingThreshold that are less than or equal to totalSpent. 45 | const multiplesToPrime = Math.floor(totalSpent / spendingThreshold); 46 | for (let i = 1; i <= multiplesToPrime; i++) { 47 | notifiedSpendingThresholds.add(i); 48 | } 49 | isSpendingCheckInitialRun = false; // Clear the flag after priming 50 | } 51 | 52 | let lastNotifiedMultiple = 0; 53 | if (notifiedSpendingThresholds.size > 0) { 54 | lastNotifiedMultiple = Math.max(0, ...Array.from(notifiedSpendingThresholds)); 55 | } 56 | 57 | let multipleToConsider = lastNotifiedMultiple + 1; 58 | 59 | while (true) { 60 | const currentThresholdAmount = multipleToConsider * spendingThreshold; 61 | if (totalSpent >= currentThresholdAmount) { 62 | log(`[Notifications] Spending threshold $${currentThresholdAmount.toFixed(2)} met or exceeded (Total spent: $${totalSpent.toFixed(2)}). Triggering notification.`); 63 | 64 | const formattedCurrentThreshold = await convertAndFormatCurrency(currentThresholdAmount); 65 | const formattedTotalSpent = await convertAndFormatCurrency(totalSpent); 66 | 67 | // For the detail message, calculate the *next* threshold after the one we're notifying about 68 | const nextHigherThresholdAmount = (multipleToConsider + 1) * spendingThreshold; 69 | const formattedNextHigherThreshold = await convertAndFormatCurrency(nextHigherThresholdAmount); 70 | 71 | const message = t('notifications.spendingThresholdReached', { amount: formattedCurrentThreshold }); 72 | const detail = `${t('notifications.currentTotalCost', { amount: formattedTotalSpent })} ${t('notifications.nextNotificationAt', { amount: formattedNextHigherThreshold })}`; 73 | 74 | // Show the notification 75 | const notificationSelection = await vscode.window.showInformationMessage( 76 | message, 77 | { modal: false, detail }, 78 | t('notifications.manageLimitTitle'), 79 | t('notifications.dismiss') 80 | ); 81 | 82 | if (notificationSelection === t('notifications.manageLimitTitle')) { 83 | await vscode.commands.executeCommand('cursor-stats.setLimit'); 84 | } 85 | 86 | // Mark this multiple as notified 87 | notifiedSpendingThresholds.add(multipleToConsider); 88 | multipleToConsider++; 89 | } else { 90 | // totalSpent is less than currentThresholdAmount, so we haven't crossed this one yet. Stop. 91 | break; 92 | } 93 | } 94 | } catch (error) { 95 | log(`[Notifications] Error during checkAndNotifySpending: ${error instanceof Error ? error.message : String(error)}`, true); 96 | } 97 | finally { 98 | isNotificationInProgress = false; 99 | } 100 | } 101 | 102 | export async function checkAndNotifyUnpaidInvoice(token: string) { 103 | if (unpaidInvoiceNotifiedThisSession || isNotificationInProgress) { 104 | return; 105 | } 106 | 107 | try { 108 | isNotificationInProgress = true; 109 | log('[Notifications] Checking for unpaid mid-month invoice notification.'); 110 | 111 | const notification = await vscode.window.showWarningMessage( 112 | t('notifications.unpaidInvoice'), 113 | t('notifications.openBillingPage'), 114 | t('notifications.dismiss') 115 | ); 116 | 117 | if (notification === t('notifications.openBillingPage')) { 118 | try { 119 | const { getStripeSessionUrl } = await import('../services/api'); // Lazy import 120 | const stripeUrl = await getStripeSessionUrl(token); 121 | vscode.env.openExternal(vscode.Uri.parse(stripeUrl)); 122 | } catch (error) { 123 | log('[Notifications] Failed to get Stripe URL, falling back to settings page.', true); 124 | vscode.env.openExternal(vscode.Uri.parse('https://www.cursor.com/settings')); 125 | } 126 | } 127 | unpaidInvoiceNotifiedThisSession = true; 128 | log('[Notifications] Unpaid invoice notification shown.'); 129 | 130 | } finally { 131 | isNotificationInProgress = false; 132 | } 133 | } 134 | 135 | export async function checkAndNotifyUsage(usageInfo: UsageInfo) { 136 | // Prevent concurrent notifications 137 | if (isNotificationInProgress) { 138 | return; 139 | } 140 | 141 | const config = vscode.workspace.getConfiguration('cursorStats'); 142 | const enableAlerts = config.get('enableAlerts', true); 143 | 144 | if (!enableAlerts) { 145 | return; 146 | } 147 | 148 | try { 149 | isNotificationInProgress = true; 150 | const thresholds = config.get('usageAlertThresholds', [10, 30, 50, 75, 90, 100]) 151 | .sort((a, b) => b - a); // Sort in descending order to get highest threshold first 152 | 153 | const { percentage, type, limit } = usageInfo; 154 | 155 | // If this is a usage-based notification and premium is not over limit, skip it 156 | if (type === 'usage-based' && usageInfo.premiumPercentage && usageInfo.premiumPercentage < 100) { 157 | log('[Notifications] Skipping usage-based notification as premium requests are not exhausted'); 158 | return; 159 | } 160 | 161 | // Find the highest threshold that has been exceeded 162 | const highestExceededThreshold = thresholds.find(threshold => percentage >= threshold); 163 | 164 | // Only notify if we haven't notified this threshold yet 165 | const relevantThresholds = type === 'premium' ? notifiedPremiumThresholds : notifiedUsageBasedThresholds; 166 | if (highestExceededThreshold && !relevantThresholds.has(highestExceededThreshold)) { 167 | log(`[Notifications] Highest usage threshold ${highestExceededThreshold}% exceeded for ${type} usage`); 168 | 169 | let message, detail; 170 | if (type === 'premium') { 171 | if (percentage > 100) { 172 | message = t('notifications.usageExceededLimit', { percentage: percentage.toFixed(1) }); 173 | detail = t('notifications.enableUsageBasedDetail'); 174 | } else { 175 | message = t('notifications.usageThresholdReached', { percentage: percentage.toFixed(1) }); 176 | detail = t('notifications.viewSettingsDetail'); 177 | } 178 | } else { 179 | // Only show usage-based notifications if premium is exhausted 180 | message = t('notifications.usageBasedSpendingThreshold', { percentage: percentage.toFixed(1), limit: limit || 0 }); 181 | detail = t('notifications.manageLimitDetail'); 182 | } 183 | 184 | // Show the notification 185 | const notification = await vscode.window.showWarningMessage( 186 | message, 187 | { modal: false, detail }, 188 | type === 'premium' && percentage > 100 ? t('notifications.enableUsageBasedTitle') : type === 'premium' ? t('notifications.viewSettingsTitle') : t('notifications.manageLimitTitle'), 189 | t('notifications.dismiss') 190 | ); 191 | 192 | if (notification === t('notifications.viewSettingsTitle')) { 193 | try { 194 | await vscode.commands.executeCommand('workbench.action.openSettings', '@ext:Dwtexe.cursor-stats'); 195 | } catch (error) { 196 | log('[Notifications] Failed to open settings directly, trying alternative method...', true); 197 | try { 198 | await vscode.commands.executeCommand('workbench.action.openSettings'); 199 | await vscode.commands.executeCommand('workbench.action.search.toggleQueryDetails'); 200 | await vscode.commands.executeCommand('workbench.action.search.action.replaceAll', '@ext:Dwtexe.cursor-stats'); 201 | } catch (fallbackError) { 202 | log('[Notifications] Failed to open settings with fallback method', true); 203 | vscode.window.showErrorMessage(t('notifications.failedToOpenSettings')); 204 | } 205 | } 206 | } else if (notification === t('notifications.manageLimitTitle') || notification === t('notifications.enableUsageBasedTitle')) { 207 | await vscode.commands.executeCommand('cursor-stats.setLimit'); 208 | } 209 | 210 | // Mark all thresholds up to and including the current one as notified 211 | thresholds.forEach(threshold => { 212 | if (threshold <= highestExceededThreshold) { 213 | relevantThresholds.add(threshold); 214 | } 215 | }); 216 | } 217 | 218 | // Clear notifications for thresholds that are no longer exceeded 219 | for (const threshold of relevantThresholds) { 220 | if (percentage < threshold) { 221 | relevantThresholds.delete(threshold); 222 | log(`[Notifications] Cleared notification for threshold ${threshold}% as ${type} usage dropped below it`); 223 | } 224 | } 225 | } finally { 226 | isNotificationInProgress = false; 227 | } 228 | } -------------------------------------------------------------------------------- /src/interfaces/i18n.ts: -------------------------------------------------------------------------------- 1 | // Language pack interface definition 2 | export interface LanguagePack { 3 | // Status bar related 4 | statusBar: { 5 | premiumFastRequests: string; 6 | usageBasedPricing: string; 7 | teamUsage: string; 8 | period: string; 9 | utilized: string; 10 | used: string; 11 | remaining: string; 12 | limit: string; 13 | spent: string; 14 | of: string; 15 | perDay: string; 16 | dailyRemaining: string; 17 | weekdaysOnly: string; 18 | today: string; 19 | isWeekend: string; 20 | cursorUsageStats: string; 21 | errorState: string; 22 | enabled: string; 23 | disabled: string; 24 | noUsageRecorded: string; 25 | usageBasedDisabled: string; 26 | errorCheckingStatus: string; 27 | unableToCheckStatus: string; 28 | unpaidAmount: string; 29 | youHavePaid: string; 30 | accountSettings: string; 31 | currency: string; 32 | extensionSettings: string; 33 | refresh: string; 34 | 35 | // New keys for updateStats 36 | noTokenFound: string; 37 | couldNotRetrieveToken: string; 38 | requestsUsed: string; 39 | fastRequestsPeriod: string; 40 | usageBasedPeriod: string; 41 | currentUsage: string; 42 | total: string; 43 | unpaid: string; 44 | discounted: string; 45 | unknownModel: string; 46 | unknownItem: string; 47 | totalCost: string; 48 | noUsageDataAvailable: string; 49 | 50 | // New keys for progressBars 51 | usage: string; 52 | weekday: string; 53 | weekdays: string; 54 | 55 | // Additional status bar keys 56 | month: string; 57 | 58 | // New keys for cooldown 59 | apiUnavailable: string; 60 | 61 | months: { 62 | january: string; 63 | february: string; 64 | march: string; 65 | april: string; 66 | may: string; 67 | june: string; 68 | july: string; 69 | august: string; 70 | september: string; 71 | october: string; 72 | november: string; 73 | december: string; 74 | }; 75 | }; 76 | 77 | // Notification related 78 | notifications: { 79 | usageThresholdReached: string; 80 | usageExceededLimit: string; 81 | spendingThresholdReached: string; 82 | unpaidInvoice: string; 83 | enableUsageBasedTitle: string; 84 | enableUsageBasedDetail: string; 85 | viewSettingsTitle: string; 86 | viewSettingsDetail: string; 87 | manageLimitTitle: string; 88 | manageLimitDetail: string; 89 | nextNotificationAt: string; 90 | currentTotalCost: string; 91 | payInvoiceToContinue: string; 92 | openBillingPage: string; 93 | dismiss: string; 94 | unknownModelsDetected: string; 95 | usageBasedSpendingThreshold: string; 96 | failedToOpenSettings: string; 97 | }; 98 | 99 | // Command related 100 | commands: { 101 | refreshStats: string; 102 | openSettings: string; 103 | setLimit: string; 104 | selectCurrency: string; 105 | createReport: string; 106 | enableUsageBased: string; 107 | setMonthlyLimit: string; 108 | disableUsageBased: string; 109 | selectLanguage: string; 110 | languageChanged: string; 111 | openGitHubIssues: string; 112 | 113 | // Report generation related 114 | createReportProgress: string; 115 | gatheringData: string; 116 | completed: string; 117 | reportCreatedSuccessfully: string; 118 | openFile: string; 119 | openFolder: string; 120 | 121 | // New keys for extension.ts 122 | enableUsageBasedOption: string; 123 | enableUsageBasedDescription: string; 124 | setMonthlyLimitOption: string; 125 | setMonthlyLimitDescription: string; 126 | disableUsageBasedOption: string; 127 | disableUsageBasedDescription: string; 128 | enterMonthlyLimit: string; 129 | enterNewMonthlyLimit: string; 130 | validNumberRequired: string; 131 | usageBasedEnabledWithLimit: string; 132 | usageBasedAlreadyEnabled: string; 133 | limitUpdatedTo: string; 134 | enableUsageBasedFirst: string; 135 | usageBasedDisabled: string; 136 | usageBasedAlreadyDisabled: string; 137 | failedToManageLimit: string; 138 | currentStatus: string; 139 | selectCurrencyPrompt: string; 140 | currentLanguagePrompt: string; 141 | selectLanguagePrompt: string; 142 | }; 143 | 144 | // Settings related 145 | settings: { 146 | enableUsageBasedPricing: string; 147 | changeMonthlyLimit: string; 148 | disableUsageBasedPricing: string; 149 | enableUsageBasedDescription: string; 150 | setLimitDescription: string; 151 | disableUsageBasedDescription: string; 152 | currentLimit: string; 153 | enterNewLimit: string; 154 | invalidLimit: string; 155 | limitUpdated: string; 156 | signInRequired: string; 157 | updateFailed: string; 158 | }; 159 | 160 | // Error messages 161 | errors: { 162 | tokenNotFound: string; 163 | apiError: string; 164 | databaseError: string; 165 | networkError: string; 166 | updateFailed: string; 167 | unknownError: string; 168 | 169 | // Report generation errors 170 | failedToCreateReport: string; 171 | errorCreatingReport: string; 172 | }; 173 | 174 | // Time related 175 | time: { 176 | day: string; 177 | days: string; 178 | hour: string; 179 | hours: string; 180 | minute: string; 181 | minutes: string; 182 | second: string; 183 | seconds: string; 184 | ago: string; 185 | refreshing: string; 186 | lastUpdated: string; 187 | }; 188 | 189 | // Currency related 190 | currency: { 191 | usd: string; 192 | eur: string; 193 | gbp: string; 194 | jpy: string; 195 | cny: string; 196 | }; 197 | 198 | // Progress bar related 199 | progressBar: { 200 | errorParsingDates: string; 201 | dailyRemainingLimitReached: string; 202 | dailyRemainingWeekend: string; 203 | dailyRemainingPeriodEnding: string; 204 | dailyRemainingCalculation: string; 205 | }; 206 | 207 | // API related 208 | api: { 209 | midMonthPayment: string; 210 | toolCalls: string; 211 | fastPremium: string; 212 | requestUnit: string; 213 | }; 214 | 215 | // GitHub related 216 | github: { 217 | preRelease: string; 218 | stableRelease: string; 219 | latest: string; 220 | updateAvailable: string; 221 | changesTitle: string; 222 | sourceCodeZip: string; 223 | sourceCodeTarGz: string; 224 | viewFullRelease: string; 225 | installedMessage: string; 226 | }; 227 | } -------------------------------------------------------------------------------- /src/interfaces/types.ts: -------------------------------------------------------------------------------- 1 | export interface UsageItem { 2 | calculation: string; 3 | totalDollars: string; 4 | description?: string; 5 | modelNameForTooltip?: string; 6 | isDiscounted?: boolean; 7 | } 8 | 9 | export interface UsageInfo { 10 | percentage: number; 11 | type: 'premium' | 'usage-based'; 12 | limit?: number; 13 | totalSpent?: number; 14 | premiumPercentage?: number; 15 | } 16 | 17 | export interface UsageBasedPricing { 18 | items: UsageItem[]; 19 | hasUnpaidMidMonthInvoice: boolean; 20 | midMonthPayment: number; 21 | } 22 | 23 | export interface CursorStats { 24 | currentMonth: { 25 | month: number; 26 | year: number; 27 | usageBasedPricing: UsageBasedPricing; 28 | }; 29 | lastMonth: { 30 | month: number; 31 | year: number; 32 | usageBasedPricing: UsageBasedPricing; 33 | }; 34 | premiumRequests: { 35 | current: number; 36 | limit: number; 37 | startOfMonth: string; 38 | }; 39 | } 40 | 41 | export interface ProgressBarSettings { 42 | barLength: number; 43 | warningThreshold: number; 44 | criticalThreshold: number; 45 | } 46 | 47 | export interface SQLiteRow { 48 | value: string; 49 | } 50 | 51 | export interface SQLiteError extends Error { 52 | code?: string; 53 | errno?: number; 54 | } 55 | 56 | export interface AxiosErrorData { 57 | status?: number; 58 | data?: any; 59 | message?: string; 60 | } 61 | 62 | export interface ExtendedAxiosError { 63 | response?: AxiosErrorData; 64 | message: string; 65 | } 66 | 67 | export interface ComposerData { 68 | conversation: Array<{ 69 | timingInfo?: { 70 | clientStartTime: number; 71 | [key: string]: any; 72 | }; 73 | [key: string]: any; 74 | }>; 75 | } 76 | 77 | export interface TimingInfo { 78 | key: string; 79 | timestamp: number; 80 | timingInfo: { 81 | clientStartTime: number; 82 | [key: string]: any; 83 | }; 84 | } 85 | 86 | export interface UsageLimitResponse { 87 | hardLimit?: number; 88 | noUsageBasedAllowed?: boolean; 89 | } 90 | 91 | export interface GitHubRelease { 92 | tag_name: string; 93 | name: string; 94 | body: string; 95 | prerelease: boolean; 96 | html_url: string; 97 | zipball_url: string; 98 | tarball_url: string; 99 | assets: Array<{ 100 | name: string; 101 | browser_download_url: string; 102 | }>; 103 | } 104 | 105 | export interface ReleaseCheckResult { 106 | hasUpdate: boolean; 107 | currentVersion: string; 108 | latestVersion: string; 109 | isPrerelease: boolean; 110 | releaseUrl: string; 111 | releaseNotes: string; 112 | releaseName: string; 113 | zipballUrl: string; 114 | tarballUrl: string; 115 | assets: Array<{ 116 | name: string; 117 | downloadUrl: string; 118 | }>; 119 | } 120 | 121 | export interface CursorUsageResponse { 122 | 'gpt-4': { 123 | numRequests: number; 124 | numRequestsTotal: number; 125 | numTokens: number; 126 | maxRequestUsage: number; 127 | maxTokenUsage: number | null; 128 | }; 129 | 'gpt-3.5-turbo': { 130 | numRequests: number; 131 | numRequestsTotal: number; 132 | numTokens: number; 133 | maxRequestUsage: number | null; 134 | maxTokenUsage: number | null; 135 | }; 136 | 'gpt-4-32k': { 137 | numRequests: number; 138 | numRequestsTotal: number; 139 | numTokens: number; 140 | maxRequestUsage: number | null; 141 | maxTokenUsage: number | null; 142 | }; 143 | startOfMonth: string; 144 | } 145 | 146 | export interface TeamInfo { 147 | teams: Team[]; 148 | } 149 | 150 | export interface Team { 151 | name: string; 152 | id: number; 153 | role: string; 154 | seats: number; 155 | hasBilling: boolean; 156 | requestQuotaPerSeat: number; 157 | privacyModeForced: boolean; 158 | allowSso: boolean; 159 | } 160 | 161 | export interface TeamMemberInfo { 162 | teamMembers: TeamMember[]; 163 | userId: number; 164 | } 165 | 166 | export interface TeamMember { 167 | name: string; 168 | email: string; 169 | id: number; 170 | role: string; 171 | } 172 | 173 | export interface TeamUsageResponse { 174 | teamMemberUsage: TeamMemberUsage[]; 175 | } 176 | 177 | export interface TeamMemberUsage { 178 | id: number; 179 | usageData: UsageData[]; 180 | } 181 | 182 | export interface UsageData { 183 | modelType: string; 184 | numRequests: number; 185 | numTokens: number; 186 | maxRequestUsage: number; 187 | lastUsage: string; 188 | copilotUsage: number; 189 | docsCount: number; 190 | copilotAcceptedUsage: number; 191 | } 192 | 193 | export interface UserCache { 194 | userId: number; 195 | jwtSub: string; 196 | isTeamMember: boolean; 197 | teamId?: number; 198 | lastChecked: number; 199 | startOfMonth?: string; 200 | } 201 | 202 | export interface CurrencyRates { 203 | date: string; 204 | usd: { 205 | [key: string]: number; 206 | }; 207 | } 208 | 209 | export interface CurrencyCache { 210 | rates: CurrencyRates; 211 | timestamp: number; 212 | } 213 | 214 | export interface CursorReport { 215 | timestamp: string; 216 | extensionVersion: string; 217 | os: string; 218 | vsCodeVersion: string; 219 | cursorStats: CursorStats | null; 220 | usageLimitResponse: UsageLimitResponse | null; 221 | premiumUsage: CursorUsageResponse | null; 222 | teamInfo: { 223 | isTeamMember: boolean; 224 | teamId?: number; 225 | userId?: number; 226 | } | null; 227 | teamUsage: TeamUsageResponse | null; 228 | rawResponses: { 229 | cursorStats?: any; 230 | usageLimit?: any; 231 | premiumUsage?: any; 232 | teamInfo?: any; 233 | teamUsage?: any; 234 | monthlyInvoice?: { 235 | current?: any; 236 | last?: any; 237 | }; 238 | }; 239 | logs: string[]; 240 | errors: { 241 | [key: string]: string; 242 | }; 243 | } -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": { 3 | "premiumFastRequests": "Premium Fast Requests", 4 | "usageBasedPricing": "Usage-Based Pricing", 5 | "teamUsage": "Team Usage", 6 | "period": "Period", 7 | "utilized": "utilized", 8 | "used": "used", 9 | "remaining": "remaining", 10 | "limit": "limit", 11 | "spent": "spent", 12 | "of": "of", 13 | "perDay": "per day", 14 | "dailyRemaining": "daily remaining", 15 | "weekdaysOnly": "weekdays only", 16 | "today": "today", 17 | "isWeekend": "is weekend", 18 | "cursorUsageStats": "Cursor Usage Stats", 19 | "errorState": "Error State", 20 | "enabled": "Enabled", 21 | "disabled": "Disabled", 22 | "noUsageRecorded": "No usage recorded for this period", 23 | "usageBasedDisabled": "Usage-based pricing is currently disabled", 24 | "errorCheckingStatus": "Error checking usage-based pricing status", 25 | "unableToCheckStatus": "Unable to check usage-based pricing status", 26 | "unpaidAmount": "Unpaid: {amount}", 27 | "youHavePaid": "ℹ️ You have paid **{amount}** of this cost already", 28 | "accountSettings": "Account Settings", 29 | "currency": "Currency", 30 | "extensionSettings": "Extension Settings", 31 | "refresh": "Refresh", 32 | "noTokenFound": "Cursor Stats: No token found", 33 | "couldNotRetrieveToken": "⚠️ Could not retrieve Cursor token from database", 34 | "requestsUsed": "requests used", 35 | "fastRequestsPeriod": "Fast Requests Period", 36 | "usageBasedPeriod": "Usage Based Period", 37 | "currentUsage": "Current Usage", 38 | "total": "Total", 39 | "unpaid": "Unpaid", 40 | "discounted": "discounted", 41 | "unknownModel": "unknown-model", 42 | "unknownItem": "Unknown Item", 43 | "totalCost": "Total Cost", 44 | "noUsageDataAvailable": "No usage data available", 45 | "usage": "Usage", 46 | "weekday": "weekday", 47 | "weekdays": "weekdays", 48 | "month": "Month", 49 | "apiUnavailable": "Cursor API Unavailable (Retrying in {countdown})", 50 | "months": { 51 | "january": "January", 52 | "february": "February", 53 | "march": "March", 54 | "april": "April", 55 | "may": "May", 56 | "june": "June", 57 | "july": "July", 58 | "august": "August", 59 | "september": "September", 60 | "october": "October", 61 | "november": "November", 62 | "december": "December" 63 | } 64 | }, 65 | "progressBar": { 66 | "errorParsingDates": "Error parsing dates", 67 | "dailyRemainingLimitReached": "📊 Daily Remaining: 0 requests/day (limit reached)", 68 | "dailyRemainingWeekend": "📊 Daily Remaining: Weekend - calculations resume Monday", 69 | "dailyRemainingPeriodEnding": "📊 Daily Remaining: Period ending soon", 70 | "dailyRemainingCalculation": "📊 Daily Remaining: {requestsPerDay} requests/{dayType}\n ({remainingRequests} requests ÷ {remainingDays} {dayTypePlural})" 71 | }, 72 | "api": { 73 | "midMonthPayment": "Mid-month payment", 74 | "toolCalls": "tool calls", 75 | "fastPremium": "fast premium", 76 | "requestUnit": "req" 77 | }, 78 | "github": { 79 | "preRelease": "Pre-release", 80 | "stableRelease": "Stable release", 81 | "latest": "Latest", 82 | "updateAvailable": "{releaseType} {releaseName} is available! You are on {currentVersion}", 83 | "changesTitle": "{releaseType} {version} Changes", 84 | "sourceCodeZip": "Source code (zip)", 85 | "sourceCodeTarGz": "Source code (tar.gz)", 86 | "viewFullRelease": "View full release on GitHub", 87 | "installedMessage": "Cursor Stats {version} has been installed" 88 | }, 89 | "notifications": { 90 | "usageThresholdReached": "Premium request usage has reached {percentage}%", 91 | "usageExceededLimit": "Premium request usage has exceeded limit ({percentage}%)", 92 | "spendingThresholdReached": "Your Cursor usage spending has reached {amount}", 93 | "unpaidInvoice": "⚠️ You have an unpaid mid-month invoice. Please pay it to continue using usage-based pricing.", 94 | "enableUsageBasedTitle": "Enable Usage-Based", 95 | "enableUsageBasedDetail": "Enable usage-based pricing to continue using premium models.", 96 | "viewSettingsTitle": "View Settings", 97 | "viewSettingsDetail": "Click View Settings to manage your usage limits.", 98 | "manageLimitTitle": "Manage Limit", 99 | "manageLimitDetail": "Click Manage Limit to adjust your usage-based pricing settings.", 100 | "nextNotificationAt": "Next spending notification at {amount}.", 101 | "currentTotalCost": "Current total usage cost is {amount}.", 102 | "payInvoiceToContinue": "Please pay your invoice to continue using usage-based pricing.", 103 | "openBillingPage": "Open Billing Page", 104 | "dismiss": "Dismiss", 105 | "unknownModelsDetected": "New or unhandled Cursor model terms detected: \"{models}\". If these seem like new models, please create a report and submit it on GitHub.", 106 | "usageBasedSpendingThreshold": "Usage-based spending has reached {percentage}% of your ${limit} limit", 107 | "failedToOpenSettings": "Failed to open Cursor Stats settings. Please try opening VS Code settings manually." 108 | }, 109 | "commands": { 110 | "refreshStats": "Cursor Stats: Refresh Statistics", 111 | "openSettings": "Cursor Stats: Open Settings", 112 | "setLimit": "Cursor Stats: Usage Based Pricing Settings", 113 | "selectCurrency": "Cursor Stats: Select Display Currency", 114 | "createReport": "Cursor Stats: Generate Diagnostic Report", 115 | "enableUsageBased": "Enable Usage-Based Pricing", 116 | "setMonthlyLimit": "Set Monthly Limit", 117 | "disableUsageBased": "Disable Usage-Based Pricing", 118 | "selectLanguage": "Cursor Stats: Select Language", 119 | "languageChanged": "Language changed to {language}. Interface will update automatically.", 120 | "openGitHubIssues": "Open GitHub Issues", 121 | "createReportProgress": "Creating Cursor Stats Report", 122 | "gatheringData": "Gathering data...", 123 | "completed": "Completed!", 124 | "reportCreatedSuccessfully": "Report created successfully!\n{fileName}", 125 | "openFile": "Open File", 126 | "openFolder": "Open Folder", 127 | "enableUsageBasedOption": "$(check) Enable Usage-Based Pricing", 128 | "enableUsageBasedDescription": "Turn on usage-based pricing and set a limit", 129 | "setMonthlyLimitOption": "$(pencil) Set Monthly Limit", 130 | "setMonthlyLimitDescription": "Change your monthly spending limit", 131 | "disableUsageBasedOption": "$(x) Disable Usage-Based Pricing", 132 | "disableUsageBasedDescription": "Turn off usage-based pricing", 133 | "enterMonthlyLimit": "Enter monthly spending limit in dollars", 134 | "enterNewMonthlyLimit": "Enter new monthly spending limit in dollars", 135 | "validNumberRequired": "Please enter a valid number greater than 0", 136 | "usageBasedEnabledWithLimit": "Usage-based pricing enabled with {limit} limit", 137 | "usageBasedAlreadyEnabled": "Usage-based pricing is already enabled", 138 | "limitUpdatedTo": "Monthly limit updated to {limit}", 139 | "enableUsageBasedFirst": "Please enable usage-based pricing first", 140 | "usageBasedDisabled": "Usage-based pricing disabled", 141 | "usageBasedAlreadyDisabled": "Usage-based pricing is already disabled", 142 | "failedToManageLimit": "Failed to manage usage limit: {error}", 143 | "currentStatus": "Current status: {status} {limit}", 144 | "selectCurrencyPrompt": "Select currency for display", 145 | "currentLanguagePrompt": "Current: {language}. Select a language for Cursor Stats interface", 146 | "selectLanguagePrompt": "Select Language / 选择语言 / 언어 선택" 147 | }, 148 | "settings": { 149 | "enableUsageBasedPricing": "Enable Usage-Based Pricing", 150 | "changeMonthlyLimit": "Change Monthly Limit", 151 | "disableUsageBasedPricing": "Disable Usage-Based Pricing", 152 | "enableUsageBasedDescription": "Turn on usage-based pricing and set a limit", 153 | "setLimitDescription": "Change your monthly spending limit", 154 | "disableUsageBasedDescription": "Turn off usage-based pricing", 155 | "currentLimit": "Current limit: ${limit}", 156 | "enterNewLimit": "Enter new monthly limit (in USD)", 157 | "invalidLimit": "Please enter a valid number greater than 0", 158 | "limitUpdated": "Usage limit updated successfully to ${limit}", 159 | "signInRequired": "Please sign in to Cursor first", 160 | "updateFailed": "Failed to update usage limit" 161 | }, 162 | "errors": { 163 | "tokenNotFound": "Session token not found. Please sign in to Cursor.", 164 | "apiError": "API request failed", 165 | "databaseError": "Database access error", 166 | "networkError": "Network connection error", 167 | "updateFailed": "Failed to update statistics", 168 | "unknownError": "An unknown error occurred", 169 | "failedToCreateReport": "Failed to create report. Check logs for details.", 170 | "errorCreatingReport": "Error creating report: {error}" 171 | }, 172 | "time": { 173 | "day": "day", 174 | "days": "days", 175 | "hour": "hour", 176 | "hours": "hours", 177 | "minute": "minute", 178 | "minutes": "minutes", 179 | "second": "second", 180 | "seconds": "seconds", 181 | "ago": "ago", 182 | "refreshing": "Refreshing...", 183 | "lastUpdated": "Last Updated" 184 | }, 185 | "currency": { 186 | "usd": "US Dollar", 187 | "eur": "Euro", 188 | "gbp": "British Pound", 189 | "jpy": "Japanese Yen", 190 | "aud": "Australian Dollar", 191 | "cad": "Canadian Dollar", 192 | "chf": "Swiss Franc", 193 | "cny": "Chinese Yuan", 194 | "inr": "Indian Rupee", 195 | "mxn": "Mexican Peso", 196 | "brl": "Brazilian Real", 197 | "rub": "Russian Ruble", 198 | "krw": "South Korean Won", 199 | "sgd": "Singapore Dollar", 200 | "nzd": "New Zealand Dollar", 201 | "try": "Turkish Lira", 202 | "zar": "South African Rand", 203 | "sek": "Swedish Krona", 204 | "nok": "Norwegian Krone", 205 | "dkk": "Danish Krone", 206 | "hkd": "Hong Kong Dollar", 207 | "twd": "Taiwan Dollar", 208 | "php": "Philippine Peso", 209 | "thb": "Thai Baht", 210 | "idr": "Indonesian Rupiah", 211 | "vnd": "Vietnamese Dong", 212 | "ils": "Israeli Shekel", 213 | "aed": "UAE Dirham", 214 | "sar": "Saudi Riyal", 215 | "myr": "Malaysian Ringgit", 216 | "pln": "Polish Złoty", 217 | "czk": "Czech Koruna", 218 | "huf": "Hungarian Forint", 219 | "ron": "Romanian Leu", 220 | "bgn": "Bulgarian Lev", 221 | "hrk": "Croatian Kuna", 222 | "egp": "Egyptian Pound", 223 | "qar": "Qatari Riyal", 224 | "kwd": "Kuwaiti Dinar", 225 | "mad": "Moroccan Dirham" 226 | } 227 | } -------------------------------------------------------------------------------- /src/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": { 3 | "premiumFastRequests": "プレミアム高速リクエスト", 4 | "usageBasedPricing": "使用量ベース課金", 5 | "teamUsage": "チーム使用状況", 6 | "period": "期間", 7 | "utilized": "使用済み", 8 | "used": "使用済み", 9 | "remaining": "残り", 10 | "limit": "制限", 11 | "spent": "支払済み", 12 | "of": "の", 13 | "perDay": "1日あたり", 14 | "dailyRemaining": "1日あたりの残り", 15 | "weekdaysOnly": "平日のみ", 16 | "today": "今日", 17 | "isWeekend": "週末です", 18 | "cursorUsageStats": "Cursor使用統計", 19 | "errorState": "エラー状態", 20 | "enabled": "有効", 21 | "disabled": "無効", 22 | "noUsageRecorded": "この期間の使用記録はありません", 23 | "usageBasedDisabled": "現在、使用量ベース課金は無効です", 24 | "errorCheckingStatus": "使用量ベース課金の状態を確認中にエラーが発生しました", 25 | "unableToCheckStatus": "使用量ベース課金の状態を確認できません", 26 | "unpaidAmount": "未払い:{amount}", 27 | "youHavePaid": "ℹ️ この費用の **{amount}** は既に支払い済みです", 28 | "accountSettings": "アカウント設定", 29 | "currency": "通貨", 30 | "extensionSettings": "拡張機能設定", 31 | "refresh": "更新", 32 | "noTokenFound": "Cursor Stats: トークンが見つかりません", 33 | "couldNotRetrieveToken": "⚠️ データベースからCursorトークンを取得できませんでした", 34 | "requestsUsed": "使用済みリクエスト", 35 | "fastRequestsPeriod": "高速リクエスト期間", 36 | "usageBasedPeriod": "使用量ベース課金期間", 37 | "currentUsage": "現在の使用量", 38 | "total": "合計", 39 | "unpaid": "未払い", 40 | "discounted": "割引済み", 41 | "unknownModel": "不明なモデル", 42 | "unknownItem": "不明な項目", 43 | "totalCost": "合計費用", 44 | "noUsageDataAvailable": "使用データがありません", 45 | "usage": "使用量", 46 | "weekday": "平日", 47 | "weekdays": "平日", 48 | "month": "月", 49 | "apiUnavailable": "Cursor APIが利用できません({countdown}後に再試行)", 50 | "months": { 51 | "january": "1月", 52 | "february": "2月", 53 | "march": "3月", 54 | "april": "4月", 55 | "may": "5月", 56 | "june": "6月", 57 | "july": "7月", 58 | "august": "8月", 59 | "september": "9月", 60 | "october": "10月", 61 | "november": "11月", 62 | "december": "12月" 63 | } 64 | }, 65 | "progressBar": { 66 | "errorParsingDates": "日付の解析エラー", 67 | "dailyRemainingLimitReached": "📊 1日あたりの残り:0リクエスト/日(制限に達しました)", 68 | "dailyRemainingWeekend": "📊 1日あたりの残り:週末 - 計算は月曜日に再開", 69 | "dailyRemainingPeriodEnding": "📊 1日あたりの残り:期間が終了間近", 70 | "dailyRemainingCalculation": "📊 1日あたりの残り:{requestsPerDay}リクエスト/{dayType}\n ({remainingRequests}リクエスト ÷ {remainingDays}{dayTypePlural})" 71 | }, 72 | "api": { 73 | "midMonthPayment": "月中支払い", 74 | "toolCalls": "ツール呼び出し", 75 | "fastPremium": "高速プレミアム", 76 | "requestUnit": "req" 77 | }, 78 | "github": { 79 | "preRelease": "プレリリース", 80 | "stableRelease": "安定版", 81 | "latest": "最新", 82 | "updateAvailable": "{releaseType} {releaseName}が利用可能です!現在のバージョンは{currentVersion}です", 83 | "changesTitle": "{releaseType} {version}の変更点", 84 | "sourceCodeZip": "ソースコード(zip)", 85 | "sourceCodeTarGz": "ソースコード(tar.gz)", 86 | "viewFullRelease": "GitHubで完全なリリースを表示", 87 | "installedMessage": "Cursor Stats {version}がインストールされました" 88 | }, 89 | "notifications": { 90 | "usageThresholdReached": "プレミアムリクエストの使用量が{percentage}%に達しました", 91 | "usageExceededLimit": "プレミアムリクエストの使用量が制限を超えました({percentage}%)", 92 | "spendingThresholdReached": "Cursorの使用料金が{amount}に達しました", 93 | "unpaidInvoice": "⚠️ 未払いの月中請求書があります。使用量ベース課金を継続するには支払いをお願いします。", 94 | "enableUsageBasedTitle": "使用量ベース課金を有効化", 95 | "enableUsageBasedDetail": "プレミアムモデルを使用するには使用量ベース課金を有効にしてください。", 96 | "viewSettingsTitle": "設定を表示", 97 | "viewSettingsDetail": "使用制限を管理するには設定を表示をクリックしてください。", 98 | "manageLimitTitle": "制限を管理", 99 | "manageLimitDetail": "使用量ベース課金の設定を調整するには制限を管理をクリックしてください。", 100 | "nextNotificationAt": "次回の料金通知は{amount}です。", 101 | "currentTotalCost": "現在の総使用コストは{amount}です。", 102 | "payInvoiceToContinue": "使用量ベース課金を継続するには請求書の支払いをお願いします。", 103 | "openBillingPage": "請求ページを開く", 104 | "dismiss": "閉じる", 105 | "unknownModelsDetected": "新規または未対応のCursorモデル用語が検出されました:\"{models}\"。新しいモデルの場合は、レポートを作成してGitHubに提出してください。", 106 | "usageBasedSpendingThreshold": "使用量ベースの支出が${limit}の制限の{percentage}%に達しました", 107 | "failedToOpenSettings": "Cursor Statsの設定を開けませんでした。VS Codeの設定を手動で開いてみてください。" 108 | }, 109 | "commands": { 110 | "refreshStats": "Cursor Stats: 統計を更新", 111 | "openSettings": "Cursor Stats: 設定を開く", 112 | "setLimit": "Cursor Stats: 使用量ベース課金設定", 113 | "selectCurrency": "Cursor Stats: 表示通貨を選択", 114 | "createReport": "Cursor Stats: 診断レポートを生成", 115 | "enableUsageBased": "使用量ベース課金を有効化", 116 | "setMonthlyLimit": "月間制限を設定", 117 | "disableUsageBased": "使用量ベース課金を無効化", 118 | "selectLanguage": "Cursor Stats: 言語を選択", 119 | "languageChanged": "言語が{language}に変更されました。インターフェースは自動的に更新されます。", 120 | "openGitHubIssues": "GitHub Issuesを開く", 121 | "createReportProgress": "Cursor Statsレポートを作成中", 122 | "gatheringData": "データを収集中...", 123 | "completed": "完了!", 124 | "reportCreatedSuccessfully": "レポートが正常に作成されました!\n{fileName}", 125 | "openFile": "ファイルを開く", 126 | "openFolder": "フォルダを開く", 127 | "enableUsageBasedOption": "$(check) 使用量ベース課金を有効化", 128 | "enableUsageBasedDescription": "使用量ベース課金をオンにして制限を設定", 129 | "setMonthlyLimitOption": "$(pencil) 月間制限を設定", 130 | "setMonthlyLimitDescription": "月間支出制限を変更", 131 | "disableUsageBasedOption": "$(x) 使用量ベース課金を無効化", 132 | "disableUsageBasedDescription": "使用量ベース課金をオフにする", 133 | "enterMonthlyLimit": "月間支出制限をドルで入力", 134 | "enterNewMonthlyLimit": "新しい月間支出制限をドルで入力", 135 | "validNumberRequired": "0より大きい有効な数値を入力してください", 136 | "usageBasedEnabledWithLimit": "使用量ベース課金が{limit}の制限で有効化されました", 137 | "usageBasedAlreadyEnabled": "使用量ベース課金は既に有効です", 138 | "limitUpdatedTo": "月間制限が{limit}に更新されました", 139 | "enableUsageBasedFirst": "先に使用量ベース課金を有効にしてください", 140 | "usageBasedDisabled": "使用量ベース課金が無効化されました", 141 | "usageBasedAlreadyDisabled": "使用量ベース課金は既に無効です", 142 | "failedToManageLimit": "使用制限の管理に失敗しました:{error}", 143 | "currentStatus": "現在の状態:{status} {limit}", 144 | "selectCurrencyPrompt": "表示通貨を選択", 145 | "currentLanguagePrompt": "現在:{language}。Cursor Statsのインターフェース言語を選択", 146 | "selectLanguagePrompt": "言語を選択 / Select Language / 选择语言 / 언어 선택" 147 | }, 148 | "settings": { 149 | "enableUsageBasedPricing": "使用量ベース課金を有効化", 150 | "changeMonthlyLimit": "月間制限を変更", 151 | "disableUsageBasedPricing": "使用量ベース課金を無効化", 152 | "enableUsageBasedDescription": "使用量ベース課金をオンにして制限を設定", 153 | "setLimitDescription": "月間支出制限を変更", 154 | "disableUsageBasedDescription": "使用量ベース課金をオフにする", 155 | "currentLimit": "現在の制限:${limit}", 156 | "enterNewLimit": "新しい月間制限を入力(USD)", 157 | "invalidLimit": "0より大きい有効な数値を入力してください", 158 | "limitUpdated": "使用制限が正常に${limit}に更新されました", 159 | "signInRequired": "先にCursorにサインインしてください", 160 | "updateFailed": "使用制限の更新に失敗しました" 161 | }, 162 | "errors": { 163 | "tokenNotFound": "セッショントークンが見つかりません。Cursorにサインインしてください。", 164 | "apiError": "APIリクエストが失敗しました", 165 | "databaseError": "データベースアクセスエラー", 166 | "networkError": "ネットワーク接続エラー", 167 | "updateFailed": "統計の更新に失敗しました", 168 | "unknownError": "不明なエラーが発生しました", 169 | "failedToCreateReport": "レポートの作成に失敗しました。詳細はログを確認してください。", 170 | "errorCreatingReport": "レポート作成中にエラーが発生しました:{error}" 171 | }, 172 | "time": { 173 | "day": "日", 174 | "days": "日", 175 | "hour": "時間", 176 | "hours": "時間", 177 | "minute": "分", 178 | "minutes": "分", 179 | "second": "秒", 180 | "seconds": "秒", 181 | "ago": "前", 182 | "refreshing": "更新中...", 183 | "lastUpdated": "最終更新" 184 | }, 185 | "currency": { 186 | "usd": "米ドル", 187 | "eur": "ユーロ", 188 | "gbp": "英ポンド", 189 | "jpy": "日本円", 190 | "aud": "豪ドル", 191 | "cad": "カナダドル", 192 | "chf": "スイスフラン", 193 | "cny": "人民元", 194 | "inr": "インドルピー", 195 | "mxn": "メキシコペソ", 196 | "brl": "ブラジルレアル", 197 | "rub": "ロシアルーブル", 198 | "krw": "韓国ウォン", 199 | "sgd": "シンガポールドル", 200 | "nzd": "ニュージーランドドル", 201 | "try": "トルコリラ", 202 | "zar": "南アフリカランド", 203 | "sek": "スウェーデンクローナ", 204 | "nok": "ノルウェークローネ", 205 | "dkk": "デンマーククローネ", 206 | "hkd": "香港ドル", 207 | "twd": "台湾ドル", 208 | "php": "フィリピンペソ", 209 | "thb": "タイバーツ", 210 | "idr": "インドネシアルピア", 211 | "vnd": "ベトナムドン", 212 | "ils": "イスラエルシェケル", 213 | "aed": "UAEディルハム", 214 | "sar": "サウジアラビアリヤル", 215 | "myr": "マレーシアリンギット", 216 | "pln": "ポーランドズウォティ", 217 | "czk": "チェココルナ", 218 | "huf": "ハンガリーフォリント", 219 | "ron": "ルーマニアレウ", 220 | "bgn": "ブルガリアレフ", 221 | "hrk": "クロアチアクーナ", 222 | "egp": "エジプトポンド", 223 | "qar": "カタールリヤル", 224 | "kwd": "クウェートディナール", 225 | "mad": "モロッコディルハム" 226 | } 227 | } -------------------------------------------------------------------------------- /src/locales/kk.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": { 3 | "premiumFastRequests": "Премиум Жылдам Сұраулар", 4 | "usageBasedPricing": "Пайдалану Негізіндегі Баға", 5 | "teamUsage": "Команда Пайдаланысы", 6 | "period": "Кезең", 7 | "utilized": "пайдаланылды", 8 | "used": "қолданылды", 9 | "remaining": "қалды", 10 | "limit": "шектеу", 11 | "spent": "жұмсалды", 12 | "of": "дегеннен", 13 | "perDay": "күніне", 14 | "dailyRemaining": "күніне қалды", 15 | "weekdaysOnly": "тек жұмыс күндері", 16 | "today": "бүгін", 17 | "isWeekend": "демалыс күні", 18 | "cursorUsageStats": "Cursor Пайдалану Статистикасы", 19 | "errorState": "Қате Күйі", 20 | "enabled": "Қосылған", 21 | "disabled": "Өшірілген", 22 | "noUsageRecorded": "Бұл кезең үшін пайдалану тіркелмеген", 23 | "usageBasedDisabled": "Пайдалану негізіндегі баға қазіргі уақытта өшірілген", 24 | "errorCheckingStatus": "Пайдалану негізіндегі баға күйін тексеру қатесі", 25 | "unableToCheckStatus": "Пайдалану негізіндегі баға күйін тексеру мүмкін емес", 26 | "unpaidAmount": "Төленбеген: {amount}", 27 | "youHavePaid": "ℹ️ Сіз бұл шығыннан **{amount}** төледіңіз", 28 | "accountSettings": "Аккаунт Параметрлері", 29 | "currency": "Валюта", 30 | "extensionSettings": "Кеңейту Параметрлері", 31 | "refresh": "Жаңарту", 32 | "noTokenFound": "Cursor Статистикасы: Токен табылмады", 33 | "couldNotRetrieveToken": "⚠️ Мәліметтер қорынан Cursor токенін алу мүмкін болмады", 34 | "requestsUsed": "сұраулар пайдаланылды", 35 | "fastRequestsPeriod": "Жылдам Сұраулар Кезеңі", 36 | "usageBasedPeriod": "Пайдалану Негізіндегі Кезең", 37 | "currentUsage": "Ағымдағы Пайдалану", 38 | "total": "Барлығы", 39 | "unpaid": "Төленбеген", 40 | "discounted": "жеңілдікпен", 41 | "unknownModel": "белгісіз-модель", 42 | "unknownItem": "Белгісіз Элемент", 43 | "totalCost": "Жалпы Құны", 44 | "noUsageDataAvailable": "Пайдалану туралы деректер қол жетімді емес", 45 | "usage": "Пайдалану", 46 | "weekday": "жұмыс күні", 47 | "weekdays": "жұмыс күндері", 48 | "month": "Ай", 49 | "apiUnavailable": "Cursor API Қол Жетімді Емес ({countdown}-дан кейін қайта)", 50 | "months": { 51 | "january": "Қаңтар", 52 | "february": "Ақпан", 53 | "march": "Наурыз", 54 | "april": "Сәуір", 55 | "may": "Мамыр", 56 | "june": "Маусым", 57 | "july": "Шілде", 58 | "august": "Тамыз", 59 | "september": "Қыркүйек", 60 | "october": "Қазан", 61 | "november": "Қараша", 62 | "december": "Желтоқсан" 63 | } 64 | }, 65 | "progressBar": { 66 | "errorParsingDates": "Күндерді талдау қатесі", 67 | "dailyRemainingLimitReached": "📊 Күнделікті Қалдық: 0 сұрау/күн (шектеуге жетті)", 68 | "dailyRemainingWeekend": "📊 Күнделікті Қалдық: Демалыс - есептеулер дүйсенбіде жалғасады", 69 | "dailyRemainingPeriodEnding": "📊 Күнделікті Қалдық: Кезең жақында аяқталады", 70 | "dailyRemainingCalculation": "📊 Күнделікті Қалдық: {requestsPerDay} сұрау/{dayType}\n ({remainingRequests} сұрау ÷ {remainingDays} {dayTypePlural})" 71 | }, 72 | "api": { 73 | "midMonthPayment": "Ай ортасындағы төлем", 74 | "toolCalls": "құрал шақырулары", 75 | "fastPremium": "жылдам премиум", 76 | "requestUnit": "сұр" 77 | }, 78 | "github": { 79 | "preRelease": "Алдын ала шығарылым", 80 | "stableRelease": "Тұрақты шығарылым", 81 | "latest": "Соңғы", 82 | "updateAvailable": "{releaseType} {releaseName} қол жетімді! Сізде {currentVersion} нұсқасы", 83 | "changesTitle": "{releaseType} {version} Өзгерістері", 84 | "sourceCodeZip": "Бастапқы код (zip)", 85 | "sourceCodeTarGz": "Бастапқы код (tar.gz)", 86 | "viewFullRelease": "GitHub-та толық шығарылымды көру", 87 | "installedMessage": "Cursor Stats {version} орнатылды" 88 | }, 89 | "notifications": { 90 | "usageThresholdReached": "Премиум сұрау пайдаланысы {percentage}%-ға жетті", 91 | "usageExceededLimit": "Премиум сұрау пайдаланысы шектеуден асты ({percentage}%)", 92 | "spendingThresholdReached": "Cursor пайдалануға жұмсаған шығыныңыз {amount}-ға жетті", 93 | "unpaidInvoice": "⚠️ Сізде ай ортасындағы төленбеген шот бар. Пайдалану негізіндегі бағаны пайдалануды жалғастыру үшін оны төлеңіз.", 94 | "enableUsageBasedTitle": "Пайдалану Негізіндегі Бағаны Қосу", 95 | "enableUsageBasedDetail": "Премиум модельдерді пайдалануды жалғастыру үшін пайдалану негізіндегі бағаны қосыңыз.", 96 | "viewSettingsTitle": "Параметрлерді Көру", 97 | "viewSettingsDetail": "Пайдалану шектеулерін басқару үшін Параметрлерді Көру түймесін басыңыз.", 98 | "manageLimitTitle": "Шектеуді Басқару", 99 | "manageLimitDetail": "Пайдалану негізіндегі баға параметрлерін реттеу үшін Шектеуді Басқару түймесін басыңыз.", 100 | "nextNotificationAt": "Келесі шығын хабарландыруы {amount}-да.", 101 | "currentTotalCost": "Ағымдағы жалпы пайдалану құны {amount}.", 102 | "payInvoiceToContinue": "Пайдалану негізіндегі бағаны пайдалануды жалғастыру үшін шотты төлеңіз.", 103 | "openBillingPage": "Шот Бетін Ашу", 104 | "dismiss": "Жабу", 105 | "unknownModelsDetected": "Жаңа немесе өңделмеген Cursor модель терминдері анықталды: \"{models}\". Егер бұлар жаңа модельдер сияқты болса, есеп жасап GitHub-қа жіберіңіз.", 106 | "usageBasedSpendingThreshold": "Пайдалану негізіндегі шығын сіздің ${limit} шектеуіңіздің {percentage}%-ына жетті", 107 | "failedToOpenSettings": "Cursor Stats параметрлерін ашу сәтсіз. VS Code параметрлерін қолмен ашып көріңіз." 108 | }, 109 | "commands": { 110 | "refreshStats": "Cursor Stats: Статистиканы Жаңарту", 111 | "openSettings": "Cursor Stats: Параметрлерді Ашу", 112 | "setLimit": "Cursor Stats: Пайдалану Негізіндегі Баға Параметрлері", 113 | "selectCurrency": "Cursor Stats: Көрсету Валютасын Таңдау", 114 | "createReport": "Cursor Stats: Диагностикалық Есеп Жасау", 115 | "enableUsageBased": "Пайдалану Негізіндегі Бағаны Қосу", 116 | "setMonthlyLimit": "Ай Сайынғы Шектеу Орнату", 117 | "disableUsageBased": "Пайдалану Негізіндегі Бағаны Өшіру", 118 | "selectLanguage": "Cursor Stats: Тіл Таңдау", 119 | "languageChanged": "Тіл {language} деп өзгертілді. Интерфейс автоматты түрде жаңартылады.", 120 | "openGitHubIssues": "GitHub Мәселелерін Ашу", 121 | "createReportProgress": "Cursor Stats Есебін Жасау", 122 | "gatheringData": "Деректер жинау...", 123 | "completed": "Аяқталды!", 124 | "reportCreatedSuccessfully": "Есеп сәтті жасалды!\n{fileName}", 125 | "openFile": "Файлды Ашу", 126 | "openFolder": "Қалтаны Ашу", 127 | "enableUsageBasedOption": "$(check) Пайдалану Негізіндегі Бағаны Қосу", 128 | "enableUsageBasedDescription": "Пайдалану негізіндегі бағаны қосу және шектеу орнату", 129 | "setMonthlyLimitOption": "$(pencil) Ай Сайынғы Шектеу Орнату", 130 | "setMonthlyLimitDescription": "Ай сайынғы шығын шектеуіңізді өзгерту", 131 | "disableUsageBasedOption": "$(x) Пайдалану Негізіндегі Бағаны Өшіру", 132 | "disableUsageBasedDescription": "Пайдалану негізіндегі бағаны өшіру", 133 | "enterMonthlyLimit": "Доллармен ай сайынғы шығын шектеуін енгізіңіз", 134 | "enterNewMonthlyLimit": "Доллармен жаңа ай сайынғы шығын шектеуін енгізіңіз", 135 | "validNumberRequired": "0-ден үлкен дұрыс санды енгізіңіз", 136 | "usageBasedEnabledWithLimit": "Пайдалану негізіндегі баға {limit} шектеуімен қосылды", 137 | "usageBasedAlreadyEnabled": "Пайдалану негізіндегі баға әлдеқашан қосылған", 138 | "limitUpdatedTo": "Ай сайынғы шектеу {limit} деп жаңартылды", 139 | "enableUsageBasedFirst": "Алдымен пайдалану негізіндегі бағаны қосыңыз", 140 | "usageBasedDisabled": "Пайдалану негізіндегі баға өшірілді", 141 | "usageBasedAlreadyDisabled": "Пайдалану негізіндегі баға әлдеқашан өшірілген", 142 | "failedToManageLimit": "Пайдалану шектеуін басқару сәтсіз: {error}", 143 | "currentStatus": "Ағымдағы күй: {status} {limit}", 144 | "selectCurrencyPrompt": "Көрсету үшін валюта таңдаңыз", 145 | "currentLanguagePrompt": "Ағымдағы: {language}. Cursor Stats интерфейсі үшін тіл таңдаңыз", 146 | "selectLanguagePrompt": "Тіл Таңдау / Select Language / 选择语言" 147 | }, 148 | "settings": { 149 | "enableUsageBasedPricing": "Пайдалану Негізіндегі Бағаны Қосу", 150 | "changeMonthlyLimit": "Ай Сайынғы Шектеуді Өзгерту", 151 | "disableUsageBasedPricing": "Пайдалану Негізіндегі Бағаны Өшіру", 152 | "enableUsageBasedDescription": "Пайдалану негізіндегі бағаны қосу және шектеу орнату", 153 | "setLimitDescription": "Ай сайынғы шығын шектеуіңізді өзгерту", 154 | "disableUsageBasedDescription": "Пайдалану негізіндегі бағаны өшіру", 155 | "currentLimit": "Ағымдағы шектеу: ${limit}", 156 | "enterNewLimit": "Жаңа ай сайынғы шектеуді енгізіңіз (АҚШ долларымен)", 157 | "invalidLimit": "0-ден үлкен дұрыс санды енгізіңіз", 158 | "limitUpdated": "Пайдалану шектеуі ${limit} деп сәтті жаңартылды", 159 | "signInRequired": "Алдымен Cursor-ға кіріңіз", 160 | "updateFailed": "Пайдалану шектеуін жаңарту сәтсіз" 161 | }, 162 | "errors": { 163 | "tokenNotFound": "Сессия токені табылмады. Cursor-ға кіріңіз.", 164 | "apiError": "API сұрауы сәтсіз", 165 | "databaseError": "Мәліметтер қорына қол жету қатесі", 166 | "networkError": "Желі қосылу қатесі", 167 | "updateFailed": "Статистиканы жаңарту сәтсіз", 168 | "unknownError": "Белгісіз қате орын алды", 169 | "failedToCreateReport": "Есеп жасау сәтсіз. Егжей-тегжейлі ақпарат үшін логтарды тексеріңіз.", 170 | "errorCreatingReport": "Есеп жасау қатесі: {error}" 171 | }, 172 | "time": { 173 | "day": "күн", 174 | "days": "күн", 175 | "hour": "сағат", 176 | "hours": "сағат", 177 | "minute": "минут", 178 | "minutes": "минут", 179 | "second": "секунд", 180 | "seconds": "секунд", 181 | "ago": "бұрын", 182 | "refreshing": "Жаңарту...", 183 | "lastUpdated": "Соңғы Жаңарту" 184 | }, 185 | "currency": { 186 | "usd": "АҚШ Доллары", 187 | "eur": "Евро", 188 | "gbp": "Британ Фунты", 189 | "jpy": "Жапон Иенасы", 190 | "aud": "Австралия Доллары", 191 | "cad": "Канада Доллары", 192 | "chf": "Швейцария Франкі", 193 | "cny": "Қытай Юаны", 194 | "inr": "Үнді Рупиясы", 195 | "mxn": "Мексика Песосы", 196 | "brl": "Бразилия Реалы", 197 | "rub": "Ресей Рублі", 198 | "krw": "Оңтүстік Корея Воны", 199 | "sgd": "Сингапур Доллары", 200 | "nzd": "Жаңа Зеландия Доллары", 201 | "try": "Түрік Лирасы", 202 | "zar": "Оңтүстік Африка Рэнді", 203 | "sek": "Швед Кронасы", 204 | "nok": "Норвегия Кронасы", 205 | "dkk": "Дания Кронасы", 206 | "hkd": "Гонконг Доллары", 207 | "twd": "Тайвань Доллары", 208 | "php": "Филиппин Песосы", 209 | "thb": "Тай Баты", 210 | "idr": "Индонезия Рупиясы", 211 | "vnd": "Вьетнам Донгы", 212 | "ils": "Израиль Шекелі", 213 | "aed": "БАӘ Дирхамы", 214 | "sar": "Сауд Риялы", 215 | "myr": "Малайзия Ринггиті", 216 | "pln": "Польша Злотысы", 217 | "czk": "Чех Кронасы", 218 | "huf": "Венгрия Форинті", 219 | "ron": "Румыния Лейі", 220 | "bgn": "Болгария Леві", 221 | "hrk": "Хорватия Кунасы", 222 | "egp": "Мысыр Фунты", 223 | "qar": "Катар Риялы", 224 | "kwd": "Кувейт Динары", 225 | "mad": "Марокко Дирхамы" 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": { 3 | "premiumFastRequests": "프리미엄 빠른 요청", 4 | "usageBasedPricing": "사용량 기반 가격", 5 | "teamUsage": "팀 사용량", 6 | "period": "기간", 7 | "utilized": "사용됨", 8 | "used": "사용", 9 | "remaining": "남음", 10 | "limit": "제한", 11 | "spent": "지출", 12 | "of": "의", 13 | "perDay": "일일", 14 | "dailyRemaining": "일일 남은 량", 15 | "weekdaysOnly": "주중만", 16 | "today": "오늘", 17 | "isWeekend": "주말임", 18 | "cursorUsageStats": "Cursor 사용 통계", 19 | "errorState": "오류 상태", 20 | "enabled": "활성화됨", 21 | "disabled": "비활성화됨", 22 | "noUsageRecorded": "이 기간에 사용 기록이 없습니다", 23 | "usageBasedDisabled": "사용량 기반 가격이 현재 비활성화되어 있습니다", 24 | "errorCheckingStatus": "사용량 기반 가격 상태 확인 중 오류", 25 | "unableToCheckStatus": "사용량 기반 가격 상태를 확인할 수 없습니다", 26 | "unpaidAmount": "미납액: {amount}", 27 | "youHavePaid": "이 비용 중 {amount}를 이미 지불하셨습니다", 28 | "accountSettings": "계정 설정", 29 | "currency": "통화", 30 | "extensionSettings": "확장 설정", 31 | "refresh": "새로고침", 32 | "noTokenFound": "Cursor Stats: 토큰을 찾을 수 없습니다", 33 | "couldNotRetrieveToken": "⚠️ 데이터베이스에서 Cursor 토큰을 검색할 수 없습니다", 34 | "requestsUsed": "요청 사용됨", 35 | "fastRequestsPeriod": "빠른 요청 기간", 36 | "usageBasedPeriod": "사용량 기반 기간", 37 | "currentUsage": "현재 사용량", 38 | "total": "합계", 39 | "unpaid": "미납", 40 | "discounted": "할인됨", 41 | "unknownModel": "알 수 없는 모델", 42 | "unknownItem": "알 수 없는 항목", 43 | "totalCost": "총 비용", 44 | "noUsageDataAvailable": "사용 데이터 없음", 45 | "usage": "사용량", 46 | "weekday": "평일", 47 | "weekdays": "평일", 48 | "month": "월", 49 | "apiUnavailable": "Cursor API를 사용할 수 없습니다 (다시 시도까지 {countdown})", 50 | "months": { 51 | "january": "1월", 52 | "february": "2월", 53 | "march": "3월", 54 | "april": "4월", 55 | "may": "5월", 56 | "june": "6월", 57 | "july": "7월", 58 | "august": "8월", 59 | "september": "9월", 60 | "october": "10월", 61 | "november": "11월", 62 | "december": "12월" 63 | } 64 | }, 65 | "notifications": { 66 | "usageThresholdReached": "프리미엄 요청 사용량이 {percentage}%에 도달했습니다", 67 | "usageExceededLimit": "프리미엄 요청 사용량이 제한을 초과했습니다 ({percentage}%)", 68 | "spendingThresholdReached": "Cursor 사용 비용이 {amount}에 도달했습니다", 69 | "unpaidInvoice": "⚠️ 미납된 월중 청구서가 있습니다. 사용량 기반 가격 책정을 계속 사용하려면 결제해 주세요.", 70 | "enableUsageBasedTitle": "사용량 기반 가격 활성화", 71 | "enableUsageBasedDetail": "프리미엄 모델을 계속 사용하려면 사용량 기반 가격을 활성화하세요.", 72 | "viewSettingsTitle": "설정 보기", 73 | "viewSettingsDetail": "사용 제한을 관리하려면 설정 보기를 클릭하세요.", 74 | "manageLimitTitle": "제한 관리", 75 | "manageLimitDetail": "사용량 기반 가격 설정을 조정하려면 제한 관리를 클릭하세요.", 76 | "nextNotificationAt": "다음 지출 알림은 {amount}입니다.", 77 | "currentTotalCost": "현재 총 사용 비용은 {amount}입니다.", 78 | "payInvoiceToContinue": "사용량 기반 가격을 계속 사용하려면 청구서를 결제해 주세요.", 79 | "openBillingPage": "결제 페이지 열기", 80 | "dismiss": "닫기", 81 | "unknownModelsDetected": "새롭거나 처리되지 않은 Cursor 모델 용어가 감지되었습니다: \"{models}\". 새로운 모델인 것 같으면 리포트를 생성하여 GitHub에 제출해 주세요", 82 | "usageBasedSpendingThreshold": "사용량 기반 지출이 ${limit} 한도의 {percentage}%에 도달했습니다", 83 | "failedToOpenSettings": "Cursor Stats 설정을 열지 못했습니다. VS Code 설정을 수동으로 열어보세요" 84 | }, 85 | "commands": { 86 | "refreshStats": "Cursor Stats: 통계 새로고침", 87 | "openSettings": "Cursor Stats: 설정 열기", 88 | "setLimit": "Cursor Stats: 사용량 기반 가격 설정", 89 | "selectCurrency": "Cursor Stats: 표시 통화 선택", 90 | "createReport": "Cursor Stats: 진단 보고서 생성", 91 | "enableUsageBased": "사용량 기반 가격 활성화", 92 | "setMonthlyLimit": "월간 제한 설정", 93 | "disableUsageBased": "사용량 기반 가격 비활성화", 94 | "selectLanguage": "Cursor Stats: 언어 선택", 95 | "languageChanged": "언어가 {language}로 변경되었습니다. 인터페이스가 자동으로 업데이트됩니다", 96 | "openGitHubIssues": "Cursor Stats: GitHub 이슈 열기", 97 | "createReportProgress": "Cursor Stats 보고서 생성 중", 98 | "gatheringData": "데이터 수집 중...", 99 | "completed": "완료!", 100 | "reportCreatedSuccessfully": "보고서가 성공적으로 생성되었습니다!\n{fileName}", 101 | "openFile": "파일 열기", 102 | "openFolder": "폴더 열기", 103 | "enableUsageBasedOption": "$(check) 사용량 기반 가격 활성화", 104 | "enableUsageBasedDescription": "사용량 기반 가격을 활성화하고 제한을 설정합니다", 105 | "setMonthlyLimitOption": "$(pencil) 월간 제한 설정", 106 | "setMonthlyLimitDescription": "월간 지출 제한을 변경합니다", 107 | "disableUsageBasedOption": "$(x) 사용량 기반 가격 비활성화", 108 | "disableUsageBasedDescription": "사용량 기반 가격을 비활성화합니다", 109 | "enterMonthlyLimit": "월간 지출 제한을 달러로 입력하세요", 110 | "enterNewMonthlyLimit": "새로운 월간 지출 제한을 달러로 입력하세요", 111 | "validNumberRequired": "0보다 큰 유효한 숫자를 입력해 주세요", 112 | "usageBasedEnabledWithLimit": "사용량 기반 가격이 {limit} 제한으로 활성화되었습니다", 113 | "usageBasedAlreadyEnabled": "사용량 기반 가격이 이미 활성화되어 있습니다", 114 | "limitUpdatedTo": "월간 제한이 {limit}로 업데이트되었습니다", 115 | "enableUsageBasedFirst": "먼저 사용량 기반 가격을 활성화해 주세요", 116 | "usageBasedDisabled": "사용량 기반 가격이 비활성화되었습니다", 117 | "usageBasedAlreadyDisabled": "사용량 기반 가격이 이미 비활성화되어 있습니다", 118 | "failedToManageLimit": "사용 제한 관리 실패: {error}", 119 | "currentStatus": "현재 상태: {status} {limit}", 120 | "selectCurrencyPrompt": "표시할 통화를 선택하세요", 121 | "currentLanguagePrompt": "현재: {language}. Cursor Stats 인터페이스 언어를 선택하세요", 122 | "selectLanguagePrompt": "언어 선택 / Select Language / 选择语言" 123 | }, 124 | "settings": { 125 | "enableUsageBasedPricing": "사용량 기반 가격 활성화", 126 | "changeMonthlyLimit": "월간 제한 변경", 127 | "disableUsageBasedPricing": "사용량 기반 가격 비활성화", 128 | "enableUsageBasedDescription": "사용량 기반 가격을 활성화하고 제한을 설정합니다", 129 | "setLimitDescription": "월간 지출 제한을 변경합니다", 130 | "disableUsageBasedDescription": "사용량 기반 가격을 비활성화합니다", 131 | "currentLimit": "현재 제한: ${limit}", 132 | "enterNewLimit": "새로운 월간 제한을 입력하세요 (USD)", 133 | "invalidLimit": "0보다 큰 유효한 숫자를 입력해 주세요", 134 | "limitUpdated": "사용 제한이 ${limit}로 성공적으로 업데이트되었습니다", 135 | "signInRequired": "먼저 Cursor에 로그인해 주세요", 136 | "updateFailed": "사용 제한 업데이트 실패" 137 | }, 138 | "errors": { 139 | "tokenNotFound": "세션 토큰을 찾을 수 없습니다. Cursor에 로그인해 주세요.", 140 | "apiError": "API 요청 실패", 141 | "databaseError": "데이터베이스 액세스 오류", 142 | "networkError": "네트워크 연결 오류", 143 | "updateFailed": "통계 업데이트 실패", 144 | "unknownError": "알 수 없는 오류가 발생했습니다", 145 | "failedToCreateReport": "보고서 생성 실패. 자세한 내용은 로그를 확인하세요", 146 | "errorCreatingReport": "보고서 생성 오류: {error}" 147 | }, 148 | "time": { 149 | "day": "일", 150 | "days": "일", 151 | "hour": "시간", 152 | "hours": "시간", 153 | "minute": "분", 154 | "minutes": "분", 155 | "second": "초", 156 | "seconds": "초", 157 | "ago": "전", 158 | "refreshing": "새로고침 중...", 159 | "lastUpdated": "마지막 업데이트" 160 | }, 161 | "currency": { 162 | "usd": "미국 달러", 163 | "eur": "유로", 164 | "gbp": "영국 파운드", 165 | "jpy": "일본 엔", 166 | "aud": "호주 달러", 167 | "cad": "캐나다 달러", 168 | "chf": "스위스 프랑", 169 | "cny": "중국 위안", 170 | "inr": "인도 루피", 171 | "mxn": "멕시코 페소", 172 | "brl": "브라질 헤알", 173 | "rub": "러시아 루블", 174 | "krw": "대한민국 원", 175 | "sgd": "싱가포르 달러", 176 | "nzd": "뉴질랜드 달러", 177 | "try": "터키 리라", 178 | "zar": "남아프리카 랜드", 179 | "sek": "스웨덴 크로나", 180 | "nok": "노르웨이 크로네", 181 | "dkk": "덴마크 크로네", 182 | "hkd": "홍콩 달러", 183 | "twd": "대만 달러", 184 | "php": "필리핀 페소", 185 | "thb": "태국 바트", 186 | "idr": "인도네시아 루피아", 187 | "vnd": "베트남 동", 188 | "ils": "이스라엘 셰켈", 189 | "aed": "아랍에미리트 디르함", 190 | "sar": "사우디아라비아 리얄", 191 | "myr": "말레이시아 링깃", 192 | "pln": "폴란드 즈워티", 193 | "czk": "체코 코루나", 194 | "huf": "헝가리 포린트", 195 | "ron": "루마니아 레우", 196 | "bgn": "불가리아 레프", 197 | "hrk": "크로아티아 쿠나", 198 | "egp": "이집트 파운드", 199 | "qar": "카타르 리얄", 200 | "kwd": "쿠웨이트 디나르", 201 | "mad": "모로코 디르함" 202 | }, 203 | "progressBar": { 204 | "errorParsingDates": "날짜 구문 분석 오류", 205 | "dailyRemainingLimitReached": "📊 일일 남은: 0 요청/일 (제한 도달)", 206 | "dailyRemainingWeekend": "📊 일일 남은: 주말 - 계산은 월요일에 재개됩니다", 207 | "dailyRemainingPeriodEnding": "📊 일일 남은: 기간이 거의 종료됩니다", 208 | "dailyRemainingCalculation": "📊 일일 남은: {requestsPerDay} 요청/{dayType}\n ({remainingRequests} 요청 ÷ {remainingDays} {dayTypePlural})" 209 | }, 210 | "api": { 211 | "midMonthPayment": "월중 결제", 212 | "toolCalls": "툴 호출", 213 | "fastPremium": "빠른 프리미엄", 214 | "requestUnit": "req" 215 | }, 216 | "github": { 217 | "preRelease": "프리 릴리스", 218 | "stableRelease": "정식 릴리스", 219 | "latest": "최신", 220 | "updateAvailable": "{releaseType} {releaseName} 사용 가능! 현재 버전은 {currentVersion}입니다", 221 | "changesTitle": "{releaseType} {version} 변경사항", 222 | "sourceCodeZip": "소스 코드 (zip)", 223 | "sourceCodeTarGz": "소스 코드 (tar.gz)", 224 | "viewFullRelease": "GitHub에서 전체 릴리스 보기", 225 | "installedMessage": "Cursor Stats {version}이(가) 설치되었습니다" 226 | } 227 | } -------------------------------------------------------------------------------- /src/locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": { 3 | "premiumFastRequests": "Премиум Быстрые Запросы", 4 | "usageBasedPricing": "Ценообразование по Использованию", 5 | "teamUsage": "Использование Команды", 6 | "period": "Период", 7 | "utilized": "использовано", 8 | "used": "использовано", 9 | "remaining": "осталось", 10 | "limit": "лимит", 11 | "spent": "потрачено", 12 | "of": "из", 13 | "perDay": "в день", 14 | "dailyRemaining": "остается в день", 15 | "weekdaysOnly": "только будни", 16 | "today": "сегодня", 17 | "isWeekend": "выходной", 18 | "cursorUsageStats": "Статистика Использования Cursor", 19 | "errorState": "Состояние Ошибки", 20 | "enabled": "Включено", 21 | "disabled": "Отключено", 22 | "noUsageRecorded": "Использование не зафиксировано за этот период", 23 | "usageBasedDisabled": "Ценообразование по использованию в настоящее время отключено", 24 | "errorCheckingStatus": "Ошибка при проверке статуса ценообразования по использованию", 25 | "unableToCheckStatus": "Невозможно проверить статус ценообразования по использованию", 26 | "unpaidAmount": "Неоплачено: {amount}", 27 | "youHavePaid": "ℹ️ Вы уже оплатили **{amount}** из этой суммы", 28 | "accountSettings": "Настройки Аккаунта", 29 | "currency": "Валюта", 30 | "extensionSettings": "Настройки Расширения", 31 | "refresh": "Обновить", 32 | "noTokenFound": "Статистика Cursor: Токен не найден", 33 | "couldNotRetrieveToken": "⚠️ Не удалось получить токен Cursor из базы данных", 34 | "requestsUsed": "запросов использовано", 35 | "fastRequestsPeriod": "Период Быстрых Запросов", 36 | "usageBasedPeriod": "Период Ценообразования по Использованию", 37 | "currentUsage": "Текущее Использование", 38 | "total": "Всего", 39 | "unpaid": "Неоплачено", 40 | "discounted": "со скидкой", 41 | "unknownModel": "неизвестная-модель", 42 | "unknownItem": "Неизвестный Элемент", 43 | "totalCost": "Общая Стоимость", 44 | "noUsageDataAvailable": "Данные об использовании недоступны", 45 | "usage": "Использование", 46 | "weekday": "будний день", 47 | "weekdays": "будни", 48 | "month": "Месяц", 49 | "apiUnavailable": "API Cursor Недоступен (Повтор через {countdown})", 50 | "months": { 51 | "january": "Январь", 52 | "february": "Февраль", 53 | "march": "Март", 54 | "april": "Апрель", 55 | "may": "Май", 56 | "june": "Июнь", 57 | "july": "Июль", 58 | "august": "Август", 59 | "september": "Сентябрь", 60 | "october": "Октябрь", 61 | "november": "Ноябрь", 62 | "december": "Декабрь" 63 | } 64 | }, 65 | "progressBar": { 66 | "errorParsingDates": "Ошибка при разборе дат", 67 | "dailyRemainingLimitReached": "📊 Дневной Остаток: 0 запросов/день (лимит достигнут)", 68 | "dailyRemainingWeekend": "📊 Дневной Остаток: Выходные - расчеты возобновятся в понедельник", 69 | "dailyRemainingPeriodEnding": "📊 Дневной Остаток: Период скоро закончится", 70 | "dailyRemainingCalculation": "📊 Дневной Остаток: {requestsPerDay} запросов/{dayType}\n ({remainingRequests} запросов ÷ {remainingDays} {dayTypePlural})" 71 | }, 72 | "api": { 73 | "midMonthPayment": "Платеж в середине месяца", 74 | "toolCalls": "вызовы инструментов", 75 | "fastPremium": "быстрый премиум", 76 | "requestUnit": "зап" 77 | }, 78 | "github": { 79 | "preRelease": "Предварительный релиз", 80 | "stableRelease": "Стабильный релиз", 81 | "latest": "Последний", 82 | "updateAvailable": "{releaseType} {releaseName} доступен! У вас версия {currentVersion}", 83 | "changesTitle": "Изменения {releaseType} {version}", 84 | "sourceCodeZip": "Исходный код (zip)", 85 | "sourceCodeTarGz": "Исходный код (tar.gz)", 86 | "viewFullRelease": "Посмотреть полный релиз на GitHub", 87 | "installedMessage": "Cursor Stats {version} установлен" 88 | }, 89 | "notifications": { 90 | "usageThresholdReached": "Использование премиум запросов достигло {percentage}%", 91 | "usageExceededLimit": "Использование премиум запросов превысило лимит ({percentage}%)", 92 | "spendingThresholdReached": "Ваши расходы на использование Cursor достигли {amount}", 93 | "unpaidInvoice": "⚠️ У вас есть неоплаченный счет в середине месяца. Пожалуйста, оплатите его, чтобы продолжить использование ценообразования по использованию.", 94 | "enableUsageBasedTitle": "Включить Ценообразование по Использованию", 95 | "enableUsageBasedDetail": "Включите ценообразование по использованию для продолжения использования премиум моделей.", 96 | "viewSettingsTitle": "Посмотреть Настройки", 97 | "viewSettingsDetail": "Нажмите Посмотреть Настройки для управления лимитами использования.", 98 | "manageLimitTitle": "Управлять Лимитом", 99 | "manageLimitDetail": "Нажмите Управлять Лимитом для настройки параметров ценообразования по использованию.", 100 | "nextNotificationAt": "Следующее уведомление о расходах при {amount}.", 101 | "currentTotalCost": "Текущая общая стоимость использования составляет {amount}.", 102 | "payInvoiceToContinue": "Пожалуйста, оплатите счет для продолжения использования ценообразования по использованию.", 103 | "openBillingPage": "Открыть Страницу Счетов", 104 | "dismiss": "Отклонить", 105 | "unknownModelsDetected": "Обнаружены новые или необработанные термины моделей Cursor: \"{models}\". Если это похоже на новые модели, пожалуйста, создайте отчет и отправьте его на GitHub.", 106 | "usageBasedSpendingThreshold": "Расходы по использованию достигли {percentage}% от вашего лимита ${limit}", 107 | "failedToOpenSettings": "Не удалось открыть настройки Cursor Stats. Пожалуйста, попробуйте открыть настройки VS Code вручную." 108 | }, 109 | "commands": { 110 | "refreshStats": "Cursor Stats: Обновить Статистику", 111 | "openSettings": "Cursor Stats: Открыть Настройки", 112 | "setLimit": "Cursor Stats: Настройки Ценообразования по Использованию", 113 | "selectCurrency": "Cursor Stats: Выбрать Валюту Отображения", 114 | "createReport": "Cursor Stats: Создать Диагностический Отчет", 115 | "enableUsageBased": "Включить Ценообразование по Использованию", 116 | "setMonthlyLimit": "Установить Месячный Лимит", 117 | "disableUsageBased": "Отключить Ценообразование по Использованию", 118 | "selectLanguage": "Cursor Stats: Выбрать Язык", 119 | "languageChanged": "Язык изменен на {language}. Интерфейс обновится автоматически.", 120 | "openGitHubIssues": "Открыть Проблемы GitHub", 121 | "createReportProgress": "Создание Отчета Cursor Stats", 122 | "gatheringData": "Сбор данных...", 123 | "completed": "Завершено!", 124 | "reportCreatedSuccessfully": "Отчет создан успешно!\n{fileName}", 125 | "openFile": "Открыть Файл", 126 | "openFolder": "Открыть Папку", 127 | "enableUsageBasedOption": "$(check) Включить Ценообразование по Использованию", 128 | "enableUsageBasedDescription": "Включить ценообразование по использованию и установить лимит", 129 | "setMonthlyLimitOption": "$(pencil) Установить Месячный Лимит", 130 | "setMonthlyLimitDescription": "Изменить ваш месячный лимит расходов", 131 | "disableUsageBasedOption": "$(x) Отключить Ценообразование по Использованию", 132 | "disableUsageBasedDescription": "Отключить ценообразование по использованию", 133 | "enterMonthlyLimit": "Введите месячный лимит расходов в долларах", 134 | "enterNewMonthlyLimit": "Введите новый месячный лимит расходов в долларах", 135 | "validNumberRequired": "Пожалуйста, введите действительное число больше 0", 136 | "usageBasedEnabledWithLimit": "Ценообразование по использованию включено с лимитом {limit}", 137 | "usageBasedAlreadyEnabled": "Ценообразование по использованию уже включено", 138 | "limitUpdatedTo": "Месячный лимит обновлен до {limit}", 139 | "enableUsageBasedFirst": "Пожалуйста, сначала включите ценообразование по использованию", 140 | "usageBasedDisabled": "Ценообразование по использованию отключено", 141 | "usageBasedAlreadyDisabled": "Ценообразование по использованию уже отключено", 142 | "failedToManageLimit": "Не удалось управлять лимитом использования: {error}", 143 | "currentStatus": "Текущий статус: {status} {limit}", 144 | "selectCurrencyPrompt": "Выберите валюту для отображения", 145 | "currentLanguagePrompt": "Текущий: {language}. Выберите язык для интерфейса Cursor Stats", 146 | "selectLanguagePrompt": "Выбрать Язык / Select Language / 언어 선택" 147 | }, 148 | "settings": { 149 | "enableUsageBasedPricing": "Включить Ценообразование по Использованию", 150 | "changeMonthlyLimit": "Изменить Месячный Лимит", 151 | "disableUsageBasedPricing": "Отключить Ценообразование по Использованию", 152 | "enableUsageBasedDescription": "Включить ценообразование по использованию и установить лимит", 153 | "setLimitDescription": "Изменить ваш месячный лимит расходов", 154 | "disableUsageBasedDescription": "Отключить ценообразование по использованию", 155 | "currentLimit": "Текущий лимит: ${limit}", 156 | "enterNewLimit": "Введите новый месячный лимит (в долларах США)", 157 | "invalidLimit": "Пожалуйста, введите действительное число больше 0", 158 | "limitUpdated": "Лимит использования успешно обновлен до ${limit}", 159 | "signInRequired": "Пожалуйста, сначала войдите в Cursor", 160 | "updateFailed": "Не удалось обновить лимит использования" 161 | }, 162 | "errors": { 163 | "tokenNotFound": "Токен сессии не найден. Пожалуйста, войдите в Cursor.", 164 | "apiError": "Ошибка API запроса", 165 | "databaseError": "Ошибка доступа к базе данных", 166 | "networkError": "Ошибка сетевого подключения", 167 | "updateFailed": "Не удалось обновить статистику", 168 | "unknownError": "Произошла неизвестная ошибка", 169 | "failedToCreateReport": "Не удалось создать отчет. Проверьте логи для получения подробностей.", 170 | "errorCreatingReport": "Ошибка при создании отчета: {error}" 171 | }, 172 | "time": { 173 | "day": "день", 174 | "days": "дней", 175 | "hour": "час", 176 | "hours": "часов", 177 | "minute": "минута", 178 | "minutes": "минут", 179 | "second": "секунда", 180 | "seconds": "секунд", 181 | "ago": "назад", 182 | "refreshing": "Обновление...", 183 | "lastUpdated": "Последнее Обновление" 184 | }, 185 | "currency": { 186 | "usd": "Доллар США", 187 | "eur": "Евро", 188 | "gbp": "Британский Фунт", 189 | "jpy": "Японская Йена", 190 | "aud": "Австралийский Доллар", 191 | "cad": "Канадский Доллар", 192 | "chf": "Швейцарский Франк", 193 | "cny": "Китайский Юань", 194 | "inr": "Индийская Рупия", 195 | "mxn": "Мексиканское Песо", 196 | "brl": "Бразильский Реал", 197 | "rub": "Российский Рубль", 198 | "krw": "Южнокорейская Вона", 199 | "sgd": "Сингапурский Доллар", 200 | "nzd": "Новозеландский Доллар", 201 | "try": "Турецкая Лира", 202 | "zar": "Южноафриканский Рэнд", 203 | "sek": "Шведская Крона", 204 | "nok": "Норвежская Крона", 205 | "dkk": "Датская Крона", 206 | "hkd": "Гонконгский Доллар", 207 | "twd": "Тайваньский Доллар", 208 | "php": "Филиппинское Песо", 209 | "thb": "Тайский Бат", 210 | "idr": "Индонезийская Рупия", 211 | "vnd": "Вьетнамский Донг", 212 | "ils": "Израильский Шекель", 213 | "aed": "Дирхам ОАЭ", 214 | "sar": "Саудовский Риял", 215 | "myr": "Малайзийский Ринггит", 216 | "pln": "Польский Злотый", 217 | "czk": "Чешская Крона", 218 | "huf": "Венгерский Форинт", 219 | "ron": "Румынский Лей", 220 | "bgn": "Болгарский Лев", 221 | "hrk": "Хорватская Куна", 222 | "egp": "Египетский Фунт", 223 | "qar": "Катарский Риял", 224 | "kwd": "Кувейтский Динар", 225 | "mad": "Марокканский Дирхам" 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": { 3 | "premiumFastRequests": "高级快速请求", 4 | "usageBasedPricing": "使用量计费", 5 | "teamUsage": "团队使用情况", 6 | "period": "周期", 7 | "utilized": "已使用", 8 | "used": "已用", 9 | "remaining": "剩余", 10 | "limit": "限制", 11 | "spent": "已花费", 12 | "of": "的", 13 | "perDay": "每日", 14 | "dailyRemaining": "每日剩余", 15 | "weekdaysOnly": "仅工作日", 16 | "today": "今天", 17 | "isWeekend": "是周末", 18 | "cursorUsageStats": "Cursor 使用统计", 19 | "errorState": "错误状态", 20 | "enabled": "已启用", 21 | "disabled": "已禁用", 22 | "noUsageRecorded": "本周期内无使用记录", 23 | "usageBasedDisabled": "使用量计费当前已禁用", 24 | "errorCheckingStatus": "检查使用量计费状态时出错", 25 | "unableToCheckStatus": "无法检查使用量计费状态", 26 | "unpaidAmount": "未付:{amount}", 27 | "youHavePaid": "您已支付此费用的 {amount}", 28 | "accountSettings": "账户设置", 29 | "currency": "货币", 30 | "extensionSettings": "扩展设置", 31 | "refresh": "刷新", 32 | "noTokenFound": "Cursor Stats: 未找到令牌", 33 | "couldNotRetrieveToken": "⚠️ 无法从数据库检索 Cursor 令牌", 34 | "requestsUsed": "已使用请求", 35 | "fastRequestsPeriod": "快速请求周期", 36 | "usageBasedPeriod": "使用量计费周期", 37 | "currentUsage": "当前使用量", 38 | "total": "总计", 39 | "unpaid": "未付", 40 | "discounted": "已折扣", 41 | "unknownModel": "未知模型", 42 | "unknownItem": "未知项", 43 | "totalCost": "总费用", 44 | "noUsageDataAvailable": "无可用使用数据", 45 | "usage": "使用量", 46 | "weekday": "工作日", 47 | "weekdays": "工作日", 48 | "month": "月", 49 | "apiUnavailable": "Cursor API 不可用(重试倒计时 {countdown})", 50 | "months": { 51 | "january": "1月", 52 | "february": "2月", 53 | "march": "3月", 54 | "april": "4月", 55 | "may": "5月", 56 | "june": "6月", 57 | "july": "7月", 58 | "august": "8月", 59 | "september": "9月", 60 | "october": "10月", 61 | "november": "11月", 62 | "december": "12月" 63 | } 64 | }, 65 | "notifications": { 66 | "usageThresholdReached": "高级请求使用量已达到 {percentage}%", 67 | "usageExceededLimit": "高级请求使用量已超出限制 ({percentage}%)", 68 | "spendingThresholdReached": "您的 Cursor 使用费用已达到 {amount}", 69 | "unpaidInvoice": "⚠️ 您有未付的月中账单,请支付以继续使用基于使用量的定价。", 70 | "enableUsageBasedTitle": "启用使用量计费", 71 | "enableUsageBasedDetail": "启用使用量计费以继续使用高级模型。", 72 | "viewSettingsTitle": "查看设置", 73 | "viewSettingsDetail": "点击查看设置来管理您的使用限制。", 74 | "manageLimitTitle": "管理限制", 75 | "manageLimitDetail": "点击管理限制来调整您的使用量计费设置。", 76 | "nextNotificationAt": "下次费用通知在 {amount}。", 77 | "currentTotalCost": "当前总使用成本为 {amount}。", 78 | "payInvoiceToContinue": "请支付您的账单以继续使用基于使用量的定价。", 79 | "openBillingPage": "打开账单页面", 80 | "dismiss": "关闭", 81 | "unknownModelsDetected": "检测到新的或未处理的 Cursor 模型术语:\"{models}\"。如果这些是新模型,请创建报告并提交到 GitHub。", 82 | "usageBasedSpendingThreshold": "基于使用量的支出已达到您的 ${limit} 限额的 {percentage}%", 83 | "failedToOpenSettings": "无法打开 Cursor Stats 设置。请尝试手动打开 VS Code 设置。" 84 | }, 85 | "commands": { 86 | "refreshStats": "Cursor Stats: 刷新统计", 87 | "openSettings": "Cursor Stats: 打开设置", 88 | "setLimit": "Cursor Stats: 使用量计费设置", 89 | "selectCurrency": "Cursor Stats: 选择显示货币", 90 | "createReport": "Cursor Stats: 生成诊断报告", 91 | "enableUsageBased": "启用使用量计费", 92 | "setMonthlyLimit": "设置月度限制", 93 | "disableUsageBased": "禁用使用量计费", 94 | "selectLanguage": "Cursor Stats: 选择语言", 95 | "languageChanged": "语言已切换为{language},界面将自动更新。", 96 | "openGitHubIssues": "Cursor Stats: 打开 GitHub 问题", 97 | "createReportProgress": "正在创建 Cursor Stats 报告", 98 | "gatheringData": "正在收集数据...", 99 | "completed": "完成!", 100 | "reportCreatedSuccessfully": "报告已成功创建!\n{fileName}", 101 | "openFile": "打开文件", 102 | "openFolder": "打开文件夹", 103 | "enableUsageBasedOption": "$(check) 启用使用量计费", 104 | "enableUsageBasedDescription": "开启使用量计费并设置限制", 105 | "setMonthlyLimitOption": "$(pencil) 设置月度限制", 106 | "setMonthlyLimitDescription": "更改您的月度支出限制", 107 | "disableUsageBasedOption": "$(x) 禁用使用量计费", 108 | "disableUsageBasedDescription": "关闭使用量计费", 109 | "enterMonthlyLimit": "输入月度支出限制(美元)", 110 | "enterNewMonthlyLimit": "输入新的月度支出限制(美元)", 111 | "validNumberRequired": "请输入大于 0 的有效数字", 112 | "usageBasedEnabledWithLimit": "使用量计费已启用,限制为 {limit}", 113 | "usageBasedAlreadyEnabled": "使用量计费已经启用", 114 | "limitUpdatedTo": "月度限制已更新为 {limit}", 115 | "enableUsageBasedFirst": "请先启用使用量计费", 116 | "usageBasedDisabled": "使用量计费已禁用", 117 | "usageBasedAlreadyDisabled": "使用量计费已经禁用", 118 | "failedToManageLimit": "管理使用限制失败:{error}", 119 | "currentStatus": "当前状态:{status} {limit}", 120 | "selectCurrencyPrompt": "选择显示货币", 121 | "currentLanguagePrompt": "当前:{language}。选择 Cursor Stats 界面语言", 122 | "selectLanguagePrompt": "选择语言 / Select Language / 언어 선택" 123 | }, 124 | "settings": { 125 | "enableUsageBasedPricing": "启用使用量计费", 126 | "changeMonthlyLimit": "更改月度限制", 127 | "disableUsageBasedPricing": "禁用使用量计费", 128 | "enableUsageBasedDescription": "开启使用量计费并设置限制", 129 | "setLimitDescription": "更改您的月度支出限制", 130 | "disableUsageBasedDescription": "关闭使用量计费", 131 | "currentLimit": "当前限制:${limit}", 132 | "enterNewLimit": "输入新的月度限制(美元)", 133 | "invalidLimit": "请输入大于 0 的有效数字", 134 | "limitUpdated": "使用限制已成功更新至 ${limit}", 135 | "signInRequired": "请先登录 Cursor", 136 | "updateFailed": "更新使用限制失败" 137 | }, 138 | "errors": { 139 | "tokenNotFound": "未找到会话令牌,请登录 Cursor。", 140 | "apiError": "API 请求失败", 141 | "databaseError": "数据库访问错误", 142 | "networkError": "网络连接错误", 143 | "updateFailed": "更新统计失败", 144 | "unknownError": "发生未知错误", 145 | "failedToCreateReport": "报告创建失败。请检查日志以获取详细信息。", 146 | "errorCreatingReport": "创建报告时出错:{error}" 147 | }, 148 | "time": { 149 | "day": "天", 150 | "days": "天", 151 | "hour": "小时", 152 | "hours": "小时", 153 | "minute": "分钟", 154 | "minutes": "分钟", 155 | "second": "秒", 156 | "seconds": "秒", 157 | "ago": "前", 158 | "refreshing": "刷新中...", 159 | "lastUpdated": "最后更新" 160 | }, 161 | "currency": { 162 | "usd": "美元", 163 | "eur": "欧元", 164 | "gbp": "英镑", 165 | "jpy": "日元", 166 | "aud": "澳大利亚元", 167 | "cad": "加拿大元", 168 | "chf": "瑞士法郎", 169 | "cny": "人民币", 170 | "inr": "印度卢比", 171 | "mxn": "墨西哥比索", 172 | "brl": "巴西雷亚尔", 173 | "rub": "俄罗斯卢布", 174 | "krw": "韩元", 175 | "sgd": "新加坡元", 176 | "nzd": "新西兰元", 177 | "try": "土耳其里拉", 178 | "zar": "南非兰特", 179 | "sek": "瑞典克朗", 180 | "nok": "挪威克朗", 181 | "dkk": "丹麦克朗", 182 | "hkd": "港元", 183 | "twd": "新台币", 184 | "php": "菲律宾比索", 185 | "thb": "泰铢", 186 | "idr": "印度尼西亚卢比", 187 | "vnd": "越南盾", 188 | "ils": "以色列谢克尔", 189 | "aed": "阿联酋迪拉姆", 190 | "sar": "沙特里亚尔", 191 | "myr": "马来西亚林吉特", 192 | "pln": "波兰兹罗提", 193 | "czk": "捷克克朗", 194 | "huf": "匈牙利福林", 195 | "ron": "罗马尼亚列伊", 196 | "bgn": "保加利亚列弗", 197 | "hrk": "克罗地亚库纳", 198 | "egp": "埃及镑", 199 | "qar": "卡塔尔里亚尔", 200 | "kwd": "科威特第纳尔", 201 | "mad": "摩洛哥迪拉姆" 202 | }, 203 | "progressBar": { 204 | "errorParsingDates": "解析日期错误", 205 | "dailyRemainingLimitReached": "📊 每日剩余:0 请求/天(已达到限制)", 206 | "dailyRemainingWeekend": "📊 每日剩余:周末 - 计算将在周一恢复", 207 | "dailyRemainingPeriodEnding": "📊 每日剩余:周期即将结束", 208 | "dailyRemainingCalculation": "📊 每日剩余:{requestsPerDay} 请求/{dayType}\n ({remainingRequests} 请求 ÷ {remainingDays} {dayTypePlural})" 209 | }, 210 | "api": { 211 | "midMonthPayment": "月中支付", 212 | "toolCalls": "工具调用", 213 | "fastPremium": "快速高级", 214 | "requestUnit": "req" 215 | }, 216 | "github": { 217 | "preRelease": "预发布", 218 | "stableRelease": "稳定发布", 219 | "latest": "最新", 220 | "updateAvailable": "{releaseType} {releaseName} 可用!您当前的版本是 {currentVersion}", 221 | "changesTitle": "{releaseType} {version} 更新内容", 222 | "sourceCodeZip": "源码 (zip)", 223 | "sourceCodeTarGz": "源码 (tar.gz)", 224 | "viewFullRelease": "在 GitHub 上查看完整发布", 225 | "installedMessage": "Cursor Stats {version} 已安装" 226 | } 227 | } -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { CursorStats, UsageLimitResponse, ExtendedAxiosError, UsageItem, CursorUsageResponse } from '../interfaces/types'; 3 | import { log } from '../utils/logger'; 4 | import { checkTeamMembership, getTeamUsage, extractUserUsage } from './team'; 5 | import { getExtensionContext } from '../extension'; 6 | import { t } from '../utils/i18n'; 7 | import * as fs from 'fs'; 8 | 9 | export async function getCurrentUsageLimit(token: string): Promise { 10 | try { 11 | const response = await axios.post('https://www.cursor.com/api/dashboard/get-hard-limit', 12 | {}, // empty JSON body 13 | { 14 | headers: { 15 | Cookie: `WorkosCursorSessionToken=${token}` 16 | } 17 | } 18 | ); 19 | return response.data; 20 | } catch (error: any) { 21 | log('[API] Error fetching usage limit: ' + error.message, true); 22 | throw error; 23 | } 24 | } 25 | 26 | export async function setUsageLimit(token: string, hardLimit: number, noUsageBasedAllowed: boolean): Promise { 27 | try { 28 | await axios.post('https://www.cursor.com/api/dashboard/set-hard-limit', 29 | { 30 | hardLimit, 31 | noUsageBasedAllowed 32 | }, 33 | { 34 | headers: { 35 | Cookie: `WorkosCursorSessionToken=${token}` 36 | } 37 | } 38 | ); 39 | log(`[API] Successfully ${noUsageBasedAllowed ? 'disabled' : 'enabled'} usage-based pricing with limit: $${hardLimit}`); 40 | } catch (error: any) { 41 | log('[API] Error setting usage limit: ' + error.message, true); 42 | throw error; 43 | } 44 | } 45 | 46 | export async function checkUsageBasedStatus(token: string): Promise<{isEnabled: boolean, limit?: number}> { 47 | try { 48 | const response = await getCurrentUsageLimit(token); 49 | return { 50 | isEnabled: !response.noUsageBasedAllowed, 51 | limit: response.hardLimit 52 | }; 53 | } catch (error: any) { 54 | log(`[API] Error checking usage-based status: ${error.message}`, true); 55 | return { 56 | isEnabled: false 57 | }; 58 | } 59 | } 60 | 61 | async function fetchMonthData(token: string, month: number, year: number): Promise<{ items: UsageItem[], hasUnpaidMidMonthInvoice: boolean, midMonthPayment: number }> { 62 | log(`[API] Fetching data for ${month}/${year}`); 63 | try { 64 | // Path to local dev data file, leave empty for production 65 | const devDataPath: string = ""; 66 | 67 | let response; 68 | if (devDataPath) { 69 | try { 70 | log(`[API] Dev mode enabled, reading from: ${devDataPath}`); 71 | const rawData = fs.readFileSync(devDataPath, 'utf8'); 72 | response = { data: JSON.parse(rawData) }; 73 | log('[API] Successfully loaded dev data'); 74 | } catch (devError: any) { 75 | log('[API] Error reading dev data: ' + devError.message, true); 76 | throw devError; 77 | } 78 | } else { 79 | response = await axios.post('https://www.cursor.com/api/dashboard/get-monthly-invoice', { 80 | month, 81 | year, 82 | includeUsageEvents: false 83 | }, { 84 | headers: { 85 | Cookie: `WorkosCursorSessionToken=${token}` 86 | } 87 | }); 88 | } 89 | 90 | const usageItems: UsageItem[] = []; 91 | let midMonthPayment = 0; 92 | if (response.data.items) { 93 | // First pass: find the maximum request count and cost per request among valid items 94 | let maxRequestCount = 0; 95 | let maxCostPerRequest = 0; 96 | for (const item of response.data.items) { 97 | // Skip items without cents value or mid-month payments 98 | if (!item.hasOwnProperty('cents') || typeof item.cents === 'undefined' || item.description.includes('Mid-month usage paid')) { 99 | continue; 100 | } 101 | 102 | let currentItemRequestCount = 0; 103 | const tokenBasedMatch = item.description.match(/^(\d+) token-based usage calls to/); 104 | if (tokenBasedMatch && tokenBasedMatch[1]) { 105 | currentItemRequestCount = parseInt(tokenBasedMatch[1]); 106 | } else { 107 | const originalMatch = item.description.match(/^(\d+)/); // Match digits at the beginning 108 | if (originalMatch && originalMatch[1]) { 109 | currentItemRequestCount = parseInt(originalMatch[1]); 110 | } 111 | } 112 | 113 | if (currentItemRequestCount > 0) { 114 | maxRequestCount = Math.max(maxRequestCount, currentItemRequestCount); 115 | 116 | // Calculate cost per request for this item to find maximum 117 | const costPerRequestCents = item.cents / currentItemRequestCount; 118 | const costPerRequestDollars = costPerRequestCents / 100; 119 | maxCostPerRequest = Math.max(maxCostPerRequest, costPerRequestDollars); 120 | } 121 | } 122 | 123 | // Calculate the padding width based on the maximum request count 124 | const paddingWidth = maxRequestCount > 0 ? maxRequestCount.toString().length : 1; // Ensure paddingWidth is at least 1 125 | 126 | // Calculate the padding width for cost per request (format to 3 decimal places and find max width) 127 | // Max cost will be something like "XX.XXX" or "X.XXX", so we need to find the max length of that string. 128 | // Let's find the maximum cost in cents first to determine the number of integer digits. 129 | let maxCostCentsForPadding = 0; 130 | for (const item of response.data.items) { 131 | if (!item.hasOwnProperty('cents') || typeof item.cents === 'undefined' || item.description.includes('Mid-month usage paid')) { 132 | continue; 133 | } 134 | let currentItemRequestCount = 0; 135 | const tokenBasedMatch = item.description.match(/^(\d+) token-based usage calls to/); 136 | if (tokenBasedMatch && tokenBasedMatch[1]) { 137 | currentItemRequestCount = parseInt(tokenBasedMatch[1]); 138 | } else { 139 | const originalMatch = item.description.match(/^(\d+)/); 140 | if (originalMatch && originalMatch[1]) { 141 | currentItemRequestCount = parseInt(originalMatch[1]); 142 | } 143 | } 144 | if (currentItemRequestCount > 0) { 145 | const costPerRequestCents = item.cents / currentItemRequestCount; 146 | maxCostCentsForPadding = Math.max(maxCostCentsForPadding, costPerRequestCents); 147 | } 148 | } 149 | // Now format this max cost per request to get its string length 150 | const maxCostPerRequestForPaddingFormatted = (maxCostCentsForPadding / 100).toFixed(3); 151 | const costPaddingWidth = maxCostPerRequestForPaddingFormatted.length; 152 | 153 | for (const item of response.data.items) { 154 | 155 | // Skip items without cents value 156 | if (!item.hasOwnProperty('cents')) { 157 | log('[API] Skipping item without cents value: ' + item.description); 158 | continue; 159 | } 160 | 161 | // Check if this is a mid-month payment 162 | if (item.description.includes('Mid-month usage paid')) { 163 | // Skip if cents is undefined 164 | if (typeof item.cents === 'undefined') { 165 | continue; 166 | } 167 | // Add to the total mid-month payment amount (convert from cents to dollars) 168 | midMonthPayment += Math.abs(item.cents) / 100; 169 | log(`[API] Added mid-month payment of $${(Math.abs(item.cents) / 100).toFixed(2)}, total now: $${midMonthPayment.toFixed(2)}`); 170 | // Add a special line for mid-month payment that statusBar.ts can parse 171 | usageItems.push({ 172 | calculation: `${t('api.midMonthPayment')}: $${midMonthPayment.toFixed(2)}`, 173 | totalDollars: `-$${midMonthPayment.toFixed(2)}`, 174 | description: item.description 175 | }); 176 | continue; // Skip adding this to regular usage items 177 | } 178 | 179 | // Logic to parse different item description formats 180 | const cents = item.cents; 181 | 182 | if (typeof cents === 'undefined') { 183 | log('[API] Skipping item with undefined cents value: ' + item.description); 184 | continue; 185 | } 186 | 187 | let requestCount: number; 188 | let parsedModelName: string; // Renamed from modelInfo for clarity 189 | let isToolCall = false; 190 | 191 | const tokenBasedMatch = item.description.match(/^(\d+) token-based usage calls to ([\w.-]+), totalling: \$(?:[\d.]+)/); 192 | if (tokenBasedMatch) { 193 | requestCount = parseInt(tokenBasedMatch[1]); 194 | parsedModelName = tokenBasedMatch[2]; 195 | } else { 196 | const originalMatch = item.description.match(/^(\d+)\s+(.+?)(?: request| calls)?(?: beyond|\*| per|$)/i); 197 | if (originalMatch) { 198 | requestCount = parseInt(originalMatch[1]); 199 | const extractedDescription = originalMatch[2].trim(); 200 | 201 | // Updated pattern to handle "discounted" prefix and include claude-4-sonnet 202 | const genericModelPattern = /\b(?:discounted\s+)?(claude-(?:3-(?:opus|sonnet|haiku)|3\.[57]-sonnet(?:-[\w-]+)?(?:-max)?|4-sonnet(?:-thinking)?)|gpt-(?:4(?:\.\d+|o-128k|-preview)?|3\.5-turbo)|gemini-(?:1\.5-flash-500k|2[\.-]5-pro-(?:exp-\d{2}-\d{2}|preview-\d{2}-\d{2}|exp-max))|o[134](?:-mini)?)\b/i; 203 | const specificModelMatch = item.description.match(genericModelPattern); 204 | 205 | if (item.description.includes("tool calls")) { 206 | parsedModelName = t('api.toolCalls'); 207 | isToolCall = true; 208 | } else if (specificModelMatch) { 209 | // Extract the model name (group 1), which excludes the "discounted" prefix 210 | parsedModelName = specificModelMatch[1]; 211 | } else if (item.description.includes("extra fast premium request")) { 212 | const extraFastModelMatch = item.description.match(/extra fast premium requests? \(([^)]+)\)/i); 213 | if (extraFastModelMatch && extraFastModelMatch[1]) { 214 | parsedModelName = extraFastModelMatch[1]; // e.g., Haiku 215 | } else { 216 | parsedModelName = t('api.fastPremium'); 217 | } 218 | } else { 219 | // Fallback for unknown model structure 220 | parsedModelName = t('statusBar.unknownModel'); // Default to unknown-model 221 | log(`[API] Could not determine specific model for (original format): "${item.description}". Using "${parsedModelName}".`); 222 | } 223 | } else { 224 | log('[API] Could not extract request count or model info from: ' + item.description); 225 | parsedModelName = t('statusBar.unknownModel'); // Ensure it's set for items we can't parse fully 226 | // Try to get at least a request count if possible, even if model is unknown 227 | const fallbackCountMatch = item.description.match(/^(\d+)/); 228 | if (fallbackCountMatch) { 229 | requestCount = parseInt(fallbackCountMatch[1]); 230 | } else { 231 | continue; // Truly unparsable 232 | } 233 | } 234 | } 235 | 236 | // Skip items with 0 requests to avoid division by zero 237 | if (requestCount === 0) { 238 | log('[API] Skipping item with 0 requests: ' + item.description); 239 | continue; 240 | } 241 | 242 | const costPerRequestCents = cents / requestCount; 243 | const totalDollars = cents / 100; 244 | 245 | const paddedRequestCount = requestCount.toString().padStart(paddingWidth, '0'); 246 | const costPerRequestDollarsFormatted = (costPerRequestCents / 100).toFixed(3).padStart(costPaddingWidth, '0'); 247 | 248 | const isTotallingItem = !!tokenBasedMatch; 249 | const tilde = isTotallingItem ? "~" : "  "; 250 | const itemUnit = t('api.requestUnit'); // Always use "req" as the unit 251 | 252 | // Simplified calculation string, model name is now separate 253 | const calculationString = `**${paddedRequestCount}** ${itemUnit} @ **$${costPerRequestDollarsFormatted}${tilde}**`; 254 | 255 | usageItems.push({ 256 | calculation: calculationString, 257 | totalDollars: `$${totalDollars.toFixed(2)}`, 258 | description: item.description, 259 | modelNameForTooltip: parsedModelName, // Store the determined model name here 260 | isDiscounted: item.description.toLowerCase().includes("discounted") // Add a flag for discounted items 261 | }); 262 | } 263 | } 264 | 265 | return { 266 | items: usageItems, 267 | hasUnpaidMidMonthInvoice: response.data.hasUnpaidMidMonthInvoice, 268 | midMonthPayment 269 | }; 270 | } catch (error: any) { 271 | const axiosError = error as ExtendedAxiosError; 272 | log(`[API] Error fetching monthly data for ${month}/${year}: ${axiosError.message}`, true); 273 | log('[API] API error details: ' + JSON.stringify({ 274 | status: axiosError.response?.status, 275 | data: axiosError.response?.data, 276 | message: axiosError.message 277 | }), true); 278 | throw error; 279 | } 280 | } 281 | 282 | export async function fetchCursorStats(token: string): Promise { 283 | // Extract user ID from token 284 | const userId = token.split('%3A%3A')[0]; 285 | 286 | try { 287 | // Check if user is a team member 288 | const context = getExtensionContext(); 289 | const teamInfo = await checkTeamMembership(token, context); 290 | 291 | let premiumRequests; 292 | if (teamInfo.isTeamMember && teamInfo.teamId && teamInfo.userId) { 293 | // Fetch team usage for team members 294 | log('[API] Fetching team usage data...'); 295 | const teamUsage = await getTeamUsage(token, teamInfo.teamId); 296 | const userUsage = extractUserUsage(teamUsage, teamInfo.userId); 297 | 298 | premiumRequests = { 299 | current: userUsage.numRequests, 300 | limit: userUsage.maxRequestUsage, 301 | startOfMonth: teamInfo.startOfMonth 302 | }; 303 | log('[API] Successfully extracted team member usage data'); 304 | } else { 305 | const premiumResponse = await axios.get('https://www.cursor.com/api/usage', { 306 | params: { user: userId }, 307 | headers: { 308 | Cookie: `WorkosCursorSessionToken=${token}` 309 | } 310 | }); 311 | 312 | premiumRequests = { 313 | current: premiumResponse.data['gpt-4'].numRequests, 314 | limit: premiumResponse.data['gpt-4'].maxRequestUsage, 315 | startOfMonth: premiumResponse.data.startOfMonth 316 | }; 317 | } 318 | 319 | // Get current date for usage-based pricing (which renews on 2nd/3rd of each month) 320 | const currentDate = new Date(); 321 | const usageBasedBillingDay = 3; // Assuming it's the 3rd day of the month 322 | let usageBasedCurrentMonth = currentDate.getMonth() + 1; 323 | let usageBasedCurrentYear = currentDate.getFullYear(); 324 | 325 | // If we're in the first few days of the month (before billing date), 326 | // consider the previous month as the current billing period 327 | if (currentDate.getDate() < usageBasedBillingDay) { 328 | usageBasedCurrentMonth = usageBasedCurrentMonth === 1 ? 12 : usageBasedCurrentMonth - 1; 329 | if (usageBasedCurrentMonth === 12) { 330 | usageBasedCurrentYear--; 331 | } 332 | } 333 | 334 | // Calculate previous month for usage-based pricing 335 | const usageBasedLastMonth = usageBasedCurrentMonth === 1 ? 12 : usageBasedCurrentMonth - 1; 336 | const usageBasedLastYear = usageBasedCurrentMonth === 1 ? usageBasedCurrentYear - 1 : usageBasedCurrentYear; 337 | 338 | const currentMonthData = await fetchMonthData(token, usageBasedCurrentMonth, usageBasedCurrentYear); 339 | const lastMonthData = await fetchMonthData(token, usageBasedLastMonth, usageBasedLastYear); 340 | 341 | return { 342 | currentMonth: { 343 | month: usageBasedCurrentMonth, 344 | year: usageBasedCurrentYear, 345 | usageBasedPricing: currentMonthData 346 | }, 347 | lastMonth: { 348 | month: usageBasedLastMonth, 349 | year: usageBasedLastYear, 350 | usageBasedPricing: lastMonthData 351 | }, 352 | premiumRequests 353 | }; 354 | } catch (error: any) { 355 | log('[API] Error fetching premium requests: ' + error, true); 356 | log('[API] API error details: ' + JSON.stringify({ 357 | status: error.response?.status, 358 | data: error.response?.data, 359 | message: error.message 360 | }), true); 361 | throw error; 362 | } 363 | } 364 | 365 | export async function getStripeSessionUrl(token: string): Promise { 366 | try { 367 | const response = await axios.get('https://www.cursor.com/api/stripeSession', { 368 | headers: { 369 | Cookie: `WorkosCursorSessionToken=${token}` 370 | } 371 | }); 372 | // Remove quotes from the response string 373 | return response.data.replace(/"/g, ''); 374 | } catch (error: any) { 375 | log('[API] Error getting Stripe session URL: ' + error.message, true); 376 | throw error; 377 | } 378 | } -------------------------------------------------------------------------------- /src/services/database.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as os from 'os'; 3 | import * as jwt from 'jsonwebtoken'; 4 | import * as vscode from 'vscode'; 5 | import * as fs from 'fs'; 6 | import initSqlJs from 'sql.js'; 7 | import { log } from '../utils/logger'; 8 | import { execSync } from 'child_process'; 9 | 10 | // use globalStorageUri to get the user directory path 11 | // support Portable mode : https://code.visualstudio.com/docs/editor/portable 12 | function getDefaultUserDirPath(): string { 13 | // Import getExtensionContext here to avoid circular dependency 14 | const { getExtensionContext } = require('../extension'); 15 | const context = getExtensionContext(); 16 | const extensionGlobalStoragePath = context.globalStorageUri.fsPath; 17 | const userDirPath = path.dirname(path.dirname(path.dirname(extensionGlobalStoragePath))); 18 | log(`[Database] Default user directory path: ${userDirPath}`); 19 | return userDirPath; 20 | } 21 | 22 | export function getCursorDBPath(): string { 23 | // Check for custom path in settings 24 | const config = vscode.workspace.getConfiguration('cursorStats'); 25 | const customPath = config.get('customDatabasePath'); 26 | const userDirPath = getDefaultUserDirPath(); 27 | 28 | if (customPath && customPath.trim() !== '') { 29 | log(`[Database] Using custom path: ${customPath}`); 30 | return customPath; 31 | } 32 | const folderName = vscode.env.appName; 33 | 34 | if (process.platform === 'win32') { 35 | return path.join(userDirPath, 'User', 'globalStorage', 'state.vscdb'); 36 | } else if (process.platform === 'linux') { 37 | const isWSL = vscode.env.remoteName === 'wsl'; 38 | if (isWSL) { 39 | const windowsUsername = getWindowsUsername(); 40 | if (windowsUsername) { 41 | return path.join('/mnt/c/Users', windowsUsername, 'AppData/Roaming', folderName, 'User/globalStorage/state.vscdb'); 42 | } 43 | } 44 | return path.join(userDirPath, 'User', 'globalStorage', 'state.vscdb'); 45 | } else if (process.platform === 'darwin') { 46 | return path.join(userDirPath, 'User', 'globalStorage', 'state.vscdb'); 47 | } 48 | return path.join(userDirPath, 'User', 'globalStorage', 'state.vscdb'); 49 | } 50 | 51 | export async function getCursorTokenFromDB(): Promise { 52 | try { 53 | const dbPath = getCursorDBPath(); 54 | log(`[Database] Attempting to open database at: ${dbPath}`); 55 | 56 | if (!fs.existsSync(dbPath)) { 57 | log('[Database] Database file does not exist', true); 58 | return undefined; 59 | } 60 | 61 | const dbBuffer = fs.readFileSync(dbPath); 62 | const SQL = await initSqlJs(); 63 | const db = new SQL.Database(new Uint8Array(dbBuffer)); 64 | 65 | const result = db.exec("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'"); 66 | 67 | if (!result.length || !result[0].values.length) { 68 | log('[Database] No token found in database'); 69 | db.close(); 70 | return undefined; 71 | } 72 | 73 | const token = result[0].values[0][0] as string; 74 | log(`[Database] Token starts with: ${token.substring(0, 20)}...`); 75 | 76 | try { 77 | const decoded = jwt.decode(token, { complete: true }); 78 | 79 | if (!decoded || !decoded.payload || !decoded.payload.sub) { 80 | log('[Database] Invalid JWT structure: ' + JSON.stringify({ decoded }), true); 81 | db.close(); 82 | return undefined; 83 | } 84 | 85 | const sub = decoded.payload.sub.toString(); 86 | const userId = sub.split('|')[1]; 87 | const sessionToken = `${userId}%3A%3A${token}`; 88 | log(`[Database] Created session token, length: ${sessionToken.length}`); 89 | db.close(); 90 | return sessionToken; 91 | } catch (error: any) { 92 | log('[Database] Error processing token: ' + error, true); 93 | log('[Database] Error details: ' + JSON.stringify({ 94 | name: error.name, 95 | message: error.message, 96 | stack: error.stack 97 | }), true); 98 | db.close(); 99 | return undefined; 100 | } 101 | } catch (error: any) { 102 | log('[Database] Error opening database: ' + error, true); 103 | log('[Database] Database error details: ' + JSON.stringify({ 104 | message: error.message, 105 | stack: error.stack 106 | }), true); 107 | return undefined; 108 | } 109 | } 110 | export function getWindowsUsername(): string | undefined { 111 | try { 112 | // Executes cmd.exe and echoes the %USERNAME% variable 113 | const result = execSync('cmd.exe /C "echo %USERNAME%"', { encoding: 'utf8' }); 114 | const username = result.trim(); 115 | return username || undefined; 116 | } catch (error) { 117 | console.error('Error getting Windows username:', error); 118 | return undefined; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/services/github.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as semver from 'semver'; 3 | import { GitHubRelease, ReleaseCheckResult } from '../interfaces/types'; 4 | import { log } from '../utils/logger'; 5 | import * as vscode from 'vscode'; 6 | import { marked } from 'marked'; 7 | import { getExtensionContext } from '../extension'; 8 | import { t } from '../utils/i18n'; 9 | 10 | const SHOWN_CHANGELOGS_KEY = 'shownChangelogs'; 11 | 12 | export async function checkGitHubRelease(): Promise { 13 | try { 14 | 15 | // Get current version from package.json 16 | const packageJson = require('../../package.json'); 17 | const currentVersion = packageJson.version; 18 | 19 | const response = await axios.get('https://api.github.com/repos/dwtexe/cursor-stats/releases'); 20 | const releases: GitHubRelease[] = response.data; 21 | 22 | if (!releases || releases.length === 0) { 23 | log('[GitHub] No releases found'); 24 | return null; 25 | } 26 | 27 | // Find the latest release (can be prerelease or stable) 28 | const latestRelease = releases[0]; 29 | const latestVersion = latestRelease.tag_name.replace('v', ''); 30 | log(`[GitHub] Latest release: ${latestVersion} (${latestRelease.prerelease ? 'pre-release' : 'stable'})`); 31 | 32 | // Use semver to compare versions 33 | const hasUpdate = semver.gt(latestVersion, currentVersion); 34 | log(`[GitHub] Version comparison: ${currentVersion} -> ${latestVersion} (update available: ${hasUpdate})`); 35 | 36 | if (!hasUpdate) { 37 | return null; 38 | } 39 | 40 | log(`[GitHub] Update available: ${latestRelease.name}`); 41 | log(`[GitHub] Release notes: ${latestRelease.body.substring(0, 100)}...`); 42 | 43 | return { 44 | hasUpdate, 45 | currentVersion, 46 | latestVersion, 47 | isPrerelease: latestRelease.prerelease, 48 | releaseUrl: latestRelease.html_url, 49 | releaseNotes: latestRelease.body, 50 | releaseName: latestRelease.name, 51 | zipballUrl: latestRelease.zipball_url, 52 | tarballUrl: latestRelease.tarball_url, 53 | assets: latestRelease.assets.map(asset => ({ 54 | name: asset.name, 55 | downloadUrl: asset.browser_download_url 56 | })) 57 | }; 58 | } catch (error: any) { 59 | log(`[GitHub] Error checking for updates: ${error.message}`, true); 60 | log(`[GitHub] Error details: ${JSON.stringify({ 61 | status: error.response?.status, 62 | data: error.response?.data, 63 | message: error.message 64 | })}`, true); 65 | return null; 66 | } 67 | } 68 | 69 | export async function checkForUpdates(lastReleaseCheck: number, RELEASE_CHECK_INTERVAL: number, specificVersion?: string): Promise { 70 | try { 71 | const context = getExtensionContext(); 72 | 73 | if (specificVersion) { 74 | // Show changelog for specific version 75 | log(`[GitHub] Showing changelog for specific version: ${specificVersion}`); 76 | const versionQuery = specificVersion.startsWith('v') ? specificVersion : `v${specificVersion}`; 77 | 78 | const response = await axios.get(`https://api.github.com/repos/dwtexe/cursor-stats/releases/tags/${versionQuery}`); 79 | const release: GitHubRelease = response.data; 80 | 81 | if (!release) { 82 | log(`[GitHub] No release found for version ${specificVersion}`); 83 | return; 84 | } 85 | 86 | // Show changelog directly 87 | showChangelogWebview(release, specificVersion); 88 | return; 89 | } 90 | 91 | // Normal update check flow 92 | const now = Date.now(); 93 | if (now - lastReleaseCheck < RELEASE_CHECK_INTERVAL) { 94 | log('[GitHub] Skipping update check - too soon since last check'); 95 | return; 96 | } 97 | 98 | lastReleaseCheck = now; 99 | const releaseInfo = await checkGitHubRelease(); 100 | 101 | if (releaseInfo?.hasUpdate) { 102 | // Get previously shown changelogs 103 | const shownChangelogs: string[] = context.globalState.get(SHOWN_CHANGELOGS_KEY, []); 104 | 105 | // Check if this version's changelog has been shown before 106 | if (!shownChangelogs.includes(releaseInfo.latestVersion)) { 107 | const releaseType = releaseInfo.isPrerelease ? t('github.preRelease') : t('github.stableRelease'); 108 | const message = t('github.updateAvailable', { 109 | releaseType: releaseType, 110 | releaseName: releaseInfo.releaseName, 111 | currentVersion: releaseInfo.currentVersion 112 | }); 113 | log(`[GitHub] Showing update notification: ${message}`); 114 | 115 | // Show changelog directly without asking 116 | log('[GitHub] Showing changelog webview...'); 117 | 118 | // Create the GitHub release object from releaseInfo 119 | const release: GitHubRelease = { 120 | tag_name: `v${releaseInfo.latestVersion}`, 121 | name: releaseInfo.releaseName, 122 | body: releaseInfo.releaseNotes, 123 | html_url: releaseInfo.releaseUrl, 124 | prerelease: releaseInfo.isPrerelease, 125 | zipball_url: releaseInfo.zipballUrl, 126 | tarball_url: releaseInfo.tarballUrl, 127 | assets: releaseInfo.assets.map(asset => ({ 128 | name: asset.name, 129 | browser_download_url: asset.downloadUrl 130 | })) 131 | }; 132 | 133 | showChangelogWebview(release, releaseInfo.latestVersion); 134 | 135 | // Show a small notification that there's an update, but don't ask for action 136 | vscode.window.showInformationMessage(message); 137 | } else { 138 | log(`[GitHub] Changelog for version ${releaseInfo.latestVersion} has already been shown`); 139 | } 140 | } 141 | } catch (error: any) { 142 | log(`[GitHub] Error checking for updates: ${error.message}`, true); 143 | log(`[GitHub] Error details: ${JSON.stringify({ 144 | status: error.response?.status, 145 | data: error.response?.data, 146 | message: error.message 147 | })}`, true); 148 | } 149 | } 150 | 151 | function showChangelogWebview(release: GitHubRelease, version: string): void { 152 | try { 153 | const context = getExtensionContext(); 154 | const releaseType = release.prerelease ? t('github.preRelease') : t('github.stableRelease'); 155 | 156 | const panel = vscode.window.createWebviewPanel( 157 | 'releaseNotes', 158 | t('github.changesTitle', { releaseType: releaseType, version: version }), 159 | vscode.ViewColumn.One, 160 | { 161 | enableScripts: false 162 | } 163 | ); 164 | 165 | const markdownContent = marked(release.body); 166 | 167 | panel.webview.html = ` 168 | 169 | 170 | 171 | 172 | 173 | 300 | 301 | 302 |
303 |
304 |

305 | ${release.name} 306 | ${release.prerelease ? t('github.preRelease') : t('github.latest')} 307 |

308 |
309 |
310 | ${markdownContent} 311 |
312 | 337 | 340 |
341 | 342 | `; 343 | 344 | // Add to the shown changelogs array 345 | const shownChangelogs: string[] = context.globalState.get(SHOWN_CHANGELOGS_KEY, []); 346 | if (!shownChangelogs.includes(version)) { 347 | shownChangelogs.push(version); 348 | context.globalState.update(SHOWN_CHANGELOGS_KEY, shownChangelogs); 349 | log(`[GitHub] Added version ${version} to shown changelogs`); 350 | } 351 | 352 | // If showing for a specific version, show an installation notification 353 | if (version !== release.tag_name.replace('v', '')) { 354 | vscode.window.showInformationMessage(t('github.installedMessage', { version: version })); 355 | } 356 | } catch (error: any) { 357 | log(`[GitHub] Error showing changelog webview: ${error.message}`, true); 358 | } 359 | } -------------------------------------------------------------------------------- /src/services/team.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import axios from 'axios'; 5 | import * as jwt from 'jsonwebtoken'; 6 | import { TeamInfo, TeamMemberInfo, TeamUsageResponse, UserCache, CursorUsageResponse } from '../interfaces/types'; 7 | import { log } from '../utils/logger'; 8 | 9 | const CACHE_FILE_NAME = 'user-cache.json'; 10 | 11 | export async function getUserCachePath(context: vscode.ExtensionContext): Promise { 12 | const cachePath = path.join(context.extensionPath, CACHE_FILE_NAME); 13 | return cachePath; 14 | } 15 | 16 | export async function loadUserCache(context: vscode.ExtensionContext): Promise { 17 | try { 18 | const cachePath = await getUserCachePath(context); 19 | if (fs.existsSync(cachePath)) { 20 | const cacheData = fs.readFileSync(cachePath, 'utf8'); 21 | const cache = JSON.parse(cacheData); 22 | return cache; 23 | } else { 24 | log('[Team] No cache file found'); 25 | } 26 | } catch (error: any) { 27 | log('[Team] Error loading user cache', error.message, true); 28 | log('[Team] Cache error details', { 29 | name: error.name, 30 | stack: error.stack, 31 | code: error.code 32 | }, true); 33 | } 34 | return null; 35 | } 36 | 37 | export async function saveUserCache(context: vscode.ExtensionContext, cache: UserCache): Promise { 38 | try { 39 | const cachePath = await getUserCachePath(context); 40 | log('[Team] Saving cache with data', { 41 | userId: cache.userId, 42 | isTeamMember: cache.isTeamMember, 43 | teamId: cache.teamId, 44 | lastChecked: new Date(cache.lastChecked).toISOString(), 45 | hasStartOfMonth: !!cache.startOfMonth 46 | }); 47 | 48 | fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2)); 49 | log('[Team] Cache saved successfully'); 50 | } catch (error: any) { 51 | log('[Team] Error saving user cache', error.message, true); 52 | log('[Team] Save error details', { 53 | name: error.name, 54 | stack: error.stack, 55 | code: error.code 56 | }, true); 57 | } 58 | } 59 | 60 | export async function checkTeamMembership(token: string, context: vscode.ExtensionContext): Promise<{ isTeamMember: boolean; teamId?: number; userId?: number; startOfMonth: string }> { 61 | try { 62 | // Extract JWT sub from token 63 | const jwtToken = token.split('%3A%3A')[1]; 64 | const decoded = jwt.decode(jwtToken, { complete: true }); 65 | const jwtSub = decoded?.payload?.sub as string; 66 | 67 | // Check cache first 68 | const cache = await loadUserCache(context); 69 | if (cache && cache.jwtSub === jwtSub && cache.startOfMonth) { 70 | return { 71 | isTeamMember: cache.isTeamMember, 72 | teamId: cache.teamId, 73 | userId: cache.userId, 74 | startOfMonth: cache.startOfMonth 75 | }; 76 | } 77 | 78 | // Get start of month from usage API 79 | log('[Team] Cache miss or invalid, fetching fresh usage data'); 80 | const tokenUserId = token.split('%3A%3A')[0]; 81 | log('[Team] Making request to /api/usage endpoint'); 82 | const usageResponse = await axios.get('https://www.cursor.com/api/usage', { 83 | params: { user: tokenUserId }, 84 | headers: { 85 | Cookie: `WorkosCursorSessionToken=${token}` 86 | } 87 | }); 88 | const startOfMonth = usageResponse.data.startOfMonth; 89 | log('[Team] Usage API response', { 90 | startOfMonth, 91 | hasGPT4Data: !!usageResponse.data['gpt-4'], 92 | status: usageResponse.status 93 | }); 94 | 95 | // Fetch team membership data 96 | log('[Team] Making request to /api/dashboard/teams endpoint'); 97 | const response = await axios.post('https://www.cursor.com/api/dashboard/teams', 98 | {}, // empty JSON body 99 | { 100 | headers: { 101 | Cookie: `WorkosCursorSessionToken=${token}` 102 | } 103 | } 104 | ); 105 | 106 | const isTeamMember = response.data.teams && response.data.teams.length > 0; 107 | const teamId = isTeamMember ? response.data.teams[0].id : undefined; 108 | log('[Team] Teams API response', { 109 | isTeamMember, 110 | teamId, 111 | teamCount: response.data.teams?.length || 0, 112 | status: response.status 113 | }); 114 | 115 | let teamUserId: number | undefined; 116 | 117 | if (isTeamMember && teamId) { 118 | // Fetch team details to get userId 119 | log('[Team] Making request to /api/dashboard/team endpoint'); 120 | const teamResponse = await axios.post('https://www.cursor.com/api/dashboard/team', 121 | { teamId }, 122 | { 123 | headers: { 124 | Cookie: `WorkosCursorSessionToken=${token}` 125 | } 126 | } 127 | ); 128 | teamUserId = teamResponse.data.userId; 129 | log('[Team] Team details response', { 130 | userId: teamUserId, 131 | memberCount: teamResponse.data.teamMembers.length, 132 | status: teamResponse.status 133 | }); 134 | } 135 | 136 | // Save to cache 137 | const cacheData = { 138 | userId: teamUserId || 0, 139 | jwtSub, 140 | isTeamMember, 141 | teamId, 142 | lastChecked: Date.now(), 143 | startOfMonth 144 | }; 145 | log('[Team] Saving new cache data'); 146 | await saveUserCache(context, cacheData); 147 | 148 | return { isTeamMember, teamId, userId: teamUserId, startOfMonth }; 149 | } catch (error: any) { 150 | log('[Team] Error checking team membership', error.message, true); 151 | log('[Team] API error details', { 152 | status: error.response?.status, 153 | data: error.response?.data, 154 | headers: error.response?.headers, 155 | config: { 156 | url: error.config?.url, 157 | method: error.config?.method 158 | } 159 | }, true); 160 | throw error; 161 | } 162 | } 163 | 164 | export async function getTeamUsage(token: string, teamId: number): Promise { 165 | try { 166 | log('[Team] Making request to get team usage'); 167 | const response = await axios.post('https://www.cursor.com/api/dashboard/get-team-usage', 168 | { teamId }, // Include teamId in request body 169 | { 170 | headers: { 171 | Cookie: `WorkosCursorSessionToken=${token}` 172 | } 173 | } 174 | ); 175 | log('[Team] Team usage response', { 176 | memberCount: response.data.teamMemberUsage.length, 177 | status: response.status 178 | }); 179 | return response.data; 180 | } catch (error: any) { 181 | log('[Team] Error fetching team usage', error.message, true); 182 | log('[Team] Team usage error details', { 183 | status: error.response?.status, 184 | data: error.response?.data, 185 | headers: error.response?.headers, 186 | config: { 187 | url: error.config?.url, 188 | method: error.config?.method 189 | } 190 | }, true); 191 | throw error; 192 | } 193 | } 194 | 195 | export function extractUserUsage(teamUsage: TeamUsageResponse, userId: number) { 196 | log('[Team] Extracting usage data for user', { userId }); 197 | 198 | const userUsage = teamUsage.teamMemberUsage.find(member => member.id === userId); 199 | if (!userUsage) { 200 | log('[Team] User usage data not found in team response', { 201 | availableUserIds: teamUsage.teamMemberUsage.map(m => m.id), 202 | searchedUserId: userId 203 | }, true); 204 | throw new Error('User usage data not found in team usage response'); 205 | } 206 | 207 | const gpt4Usage = userUsage.usageData.find(data => data.modelType === 'gpt-4'); 208 | if (!gpt4Usage) { 209 | log('[Team] GPT-4 usage data not found for user', { 210 | userId, 211 | availableModels: userUsage.usageData.map(d => d.modelType) 212 | }, true); 213 | throw new Error('GPT-4 usage data not found for user'); 214 | } 215 | 216 | log('[Team] Successfully extracted user usage data', { 217 | userId, 218 | numRequests: gpt4Usage.numRequests, 219 | maxRequestUsage: gpt4Usage.maxRequestUsage, 220 | lastUsage: gpt4Usage.lastUsage 221 | }); 222 | 223 | return { 224 | numRequests: gpt4Usage.numRequests ?? 0, 225 | maxRequestUsage: gpt4Usage.maxRequestUsage 226 | }; 227 | } -------------------------------------------------------------------------------- /src/utils/cooldown.ts: -------------------------------------------------------------------------------- 1 | import { getRefreshIntervalMs } from '../extension'; 2 | import { updateStats } from '../utils/updateStats'; 3 | import { log } from './logger'; 4 | import * as vscode from 'vscode'; 5 | import { t } from './i18n'; 6 | 7 | // Private state 8 | let _countdownInterval: NodeJS.Timeout | null = null; 9 | let _refreshInterval: NodeJS.Timeout | null = null; 10 | let _cooldownStartTime: number | null = null; 11 | let _consecutiveErrorCount: number = 0; 12 | let _isWindowFocused: boolean = true; 13 | let _statusBarItem: vscode.StatusBarItem | null = null; 14 | 15 | export const COOLDOWN_DURATION_MS = 10 * 60 * 1000; // 10 minutes 16 | 17 | // Getters 18 | export const getCountdownInterval = () => _countdownInterval; 19 | export const getRefreshInterval = () => _refreshInterval; 20 | export const getCooldownStartTime = () => _cooldownStartTime; 21 | export const getConsecutiveErrorCount = () => _consecutiveErrorCount; 22 | export const getIsWindowFocused = () => _isWindowFocused; 23 | export const getStatusBarItem = () => _statusBarItem; 24 | 25 | // Setters 26 | export const setCountdownInterval = (interval: NodeJS.Timeout | null) => { 27 | _countdownInterval = interval; 28 | }; 29 | 30 | export const setRefreshInterval = (interval: NodeJS.Timeout | null) => { 31 | _refreshInterval = interval; 32 | }; 33 | 34 | export const setCooldownStartTime = (time: number | null) => { 35 | _cooldownStartTime = time; 36 | }; 37 | 38 | export const setConsecutiveErrorCount = (count: number) => { 39 | _consecutiveErrorCount = count; 40 | }; 41 | 42 | export const setIsWindowFocused = (focused: boolean) => { 43 | _isWindowFocused = focused; 44 | }; 45 | 46 | export const setStatusBarItem = (item: vscode.StatusBarItem) => { 47 | _statusBarItem = item; 48 | }; 49 | 50 | export const incrementConsecutiveErrorCount = () => { 51 | _consecutiveErrorCount++; 52 | return _consecutiveErrorCount; 53 | }; 54 | 55 | export const resetConsecutiveErrorCount = () => { 56 | _consecutiveErrorCount = 0; 57 | }; 58 | 59 | export function formatCountdown(remainingMs: number): string { 60 | const minutes = Math.floor(remainingMs / 60000); 61 | const seconds = Math.floor((remainingMs % 60000) / 1000); 62 | return `${minutes}:${seconds.toString().padStart(2, '0')}`; 63 | } 64 | 65 | export function startCountdownDisplay() { 66 | if (_countdownInterval) { 67 | clearInterval(_countdownInterval); 68 | _countdownInterval = null; 69 | } 70 | 71 | const updateCountdown = () => { 72 | if (!_cooldownStartTime || !_statusBarItem) { 73 | return; 74 | } 75 | 76 | const now = Date.now(); 77 | const elapsed = now - _cooldownStartTime; 78 | const remaining = COOLDOWN_DURATION_MS - elapsed; 79 | 80 | if (remaining <= 0) { 81 | // Cooldown finished 82 | if (_countdownInterval) { 83 | clearInterval(_countdownInterval); 84 | _countdownInterval = null; 85 | } 86 | _cooldownStartTime = null; 87 | _consecutiveErrorCount = 0; 88 | startRefreshInterval(); // Resume normal operation 89 | if (_statusBarItem) { 90 | updateStats(_statusBarItem); // Try updating immediately 91 | } 92 | return; 93 | } 94 | 95 | // Update status bar with countdown 96 | _statusBarItem.text = `$(warning) ${t('statusBar.apiUnavailable', { countdown: formatCountdown(remaining) })}`; 97 | _statusBarItem.show(); 98 | log(`[Cooldown] Updated countdown: ${formatCountdown(remaining)}`); 99 | }; 100 | 101 | // Start the countdown immediately 102 | updateCountdown(); 103 | // Then set up the interval 104 | _countdownInterval = setInterval(updateCountdown, 1000); 105 | 106 | log(`[Cooldown] Started countdown timer at ${new Date().toISOString()}`); 107 | } 108 | 109 | export function startRefreshInterval() { 110 | // Clear any existing interval 111 | if (_refreshInterval) { 112 | clearInterval(_refreshInterval); 113 | _refreshInterval = null; 114 | } 115 | 116 | // Don't start interval if in cooldown or window not focused 117 | if (_cooldownStartTime || !_isWindowFocused) { 118 | log(`[Refresh] Refresh interval not started: ${_cooldownStartTime ? 'in cooldown' : 'window not focused'}`); 119 | return; 120 | } 121 | 122 | // Start new interval 123 | const intervalMs = getRefreshIntervalMs(); 124 | log(`[Refresh] Starting refresh interval: ${intervalMs}ms`); 125 | if (_statusBarItem) { 126 | _refreshInterval = setInterval(() => { 127 | if (!_cooldownStartTime) { // Double-check we're not in cooldown 128 | updateStats(_statusBarItem!); 129 | } 130 | }, intervalMs); 131 | } 132 | } 133 | 134 | // Cleanup function 135 | export function clearAllIntervals() { 136 | if (_countdownInterval) { 137 | clearInterval(_countdownInterval); 138 | _countdownInterval = null; 139 | } 140 | if (_refreshInterval) { 141 | clearInterval(_refreshInterval); 142 | _refreshInterval = null; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/utils/currency.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import axios from 'axios'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import { log } from './logger'; 6 | import { CurrencyRates, CurrencyCache } from '../interfaces/types'; 7 | import { getExtensionContext } from '../extension'; 8 | 9 | const CURRENCY_API_URL = 'https://latest.currency-api.pages.dev/v1/currencies/usd.json'; 10 | const CURRENCY_CACHE_FILE = 'currency-rates.json'; 11 | const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours 12 | 13 | // List of supported currencies (excluding cryptocurrencies) 14 | // Updated to list only currency codes; names will be localized via i18n 15 | export const SUPPORTED_CURRENCIES: string[] = [ 16 | 'USD', 17 | 'EUR', 18 | 'GBP', 19 | 'JPY', 20 | 'AUD', 21 | 'CAD', 22 | 'CHF', 23 | 'CNY', 24 | 'INR', 25 | 'MXN', 26 | 'BRL', 27 | 'RUB', 28 | 'KRW', 29 | 'SGD', 30 | 'NZD', 31 | 'TRY', 32 | 'ZAR', 33 | 'SEK', 34 | 'NOK', 35 | 'DKK', 36 | 'HKD', 37 | 'TWD', 38 | 'PHP', 39 | 'THB', 40 | 'IDR', 41 | 'VND', 42 | 'ILS', 43 | 'AED', 44 | 'SAR', 45 | 'MYR', 46 | 'PLN', 47 | 'CZK', 48 | 'HUF', 49 | 'RON', 50 | 'BGN', 51 | 'HRK', 52 | 'EGP', 53 | 'QAR', 54 | 'KWD', 55 | 'MAD' 56 | ]; 57 | 58 | export function getCurrencySymbol(currencyCode: string): string { 59 | const symbolMap: { [key: string]: string } = { 60 | 'USD': '$', 61 | 'EUR': '€', 62 | 'GBP': '£', 63 | 'JPY': '¥', 64 | 'AUD': 'A$', 65 | 'CAD': 'C$', 66 | 'CHF': 'CHF', 67 | 'CNY': '¥', 68 | 'INR': '₹', 69 | 'MXN': 'Mex$', 70 | 'BRL': 'R$', 71 | 'RUB': '₽', 72 | 'KRW': '₩', 73 | 'SGD': 'S$', 74 | 'NZD': 'NZ$', 75 | 'TRY': '₺', 76 | 'ZAR': 'R', 77 | 'SEK': 'kr', 78 | 'NOK': 'kr', 79 | 'DKK': 'kr', 80 | 'HKD': 'HK$', 81 | 'TWD': 'NT$', 82 | 'PHP': '₱', 83 | 'THB': '฿', 84 | 'IDR': 'Rp', 85 | 'VND': '₫', 86 | 'ILS': '₪', 87 | 'AED': 'د.إ', 88 | 'SAR': '﷼', 89 | 'MYR': 'RM', 90 | 'PLN': 'zł', 91 | 'CZK': 'Kč', 92 | 'HUF': 'Ft', 93 | 'RON': 'lei', 94 | 'BGN': 'лв', 95 | 'HRK': 'kn', 96 | 'EGP': 'E£', 97 | 'QAR': 'ر.ق', 98 | 'KWD': 'د.ك', 99 | 'MAD': 'د.م.' 100 | }; 101 | 102 | return symbolMap[currencyCode] || currencyCode; 103 | } 104 | 105 | export async function getCachedRates(): Promise { 106 | try { 107 | const context = getExtensionContext(); 108 | const cachePath = path.join(context.extensionPath, CURRENCY_CACHE_FILE); 109 | 110 | if (fs.existsSync(cachePath)) { 111 | const cacheData = fs.readFileSync(cachePath, 'utf8'); 112 | const cache: CurrencyCache = JSON.parse(cacheData); 113 | 114 | // Check if cache is still valid (less than 24 hours old) 115 | if (Date.now() - cache.timestamp < CACHE_EXPIRY_MS) { 116 | log('[Currency] Using cached exchange rates', { 117 | date: cache.rates.date, 118 | cacheAge: Math.round((Date.now() - cache.timestamp) / 1000 / 60) + ' minutes' 119 | }); 120 | return cache.rates; 121 | } 122 | 123 | log('[Currency] Cache expired, will fetch new rates'); 124 | } else { 125 | log('[Currency] No cache file found'); 126 | } 127 | } catch (error: any) { 128 | log('[Currency] Error reading cache: ' + error.message, true); 129 | } 130 | 131 | return null; 132 | } 133 | 134 | export async function saveCurrencyCache(rates: CurrencyRates): Promise { 135 | try { 136 | const context = getExtensionContext(); 137 | const cachePath = path.join(context.extensionPath, CURRENCY_CACHE_FILE); 138 | 139 | const cache: CurrencyCache = { 140 | rates, 141 | timestamp: Date.now() 142 | }; 143 | 144 | fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2)); 145 | log('[Currency] Exchange rates cached successfully'); 146 | } catch (error: any) { 147 | log('[Currency] Error saving cache: ' + error.message, true); 148 | } 149 | } 150 | 151 | export async function fetchExchangeRates(): Promise { 152 | try { 153 | // First check if we have a valid cache 154 | const cachedRates = await getCachedRates(); 155 | if (cachedRates) { 156 | return cachedRates; 157 | } 158 | 159 | // If not, fetch from API 160 | log('[Currency] Fetching exchange rates from API'); 161 | const response = await axios.get(CURRENCY_API_URL); 162 | log('[Currency] Received exchange rates', { 163 | date: response.data.date, 164 | currencies: Object.keys(response.data.usd).length 165 | }); 166 | 167 | // Save to cache 168 | await saveCurrencyCache(response.data); 169 | 170 | return response.data; 171 | } catch (error: any) { 172 | log('[Currency] Error fetching exchange rates: ' + error.message, true); 173 | throw new Error(`Failed to fetch currency exchange rates: ${error.message}`); 174 | } 175 | } 176 | 177 | export async function convertAmount(amount: number, targetCurrency: string): Promise<{ value: number; symbol: string }> { 178 | try { 179 | // If target is USD, no conversion needed 180 | if (targetCurrency === 'USD') { 181 | return { value: amount, symbol: '$' }; 182 | } 183 | 184 | // Get exchange rates 185 | const rates = await fetchExchangeRates(); 186 | 187 | // Get the exchange rate for the target currency (rates are USD to target) 188 | const rate = rates.usd[targetCurrency.toLowerCase()]; 189 | 190 | if (!rate) { 191 | log(`[Currency] Exchange rate not found for ${targetCurrency}`, true); 192 | return { value: amount, symbol: '$' }; // Fall back to USD 193 | } 194 | 195 | // Convert the amount 196 | const convertedValue = amount * rate; 197 | 198 | // Get the currency symbol 199 | const symbol = getCurrencySymbol(targetCurrency); 200 | 201 | log(`[Currency] Converted $${amount} to ${symbol}${convertedValue.toFixed(2)} (${targetCurrency})`); 202 | 203 | return { value: convertedValue, symbol }; 204 | } catch (error: any) { 205 | log('[Currency] Conversion error: ' + error.message, true); 206 | return { value: amount, symbol: '$' }; // Fall back to USD 207 | } 208 | } 209 | 210 | export function formatCurrency(amount: number, currencyCode: string, decimals: number = 2): string { 211 | const symbol = getCurrencySymbol(currencyCode); 212 | 213 | // Special formatting for some currencies 214 | if (currencyCode === 'JPY' || currencyCode === 'KRW') { 215 | // These currencies typically don't use decimal places 216 | return `${symbol}${Math.round(amount)}`; 217 | } 218 | 219 | return `${symbol}${amount.toFixed(decimals)}`; 220 | } 221 | 222 | export function getCurrentCurrency(): string { 223 | const config = vscode.workspace.getConfiguration('cursorStats'); 224 | return config.get('currency', 'USD'); 225 | } 226 | 227 | export async function convertAndFormatCurrency(amount: number, decimals: number = 2): Promise { 228 | const currencyCode = getCurrentCurrency(); 229 | 230 | if (currencyCode === 'USD') { 231 | return `$${amount.toFixed(decimals)}`; 232 | } 233 | 234 | try { 235 | const { value, symbol } = await convertAmount(amount, currencyCode); 236 | return formatCurrency(value, currencyCode, decimals); 237 | } catch (error) { 238 | return `$${amount.toFixed(decimals)}`; 239 | } 240 | } -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { log } from './logger'; 5 | import { LanguagePack } from '../interfaces/i18n'; 6 | 7 | // Language pack storage 8 | const languagePacks: { [key: string]: LanguagePack } = {}; 9 | 10 | let currentLanguage = 'en'; 11 | let currentLanguagePack: LanguagePack; 12 | let onLanguageChangeCallback: ((newLanguage: string, languageLabel: string) => void) | null = null; 13 | 14 | /** 15 | * Initialize internationalization system 16 | */ 17 | export function initializeI18n(): void { 18 | loadLanguagePacks(); 19 | 20 | // Set initial language pack 21 | if (!currentLanguagePack) { 22 | const config = vscode.workspace.getConfiguration('cursorStats'); 23 | const language = config.get('language', 'en'); 24 | currentLanguagePack = languagePacks[language] || languagePacks['en']; 25 | currentLanguage = language; 26 | 27 | if (!currentLanguagePack) { 28 | log('[I18n] Critical: No language pack available! Extension may not work properly.', true); 29 | } 30 | } 31 | 32 | updateCurrentLanguage(); 33 | 34 | // Listen for language setting changes 35 | vscode.workspace.onDidChangeConfiguration((e: vscode.ConfigurationChangeEvent) => { 36 | if (e.affectsConfiguration('cursorStats.language')) { 37 | updateCurrentLanguage(); 38 | log('[I18n] Language setting changed, reloading language pack'); 39 | } 40 | }); 41 | } 42 | 43 | /** 44 | * Load language pack file 45 | */ 46 | function loadLanguagePackFromFile(languageCode: string): LanguagePack | null { 47 | try { 48 | // Get extension root directory path 49 | const extensionPath = vscode.extensions.getExtension('Dwtexe.cursor-stats')?.extensionPath; 50 | if (!extensionPath) { 51 | log(`[I18n] Extension path not found`, true); 52 | return null; 53 | } 54 | 55 | // Try multiple paths to handle both development and production scenarios 56 | const possiblePaths = [ 57 | // Production path (when extension is packaged) 58 | path.join(extensionPath, 'src', 'locales', `${languageCode}.json`), 59 | // Alternative production path 60 | path.join(extensionPath, 'locales', `${languageCode}.json`), 61 | // Development path 62 | path.join(extensionPath, 'out', 'locales', `${languageCode}.json`) 63 | ]; 64 | 65 | let localesPath: string | null = null; 66 | for (const testPath of possiblePaths) { 67 | if (fs.existsSync(testPath)) { 68 | localesPath = testPath; 69 | break; 70 | } 71 | } 72 | 73 | if (!localesPath) { 74 | log(`[I18n] Language file not found in any of these paths:`, possiblePaths, true); 75 | return null; 76 | } 77 | 78 | const fileContent = fs.readFileSync(localesPath, 'utf8'); 79 | const languagePack = JSON.parse(fileContent) as LanguagePack; 80 | 81 | log(`[I18n] Loaded language pack for: ${languageCode} from ${localesPath}`); 82 | return languagePack; 83 | } catch (error) { 84 | log(`[I18n] Error loading language pack for ${languageCode}: ${error instanceof Error ? error.message : String(error)}`, true); 85 | return null; 86 | } 87 | } 88 | 89 | /** 90 | * Load all language packs 91 | */ 92 | function loadLanguagePacks(): void { 93 | const supportedLanguages = ['en', 'zh', 'ko', 'ja']; 94 | 95 | for (const lang of supportedLanguages) { 96 | const pack = loadLanguagePackFromFile(lang); 97 | if (pack) { 98 | languagePacks[lang] = pack; 99 | } 100 | } 101 | 102 | // Ensure English language pack is loaded (required default language) 103 | if (!languagePacks['en']) { 104 | log('[I18n] Critical: English language pack not loaded! Extension may not work properly.', true); 105 | } 106 | 107 | log('[I18n] Language packs loaded'); 108 | } 109 | 110 | /** 111 | * Update current language 112 | */ 113 | function updateCurrentLanguage(): void { 114 | const config = vscode.workspace.getConfiguration('cursorStats'); 115 | const newLanguage = config.get('language', 'en'); 116 | 117 | if (newLanguage !== currentLanguage) { 118 | const oldLanguage = currentLanguage; 119 | currentLanguage = newLanguage; 120 | 121 | // Get language pack, fallback to English if not available 122 | const languagePack = languagePacks[newLanguage] || languagePacks['en']; 123 | if (languagePack) { 124 | currentLanguagePack = languagePack; 125 | log(`[I18n] Language changed to: ${newLanguage}`); 126 | 127 | // Trigger language change callback 128 | if (onLanguageChangeCallback && oldLanguage !== 'en') { // Avoid triggering during initialization 129 | const languageLabels: { [key: string]: string } = { 130 | 'en': 'English', 131 | 'zh': '中文', 132 | 'ko': '한국어', 133 | 'ja': '日本語' 134 | }; 135 | onLanguageChangeCallback(newLanguage, languageLabels[newLanguage] || newLanguage); 136 | } 137 | } else { 138 | log(`[I18n] Warning: No language pack found for ${newLanguage} or English`, true); 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Get translated text with fallback mechanism 145 | * @param key Translation key (supports nesting, e.g., 'statusBar.premiumFastRequests') 146 | * @param params Replacement parameters 147 | */ 148 | export function t(key: string, params?: { [key: string]: string | number }): string { 149 | let value = getTranslationValue(key, currentLanguagePack); 150 | 151 | // If translation not found in current language and current language is not English, try English fallback 152 | if (value === null && currentLanguage !== 'en' && languagePacks['en']) { 153 | log(`[I18n] Translation key '${key}' not found in ${currentLanguage}, falling back to English`); 154 | value = getTranslationValue(key, languagePacks['en']); 155 | } 156 | 157 | // If still no translation found, return the key itself 158 | if (value === null) { 159 | log(`[I18n] Translation key not found in any language pack: ${key}`, true); 160 | return key; 161 | } 162 | 163 | if (typeof value !== 'string') { 164 | log(`[I18n] Translation value is not a string: ${key}`, true); 165 | return key; 166 | } 167 | 168 | // Replace parameters 169 | if (params) { 170 | Object.keys(params).forEach(param => { 171 | value = value.replace(new RegExp(`{${param}}`, 'g'), params[param].toString()); 172 | }); 173 | } 174 | 175 | return value; 176 | } 177 | 178 | /** 179 | * Helper function to get translation value from a language pack 180 | * @param key Translation key 181 | * @param languagePack Language pack to search in 182 | * @returns Translation value or null if not found 183 | */ 184 | function getTranslationValue(key: string, languagePack: LanguagePack): any { 185 | if (!languagePack) { 186 | return null; 187 | } 188 | 189 | const keys = key.split('.'); 190 | let value: any = languagePack; 191 | 192 | for (const k of keys) { 193 | if (value && typeof value === 'object' && k in value) { 194 | value = value[k]; 195 | } else { 196 | return null; // Key not found 197 | } 198 | } 199 | 200 | return value; 201 | } 202 | 203 | /** 204 | * Get current language 205 | */ 206 | export function getCurrentLanguage(): string { 207 | return currentLanguage; 208 | } 209 | 210 | /** 211 | * Get current language pack 212 | */ 213 | export function getCurrentLanguagePack(): LanguagePack { 214 | return currentLanguagePack; 215 | } 216 | 217 | /** 218 | * Set language change callback function 219 | */ 220 | export function setOnLanguageChangeCallback(callback: (newLanguage: string, languageLabel: string) => void): void { 221 | onLanguageChangeCallback = callback; 222 | } -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | let outputChannel: vscode.OutputChannel | undefined; 4 | // Keep a log history in memory for reporting 5 | const logHistory: string[] = []; 6 | const MAX_LOG_HISTORY = 1000; // Maximum number of log entries to keep in memory 7 | 8 | export function initializeLogging(context: vscode.ExtensionContext): void { 9 | try { 10 | outputChannel = vscode.window.createOutputChannel('Cursor Stats'); 11 | context.subscriptions.push(outputChannel); 12 | log('[Initialization] Output channel created successfully'); 13 | } catch { 14 | log('[Critical] Failed to create output channel', true); 15 | throw new Error('Failed to initialize logging system'); 16 | } 17 | } 18 | 19 | export function log(message: string, data?: any, error: boolean = false): void { 20 | const config = vscode.workspace.getConfiguration('cursorStats'); 21 | const loggingEnabled = config.get('enableLogging', false); 22 | 23 | const shouldLog = error || 24 | (loggingEnabled && ( 25 | message.includes('[Initialization]') || 26 | message.includes('[Status Bar]') || 27 | message.includes('[Database]') || 28 | message.includes('[Auth]') || 29 | message.includes('[Stats]') || 30 | message.includes('[API]') || 31 | message.includes('[GitHub]') || 32 | message.includes('[Panels]') || 33 | message.includes('[Command]') || 34 | message.includes('[Notifications]') || 35 | message.includes('[Refresh]') || 36 | message.includes('[Settings]') || 37 | message.includes('[Critical]') || 38 | message.includes('[Deactivation]') || 39 | message.includes('[Team]') || 40 | message.includes('[Cooldown]') || 41 | message.includes('[Currency]') || 42 | message.includes('[Report]') 43 | )); 44 | 45 | if (shouldLog) { 46 | safeLog(message, data, error); 47 | } 48 | } 49 | 50 | function safeLog(message: string, data?: any, isError: boolean = false): void { 51 | const timestamp = new Date().toISOString(); 52 | const logLevel = isError ? 'ERROR' : 'INFO'; 53 | let logMessage = `[${timestamp}] [${logLevel}] ${message}`; 54 | 55 | // Add data if provided 56 | if (data !== undefined) { 57 | try { 58 | const dataString = typeof data === 'object' ? 59 | '\n' + JSON.stringify(data, null, 2) : 60 | ' ' + data.toString(); 61 | logMessage += dataString; 62 | } catch { 63 | logMessage += ' [Error stringifying data]'; 64 | } 65 | } 66 | 67 | // Store in the log history 68 | addToLogHistory(logMessage); 69 | 70 | // Always log to console 71 | if (isError) { 72 | console.error(logMessage); 73 | } else { 74 | console.log(logMessage); 75 | } 76 | 77 | // Try to log to output channel if it exists 78 | try { 79 | outputChannel?.appendLine(logMessage); 80 | } catch { 81 | console.error('Failed to write to output channel'); 82 | } 83 | 84 | // Show error messages in the UI for critical issues 85 | if (isError && message.includes('[Critical]')) { 86 | try { 87 | vscode.window.showErrorMessage(`Cursor Stats: ${message}`); 88 | } catch { 89 | console.error('Failed to show error message in UI'); 90 | } 91 | } 92 | } 93 | 94 | function addToLogHistory(logMessage: string): void { 95 | // Add to the beginning for most recent first 96 | logHistory.unshift(logMessage); 97 | 98 | // Trim if exceeds maximum size 99 | if (logHistory.length > MAX_LOG_HISTORY) { 100 | logHistory.length = MAX_LOG_HISTORY; 101 | } 102 | } 103 | 104 | // Get all stored logs for reporting purposes 105 | export function getLogHistory(): string[] { 106 | return [...logHistory]; 107 | } 108 | 109 | // Clear the log history 110 | export function clearLogHistory(): void { 111 | logHistory.length = 0; 112 | } 113 | 114 | export function disposeLogger(): void { 115 | if (outputChannel) { 116 | outputChannel.dispose(); 117 | outputChannel = undefined; 118 | } 119 | } -------------------------------------------------------------------------------- /src/utils/progressBars.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ProgressBarSettings } from '../interfaces/types'; 3 | import { t } from './i18n'; 4 | 5 | // Emojis for progress bar representation 6 | const PROGRESS_EMPTY = '⬜'; 7 | const PROGRESS_FILLED = '🟩'; 8 | const PROGRESS_WARNING = '🟨'; 9 | const PROGRESS_CRITICAL = '🟥'; 10 | 11 | /** 12 | * Generate a progress bar using emoji characters 13 | * @param percentage The current percentage (0-100) 14 | * @param length The number of characters in the progress bar 15 | * @param warningThreshold Threshold percentage for warning color 16 | * @param criticalThreshold Threshold percentage for critical color 17 | * @returns A string representing the progress bar 18 | */ 19 | export function createProgressBar( 20 | percentage: number, 21 | length: number = 10, 22 | warningThreshold: number = 75, 23 | criticalThreshold: number = 90 24 | ): string { 25 | // Ensure percentage is within 0-100 range 26 | const clampedPercentage = Math.max(0, Math.min(100, percentage)); 27 | 28 | // Calculate filled positions 29 | const filledCount = Math.round((clampedPercentage / 100) * length); 30 | const emptyCount = length - filledCount; 31 | 32 | let bar = ''; 33 | 34 | // Choose emoji color based on thresholds 35 | let filledEmoji = PROGRESS_FILLED; 36 | if (clampedPercentage >= criticalThreshold) { 37 | filledEmoji = PROGRESS_CRITICAL; 38 | } else if (clampedPercentage >= warningThreshold) { 39 | filledEmoji = PROGRESS_WARNING; 40 | } 41 | 42 | // Build the progress bar 43 | bar = filledEmoji.repeat(filledCount) + PROGRESS_EMPTY.repeat(emptyCount); 44 | 45 | return bar; 46 | } 47 | 48 | /** 49 | * Determine if progress bars should be displayed based on user settings 50 | * @returns Whether progress bars should be shown 51 | */ 52 | export function shouldShowProgressBars(): boolean { 53 | const config = vscode.workspace.getConfiguration('cursorStats'); 54 | return config.get('showProgressBars', false); 55 | } 56 | 57 | /** 58 | * Get progress bar settings from user configuration 59 | * @returns Progress bar settings object 60 | */ 61 | export function getProgressBarSettings(): ProgressBarSettings { 62 | const config = vscode.workspace.getConfiguration('cursorStats'); 63 | 64 | return { 65 | barLength: config.get('progressBarLength', 10), 66 | warningThreshold: config.get('progressBarWarningThreshold', 75), 67 | criticalThreshold: config.get('progressBarCriticalThreshold', 90) 68 | }; 69 | } 70 | 71 | /** 72 | * Create a period progress bar showing days passed in a billing period 73 | * @param startDate Start date string of the period 74 | * @param endDate End date string of the period (optional) 75 | * @param label Label for the progress bar 76 | * @returns Formatted progress bar with label and percentage 77 | */ 78 | export function createPeriodProgressBar( 79 | startDate: string, 80 | endDate?: string, 81 | label: string = t('statusBar.period') 82 | ): string { 83 | if (!shouldShowProgressBars()) { 84 | return ''; 85 | } 86 | 87 | const settings = getProgressBarSettings(); 88 | const config = vscode.workspace.getConfiguration('cursorStats'); 89 | const excludeWeekends = config.get('excludeWeekends', false); 90 | 91 | try { 92 | // Handle date formats like "17 April - 17 May" or "3 April - 2 May" 93 | let start: Date; 94 | let end: Date; 95 | 96 | if (startDate.includes('-')) { 97 | // Parse date range in format like "17 April - 17 May" 98 | const [startStr, endStr] = startDate.split('-').map((s) => s.trim()); 99 | 100 | // Get current year 101 | const currentYear = new Date().getFullYear(); 102 | 103 | // Parse start date 104 | const startParts = startStr.split(' '); 105 | const startDay = parseInt(startParts[0]); 106 | const startMonth = getMonthNumber(startParts[1]); 107 | 108 | // Parse end date 109 | const endParts = endStr.split(' '); 110 | const endDay = parseInt(endParts[0]); 111 | const endMonth = getMonthNumber(endParts[1]); 112 | 113 | // Create Date objects with current year 114 | start = new Date(currentYear, startMonth, startDay); 115 | end = new Date(currentYear, endMonth, endDay); 116 | 117 | // If end date is before start date, it means the period crosses into next year 118 | if (end < start) { 119 | end.setFullYear(currentYear + 1); 120 | } 121 | } else { 122 | // Regular date parsing for ISO format dates 123 | start = new Date(startDate); 124 | end = endDate ? new Date(endDate) : getEndOfPeriod(start); 125 | } 126 | 127 | const now = new Date(); 128 | 129 | let totalDays: number; 130 | let elapsedDays: number; 131 | 132 | if (excludeWeekends) { 133 | // Calculate weekdays only 134 | totalDays = calculateWeekdays(start, end); 135 | elapsedDays = calculateWeekdays(start, now); 136 | } else { 137 | // Calculate total days in the period (original logic) 138 | totalDays = Math.round( 139 | (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24) 140 | ); 141 | 142 | // Calculate days elapsed 143 | elapsedDays = Math.round( 144 | (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24) 145 | ); 146 | } 147 | 148 | // Calculate percentage elapsed 149 | const percentage = Math.min( 150 | 100, 151 | Math.max(0, (elapsedDays / totalDays) * 100) 152 | ); 153 | 154 | // Create the progress bar 155 | const progressBar = createProgressBar( 156 | percentage, 157 | settings.barLength, 158 | settings.warningThreshold, 159 | settings.criticalThreshold 160 | ); 161 | 162 | // Return with label but without percentage 163 | return `${label}: ${progressBar}`; 164 | } catch (error) { 165 | // If date parsing fails, log error and return empty string 166 | console.error(`Error creating period progress bar: ${error}`); 167 | return `${label}: ${t('progressBar.errorParsingDates')}`; 168 | } 169 | } 170 | 171 | /** 172 | * Convert month name to month number (0-11) 173 | * @param monthName Month name (e.g., "January", "Jan", "1월") 174 | * @returns Month number (0-11) 175 | */ 176 | export function getMonthNumber(monthName: string): number { 177 | const months: {[key: string]: number} = { 178 | // English month names 179 | 'january': 0, 'jan': 0, 180 | 'february': 1, 'feb': 1, 181 | 'march': 2, 'mar': 2, 182 | 'april': 3, 'apr': 3, 183 | 'may': 4, 184 | 'june': 5, 'jun': 5, 185 | 'july': 6, 'jul': 6, 186 | 'august': 7, 'aug': 7, 187 | 'september': 8, 'sep': 8, 'sept': 8, 188 | 'october': 9, 'oct': 9, 189 | 'november': 10, 'nov': 10, 190 | 'december': 11, 'dec': 11 191 | }; 192 | 193 | // Add translated month names 194 | const monthKeys = [ 195 | 'january', 'february', 'march', 'april', 196 | 'may', 'june', 'july', 'august', 197 | 'september', 'october', 'november', 'december' 198 | ]; 199 | 200 | for (let i = 0; i < 12; i++) { 201 | const translatedName = t(`statusBar.months.${monthKeys[i]}`); 202 | months[translatedName.toLowerCase()] = i; 203 | } 204 | 205 | return months[monthName.toLowerCase()] || 0; 206 | } 207 | 208 | /** 209 | * Create a usage progress bar showing amount consumed against a limit 210 | * @param current Current usage amount 211 | * @param limit Maximum limit 212 | * @param label Label for the progress bar 213 | * @returns Formatted progress bar with label and percentage 214 | */ 215 | export function createUsageProgressBar(current: number, limit: number, label: string = t('statusBar.usage')): string { 216 | if (!shouldShowProgressBars()) { 217 | return ''; 218 | } 219 | 220 | const settings = getProgressBarSettings(); 221 | 222 | // Calculate percentage 223 | const percentage = Math.min(100, Math.max(0, (current / limit) * 100)); 224 | 225 | // Create the progress bar 226 | const progressBar = createProgressBar( 227 | percentage, 228 | settings.barLength, 229 | settings.warningThreshold, 230 | settings.criticalThreshold 231 | ); 232 | 233 | // Return with label but without percentage 234 | return `${label}: ${progressBar}`; 235 | } 236 | 237 | /** 238 | * Get the end date of a billing period based on the start date 239 | * @param startDate The start date of the period 240 | * @returns The end date of the period 241 | */ 242 | function getEndOfPeriod(startDate: Date): Date { 243 | const endDate = new Date(startDate); 244 | 245 | // Add one month to the start date 246 | endDate.setMonth(endDate.getMonth() + 1); 247 | 248 | // Subtract one day to get the end of the period 249 | endDate.setDate(endDate.getDate() - 1); 250 | 251 | return endDate; 252 | } 253 | 254 | /** 255 | * Calculate the number of weekdays between two dates (excluding weekends) 256 | * @param startDate Start date 257 | * @param endDate End date 258 | * @returns Number of weekdays 259 | */ 260 | function calculateWeekdays(startDate: Date, endDate: Date): number { 261 | let count = 0; 262 | const current = new Date(startDate); 263 | 264 | while (current <= endDate) { 265 | const dayOfWeek = current.getDay(); 266 | // 0 = Sunday, 6 = Saturday 267 | if (dayOfWeek !== 0 && dayOfWeek !== 6) { 268 | count++; 269 | } 270 | current.setDate(current.getDate() + 1); 271 | } 272 | 273 | return count; 274 | } 275 | 276 | /** 277 | * Calculate remaining weekdays from current date to end date 278 | * @param endDate End date 279 | * @returns Number of remaining weekdays 280 | */ 281 | export function calculateRemainingWeekdays(endDate: Date): number { 282 | const now = new Date(); 283 | const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 284 | 285 | if (today >= endDate) { 286 | return 0; 287 | } 288 | 289 | return calculateWeekdays(today, endDate); 290 | } 291 | 292 | /** 293 | * Check if current date is a weekend 294 | * @returns True if current date is Saturday or Sunday 295 | */ 296 | export function isWeekend(): boolean { 297 | const now = new Date(); 298 | const dayOfWeek = now.getDay(); 299 | return dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday 300 | } 301 | 302 | /** 303 | * Calculate daily remaining fast requests 304 | * @param currentRequests Current number of requests used 305 | * @param limitRequests Total request limit 306 | * @param periodEndDate End date of the current period 307 | * @returns Formatted string showing requests per day or weekend message 308 | */ 309 | export function calculateDailyRemaining( 310 | currentRequests: number, 311 | limitRequests: number, 312 | periodEndDate: Date 313 | ): string { 314 | const config = vscode.workspace.getConfiguration('cursorStats'); 315 | const excludeWeekends = config.get('excludeWeekends', false); 316 | const showDailyRemaining = config.get('showDailyRemaining', false); 317 | 318 | if (!showDailyRemaining) { 319 | return ''; 320 | } 321 | 322 | const remainingRequests = limitRequests - currentRequests; 323 | 324 | if (remainingRequests <= 0) { 325 | return t('progressBar.dailyRemainingLimitReached'); 326 | } 327 | 328 | if (excludeWeekends && isWeekend()) { 329 | return t('progressBar.dailyRemainingWeekend'); 330 | } 331 | 332 | let remainingDays: number; 333 | 334 | if (excludeWeekends) { 335 | remainingDays = calculateRemainingWeekdays(periodEndDate); 336 | } else { 337 | const now = new Date(); 338 | const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 339 | remainingDays = Math.max( 340 | 0, 341 | Math.ceil( 342 | (periodEndDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24) 343 | ) 344 | ); 345 | } 346 | 347 | if (remainingDays <= 0) { 348 | return t('progressBar.dailyRemainingPeriodEnding'); 349 | } 350 | 351 | const requestsPerDay = Math.ceil(remainingRequests / remainingDays); 352 | const dayType = excludeWeekends ? t('statusBar.weekday') : t('time.day'); 353 | const dayTypePlural = excludeWeekends ? t('statusBar.weekdays') : t('time.days'); 354 | 355 | return t('progressBar.dailyRemainingCalculation', { 356 | requestsPerDay, 357 | dayType, 358 | remainingRequests, 359 | remainingDays, 360 | dayTypePlural 361 | }); 362 | } 363 | -------------------------------------------------------------------------------- /src/utils/report.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as os from 'os'; 5 | import axios from 'axios'; 6 | import { CursorReport, CursorUsageResponse } from '../interfaces/types'; 7 | import { fetchCursorStats, getCurrentUsageLimit } from '../services/api'; 8 | import { getCursorTokenFromDB } from '../services/database'; 9 | import { log, getLogHistory } from './logger'; 10 | import { getTeamUsage, checkTeamMembership } from '../services/team'; 11 | import { getExtensionContext } from '../extension'; 12 | import { t } from './i18n'; 13 | 14 | /** 15 | * Generates a comprehensive report of the extension's data and API responses 16 | */ 17 | export async function generateReport(): Promise<{ reportPath: string; success: boolean }> { 18 | log('[Report] Starting report generation'); 19 | 20 | const context = getExtensionContext(); 21 | const packageJson = require('../../package.json'); 22 | 23 | // Initialize the report object 24 | const report: CursorReport = { 25 | timestamp: new Date().toISOString(), 26 | extensionVersion: packageJson.version, 27 | os: `${os.platform()} ${os.release()}`, 28 | vsCodeVersion: vscode.version, 29 | cursorStats: null, 30 | usageLimitResponse: null, 31 | premiumUsage: null, 32 | teamInfo: null, 33 | teamUsage: null, 34 | rawResponses: {}, 35 | logs: getLogHistory().reverse(), 36 | errors: {} 37 | }; 38 | 39 | try { 40 | // Get the Cursor token 41 | const token = await getCursorTokenFromDB(); 42 | if (!token) { 43 | report.errors.token = 'Failed to retrieve token from database'; 44 | log('[Report] Failed to retrieve token', true); 45 | return saveReport(report, context); 46 | } 47 | 48 | // Extract user ID from token 49 | const userId = token.split('%3A%3A')[0]; 50 | 51 | // Get current date for usage-based pricing (which renews on 2nd/3rd of each month) 52 | const currentDate = new Date(); 53 | const usageBasedBillingDay = 3; // Assuming it's the 3rd day of the month 54 | let usageBasedCurrentMonth = currentDate.getMonth() + 1; 55 | let usageBasedCurrentYear = currentDate.getFullYear(); 56 | 57 | // If we're in the first few days of the month (before billing date), 58 | // consider the previous month as the current billing period 59 | if (currentDate.getDate() < usageBasedBillingDay) { 60 | usageBasedCurrentMonth = usageBasedCurrentMonth === 1 ? 12 : usageBasedCurrentMonth - 1; 61 | if (usageBasedCurrentMonth === 12) { 62 | usageBasedCurrentYear--; 63 | } 64 | } 65 | 66 | // Calculate previous month for usage-based pricing 67 | const usageBasedLastMonth = usageBasedCurrentMonth === 1 ? 12 : usageBasedCurrentMonth - 1; 68 | const usageBasedLastYear = usageBasedCurrentMonth === 1 ? usageBasedCurrentYear - 1 : usageBasedCurrentYear; 69 | 70 | // Collect data in parallel to speed up the process 71 | await Promise.all([ 72 | // Get Cursor usage stats 73 | fetchCursorStats(token) 74 | .then(stats => { 75 | report.cursorStats = stats; 76 | log('[Report] Successfully fetched cursor stats'); 77 | }) 78 | .catch(error => { 79 | report.errors.cursorStats = `Error fetching stats: ${error.message}`; 80 | log('[Report] Error fetching cursor stats: ' + error.message, true); 81 | }), 82 | 83 | // Get usage limit info 84 | getCurrentUsageLimit(token) 85 | .then(limitResponse => { 86 | report.usageLimitResponse = limitResponse; 87 | report.rawResponses.usageLimit = limitResponse; 88 | log('[Report] Successfully fetched usage limit info'); 89 | }) 90 | .catch(error => { 91 | report.errors.usageLimit = `Error fetching usage limit: ${error.message}`; 92 | log('[Report] Error fetching usage limit: ' + error.message, true); 93 | }), 94 | 95 | // Get premium usage directly 96 | axios.get('https://www.cursor.com/api/usage', { 97 | params: { user: userId }, 98 | headers: { Cookie: `WorkosCursorSessionToken=${token}` } 99 | }) 100 | .then(response => { 101 | report.premiumUsage = response.data; 102 | report.rawResponses.premiumUsage = response.data; 103 | log('[Report] Successfully fetched premium usage data'); 104 | }) 105 | .catch(error => { 106 | report.errors.premiumUsage = `Error fetching premium usage: ${error.message}`; 107 | log('[Report] Error fetching premium usage: ' + error.message, true); 108 | }), 109 | 110 | // Get team membership info 111 | checkTeamMembership(token, context) 112 | .then(teamInfo => { 113 | report.teamInfo = { 114 | isTeamMember: teamInfo.isTeamMember, 115 | teamId: teamInfo.teamId, 116 | userId: teamInfo.userId 117 | }; 118 | report.rawResponses.teamInfo = teamInfo; 119 | log('[Report] Successfully fetched team membership info'); 120 | 121 | // If user is a team member, fetch team usage 122 | if (teamInfo.isTeamMember && teamInfo.teamId) { 123 | return getTeamUsage(token, teamInfo.teamId) 124 | .then(teamUsage => { 125 | report.teamUsage = teamUsage; 126 | report.rawResponses.teamUsage = teamUsage; 127 | log('[Report] Successfully fetched team usage data'); 128 | }) 129 | .catch(error => { 130 | report.errors.teamUsage = `Error fetching team usage: ${error.message}`; 131 | log('[Report] Error fetching team usage: ' + error.message, true); 132 | }); 133 | } 134 | // Return a resolved promise if user is not a team member 135 | return Promise.resolve(); 136 | }) 137 | .catch(error => { 138 | report.errors.teamInfo = `Error checking team membership: ${error.message}`; 139 | log('[Report] Error checking team membership: ' + error.message, true); 140 | }), 141 | 142 | // Get current month invoice data 143 | axios.post('https://www.cursor.com/api/dashboard/get-monthly-invoice', { 144 | month: usageBasedCurrentMonth, 145 | year: usageBasedCurrentYear, 146 | includeUsageEvents: false 147 | }, { 148 | headers: { 149 | Cookie: `WorkosCursorSessionToken=${token}` 150 | } 151 | }) 152 | .then(response => { 153 | if (!report.rawResponses.monthlyInvoice) { 154 | report.rawResponses.monthlyInvoice = {}; 155 | } 156 | report.rawResponses.monthlyInvoice.current = response.data; 157 | log('[Report] Successfully fetched current month invoice data'); 158 | }) 159 | .catch(error => { 160 | report.errors.currentMonthInvoice = `Error fetching current month invoice: ${error.message}`; 161 | log('[Report] Error fetching current month invoice: ' + error.message, true); 162 | }), 163 | 164 | // Get last month invoice data 165 | axios.post('https://www.cursor.com/api/dashboard/get-monthly-invoice', { 166 | month: usageBasedLastMonth, 167 | year: usageBasedLastYear, 168 | includeUsageEvents: false 169 | }, { 170 | headers: { 171 | Cookie: `WorkosCursorSessionToken=${token}` 172 | } 173 | }) 174 | .then(response => { 175 | if (!report.rawResponses.monthlyInvoice) { 176 | report.rawResponses.monthlyInvoice = {}; 177 | } 178 | report.rawResponses.monthlyInvoice.last = response.data; 179 | log('[Report] Successfully fetched last month invoice data'); 180 | }) 181 | .catch(error => { 182 | report.errors.lastMonthInvoice = `Error fetching last month invoice: ${error.message}`; 183 | log('[Report] Error fetching last month invoice: ' + error.message, true); 184 | }) 185 | ]); 186 | 187 | log('[Report] All data collection tasks completed'); 188 | 189 | // Update logs with final entries 190 | report.logs = getLogHistory().reverse(); 191 | 192 | return saveReport(report, context); 193 | } catch (error: any) { 194 | report.errors.general = `General error: ${error.message}`; 195 | log('[Report] General error during report generation: ' + error.message, true); 196 | 197 | // Update logs with error entries 198 | report.logs = getLogHistory().reverse(); 199 | 200 | return saveReport(report, context); 201 | } 202 | } 203 | 204 | /** 205 | * Saves the report to a JSON file in the extension directory 206 | */ 207 | function saveReport(report: CursorReport, context: vscode.ExtensionContext): Promise<{ reportPath: string; success: boolean }> { 208 | try { 209 | const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); 210 | const filename = `cursor-stats-report-${timestamp}.json`; 211 | const reportPath = path.join(context.extensionPath, filename); 212 | 213 | // Pretty-print the JSON with 2-space indentation 214 | fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); 215 | 216 | log(`[Report] Report saved successfully to: ${reportPath}`); 217 | return Promise.resolve({ reportPath, success: true }); 218 | } catch (error: any) { 219 | log('[Report] Error saving report: ' + error.message, true); 220 | return Promise.resolve({ 221 | reportPath: '', 222 | success: false 223 | }); 224 | } 225 | } 226 | // Register the report generation command 227 | export const createReportCommand = vscode.commands.registerCommand('cursor-stats.createReport', async () => { 228 | log('[Command] Creating usage report...'); 229 | 230 | // Show progress notification 231 | await vscode.window.withProgress( 232 | { 233 | location: vscode.ProgressLocation.Notification, 234 | title: t('commands.createReportProgress'), 235 | cancellable: false 236 | }, 237 | async (progress) => { 238 | progress.report({ increment: 0, message: t('commands.gatheringData') }); 239 | 240 | try { 241 | const result = await generateReport(); 242 | progress.report({ increment: 100, message: t('commands.completed') }); 243 | 244 | if (result.success) { 245 | const folderPath = vscode.Uri.file(result.reportPath).with({ fragment: 'report' }); 246 | const fileName = result.reportPath.split(/[/\\]/).pop() || 'report.json'; 247 | const directoryPath = result.reportPath.substring(0, result.reportPath.length - fileName.length); 248 | 249 | const openOption = await vscode.window.showInformationMessage( 250 | t('commands.reportCreatedSuccessfully', { fileName }), 251 | t('commands.openFile'), 252 | t('commands.openFolder'), 253 | t('commands.openGitHubIssues') 254 | ); 255 | 256 | if (openOption === t('commands.openFile')) { 257 | const fileUri = vscode.Uri.file(result.reportPath); 258 | await vscode.commands.executeCommand('vscode.open', fileUri); 259 | } else if (openOption === t('commands.openFolder')) { 260 | const folderUri = vscode.Uri.file(directoryPath); 261 | await vscode.commands.executeCommand('revealFileInOS', folderUri); 262 | } else if (openOption === t('commands.openGitHubIssues')) { 263 | await vscode.env.openExternal(vscode.Uri.parse('https://github.com/Dwtexe/cursor-stats/issues/new')); 264 | } 265 | } else { 266 | vscode.window.showErrorMessage(t('errors.failedToCreateReport')); 267 | } 268 | } catch (error: any) { 269 | vscode.window.showErrorMessage(t('errors.errorCreatingReport', { error: error.message })); 270 | log('[Report] Error: ' + error.message, true); 271 | } 272 | } 273 | ); 274 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020", 8 | "DOM" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": true, /* enable all strict type-checking options */ 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedParameters": true, 16 | /* Additional Checks */ 17 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 18 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 19 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 20 | } 21 | } 22 | --------------------------------------------------------------------------------