├── .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 | [](https://marketplace.visualstudio.com/items?itemName=Dwtexe.cursor-stats) [](https://marketplace.visualstudio.com/items?itemName=Dwtexe.cursor-stats) [](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 | Default UI |
58 | Custom Currency |
59 |
60 |
61 |  |
62 |  |
63 |
64 |
65 | Progress Bars |
66 | Settings |
67 |
68 |
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 |
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 |
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 |
--------------------------------------------------------------------------------