@davidbrianethier/cc-usage 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # cc-usage
2
+
3
+ Claude.ai usage monitor for macOS menubar. Shows your 5-hour and weekly usage limits as text in the menubar.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # From npm
9
+ npm install -g @davidbrianethier/cc-usage
10
+
11
+ # Or from GitHub
12
+ npm install -g github:dbe/cc-usage
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ # First run - will prompt for sessionKey
19
+ cc-usage
20
+
21
+ # Re-authenticate
22
+ cc-usage --reauth
23
+
24
+ # Show help
25
+ cc-usage --help
26
+ ```
27
+
28
+ ## Getting Your Session Key
29
+
30
+ 1. Log into [claude.ai](https://claude.ai) in your browser
31
+ 2. Open DevTools: `Cmd+Option+I`
32
+ 3. Go to **Application** tab > **Cookies** > **claude.ai**
33
+ 4. Find `sessionKey` and copy its value
34
+ 5. Paste when prompted by cc-usage
35
+
36
+ ## Features
37
+
38
+ - **Menubar display**: Shows `45%|72%` (5-hour | 7-day usage)
39
+ - **Auto-refresh**: Updates every 10 seconds
40
+ - **Power-aware**: Pauses during system sleep
41
+ - **Auto-start**: Launches on login
42
+ - **Context menu**: Click for Reauth/Quit options
43
+
44
+ ## Requirements
45
+
46
+ - macOS 10.15+
47
+ - Node.js 18+
48
+
49
+ ## License
50
+
51
+ ISC
package/bin/cli.js ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, execSync } = require('child_process');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const fs = require('fs');
7
+
8
+ // Parse arguments
9
+ const args = process.argv.slice(2);
10
+ const showHelp = args.includes('--help') || args.includes('-h');
11
+ const forceReauth = args.includes('--reauth');
12
+
13
+ if (showHelp) {
14
+ console.log(`
15
+ cc-usage - Claude.ai Usage Monitor
16
+
17
+ Usage: cc-usage [options]
18
+
19
+ Options:
20
+ --help, -h Show this help message
21
+ --reauth Clear stored session and re-authenticate
22
+
23
+ First Run:
24
+ 1. Log into claude.ai in your browser
25
+ 2. Open DevTools (Cmd+Option+I) > Application > Cookies > claude.ai
26
+ 3. Copy the value of 'sessionKey' cookie
27
+ 4. Paste when prompted
28
+
29
+ The app will appear in your menubar showing usage limits.
30
+ `);
31
+ process.exit(0);
32
+ }
33
+
34
+ async function main() {
35
+ // Dynamic import for ES modules compatibility with keytar
36
+ const auth = require('../src/auth.js');
37
+
38
+ // Handle --reauth flag
39
+ if (forceReauth) {
40
+ await auth.clearSessionKey();
41
+ console.log('Session cleared. Please enter new session key.');
42
+ }
43
+
44
+ // Check if we have a session key
45
+ let hasKey = await auth.hasSessionKey();
46
+
47
+ if (!hasKey) {
48
+ console.log('\n=== cc-usage Setup ===\n');
49
+ console.log('To monitor your Claude.ai usage, you need to provide your session key.');
50
+ console.log('');
51
+ console.log('How to get your session key:');
52
+ console.log(' 1. Open https://claude.ai in your browser and log in');
53
+ console.log(' 2. Open DevTools: Cmd+Option+I (Mac) or F12 (Windows)');
54
+ console.log(' 3. Go to Application tab > Cookies > claude.ai');
55
+ console.log(' 4. Find "sessionKey" and copy its value');
56
+ console.log('');
57
+
58
+ const rl = readline.createInterface({
59
+ input: process.stdin,
60
+ output: process.stdout
61
+ });
62
+
63
+ const sessionKey = await new Promise((resolve) => {
64
+ rl.question('Enter sessionKey: ', (answer) => {
65
+ rl.close();
66
+ resolve(answer.trim());
67
+ });
68
+ });
69
+
70
+ if (!sessionKey) {
71
+ console.error('Error: Session key cannot be empty.');
72
+ process.exit(1);
73
+ }
74
+
75
+ await auth.setSessionKey(sessionKey);
76
+ console.log('\nSession key saved to keychain.');
77
+ console.log('Starting cc-usage...');
78
+ }
79
+
80
+ const electronPath = require('electron');
81
+ const appPath = path.join(__dirname, '..');
82
+
83
+ // macOS: clear quarantine attribute from Electron binary to avoid Gatekeeper issues
84
+ if (process.platform === 'darwin') {
85
+ const electronApp = path.join(appPath, 'node_modules', 'electron', 'dist', 'Electron.app');
86
+ if (fs.existsSync(electronApp)) {
87
+ try {
88
+ execSync(`xattr -cr "${electronApp}"`, { stdio: 'ignore' });
89
+ } catch (e) {
90
+ // Ignore errors - xattr may not be needed
91
+ }
92
+ }
93
+ }
94
+
95
+ const child = spawn(electronPath, [appPath], {
96
+ detached: true,
97
+ stdio: 'ignore'
98
+ });
99
+
100
+ child.unref();
101
+
102
+ console.log('cc-usage is now running in your menubar.');
103
+ process.exit(0);
104
+ }
105
+
106
+ main().catch((err) => {
107
+ console.error('Error:', err.message);
108
+ process.exit(1);
109
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@davidbrianethier/cc-usage",
3
+ "version": "1.0.0",
4
+ "description": "Claude.ai usage monitor for macOS menubar - shows 5-hour and weekly limits",
5
+ "main": "src/main.js",
6
+ "bin": {
7
+ "cc-usage": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "electron .",
11
+ "test": "node --test src/*.test.js"
12
+ },
13
+ "keywords": [
14
+ "claude",
15
+ "anthropic",
16
+ "usage",
17
+ "menubar",
18
+ "macos",
19
+ "tray",
20
+ "monitor"
21
+ ],
22
+ "author": "Brian Ethier",
23
+ "license": "ISC",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/dbe/cc-usage"
27
+ },
28
+ "homepage": "https://github.com/dbe/cc-usage#readme",
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "os": [
33
+ "darwin"
34
+ ],
35
+ "dependencies": {
36
+ "keytar": "^7.9.0"
37
+ },
38
+ "devDependencies": {
39
+ "electron": "^33.0.0"
40
+ }
41
+ }
package/src/api.js ADDED
@@ -0,0 +1,104 @@
1
+ const BASE_URL = 'https://claude.ai/api/organizations';
2
+
3
+ let cachedOrgId = null;
4
+
5
+ function buildHeaders(sessionKey) {
6
+ return {
7
+ 'accept': '*/*',
8
+ 'content-type': 'application/json',
9
+ 'anthropic-client-platform': 'web_claude_ai',
10
+ 'anthropic-client-version': '1.0.0',
11
+ 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
12
+ 'origin': 'https://claude.ai',
13
+ 'referer': 'https://claude.ai/settings/usage',
14
+ 'Cookie': `sessionKey=${sessionKey}`
15
+ };
16
+ }
17
+
18
+ async function fetchOrganizationId(sessionKey) {
19
+ if (cachedOrgId) return cachedOrgId;
20
+
21
+ try {
22
+ const response = await fetch(BASE_URL, {
23
+ method: 'GET',
24
+ headers: buildHeaders(sessionKey)
25
+ });
26
+
27
+ if (!response.ok) {
28
+ return null;
29
+ }
30
+
31
+ const orgs = await response.json();
32
+ if (orgs && orgs.length > 0) {
33
+ cachedOrgId = orgs[0].uuid;
34
+ return cachedOrgId;
35
+ }
36
+ return null;
37
+ } catch (error) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ async function fetchUsage(sessionKey) {
43
+ const errorResult = {
44
+ fiveHour: null,
45
+ sevenDay: null,
46
+ sevenDayOpus: null,
47
+ sevenDaySonnet: null,
48
+ error: null
49
+ };
50
+
51
+ try {
52
+ const orgId = await fetchOrganizationId(sessionKey);
53
+ if (!orgId) {
54
+ return { ...errorResult, error: '401' };
55
+ }
56
+
57
+ const response = await fetch(`${BASE_URL}/${orgId}/usage`, {
58
+ method: 'GET',
59
+ headers: buildHeaders(sessionKey)
60
+ });
61
+
62
+ if (!response.ok) {
63
+ if (response.status === 401 || response.status === 403) {
64
+ cachedOrgId = null;
65
+ return { ...errorResult, error: '401' };
66
+ }
67
+ return { ...errorResult, error: `HTTP ${response.status}` };
68
+ }
69
+
70
+ const data = await response.json();
71
+
72
+ return {
73
+ fiveHour: data.five_hour ? {
74
+ percent: data.five_hour.utilization,
75
+ resetsAt: data.five_hour.resets_at ? new Date(data.five_hour.resets_at) : null
76
+ } : null,
77
+ sevenDay: data.seven_day ? {
78
+ percent: data.seven_day.utilization,
79
+ resetsAt: data.seven_day.resets_at ? new Date(data.seven_day.resets_at) : null
80
+ } : null,
81
+ sevenDayOpus: data.seven_day_opus ? {
82
+ percent: data.seven_day_opus.utilization,
83
+ resetsAt: data.seven_day_opus.resets_at ? new Date(data.seven_day_opus.resets_at) : null
84
+ } : null,
85
+ sevenDaySonnet: data.seven_day_sonnet ? {
86
+ percent: data.seven_day_sonnet.utilization,
87
+ resetsAt: data.seven_day_sonnet.resets_at ? new Date(data.seven_day_sonnet.resets_at) : null
88
+ } : null,
89
+ error: null
90
+ };
91
+ } catch (error) {
92
+ return { ...errorResult, error: `Network error: ${error.message}` };
93
+ }
94
+ }
95
+
96
+ function clearCache() {
97
+ cachedOrgId = null;
98
+ }
99
+
100
+ module.exports = {
101
+ fetchOrganizationId,
102
+ fetchUsage,
103
+ clearCache
104
+ };
@@ -0,0 +1,143 @@
1
+ const { describe, it, mock, beforeEach } = require('node:test');
2
+ const assert = require('node:assert');
3
+
4
+ describe('api', () => {
5
+ let api;
6
+ let mockFetch;
7
+
8
+ beforeEach(() => {
9
+ // Reset module cache to allow fresh mocking
10
+ delete require.cache[require.resolve('./api.js')];
11
+
12
+ mockFetch = mock.fn();
13
+ global.fetch = mockFetch;
14
+ });
15
+
16
+ describe('fetchOrganizationId', () => {
17
+ it('returns organization ID on success', async () => {
18
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
19
+ ok: true,
20
+ status: 200,
21
+ json: () => Promise.resolve([{ uuid: 'org-123', name: 'Personal' }])
22
+ }));
23
+
24
+ api = require('./api.js');
25
+ const result = await api.fetchOrganizationId('sk-ant-test');
26
+
27
+ assert.strictEqual(result, 'org-123');
28
+ });
29
+
30
+ it('returns null on 401', async () => {
31
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
32
+ ok: false,
33
+ status: 401
34
+ }));
35
+
36
+ api = require('./api.js');
37
+ const result = await api.fetchOrganizationId('bad-key');
38
+
39
+ assert.strictEqual(result, null);
40
+ });
41
+ });
42
+
43
+ describe('fetchUsage', () => {
44
+ it('returns usage data on success', async () => {
45
+ // First call for org ID, second for usage
46
+ let callCount = 0;
47
+ mockFetch.mock.mockImplementation(() => {
48
+ callCount++;
49
+ if (callCount === 1) {
50
+ return Promise.resolve({
51
+ ok: true,
52
+ status: 200,
53
+ json: () => Promise.resolve([{ uuid: 'org-123' }])
54
+ });
55
+ }
56
+ return Promise.resolve({
57
+ ok: true,
58
+ status: 200,
59
+ json: () => Promise.resolve({
60
+ five_hour: { utilization: 45.5, resets_at: '2026-01-31T00:00:00Z' },
61
+ seven_day: { utilization: 68.2, resets_at: '2026-02-05T00:00:00Z' },
62
+ seven_day_opus: { utilization: 77.0, resets_at: '2026-02-05T00:00:00Z' },
63
+ seven_day_sonnet: { utilization: 30.0, resets_at: '2026-02-05T00:00:00Z' }
64
+ })
65
+ });
66
+ });
67
+
68
+ api = require('./api.js');
69
+ const result = await api.fetchUsage('sk-ant-test');
70
+
71
+ assert.strictEqual(result.error, null);
72
+ assert.strictEqual(result.fiveHour.percent, 45.5);
73
+ assert.strictEqual(result.sevenDay.percent, 68.2);
74
+ assert.strictEqual(result.sevenDayOpus.percent, 77.0);
75
+ assert.strictEqual(result.sevenDaySonnet.percent, 30.0);
76
+ });
77
+
78
+ it('returns error object on 401', async () => {
79
+ mockFetch.mock.mockImplementation(() => Promise.resolve({
80
+ ok: false,
81
+ status: 401
82
+ }));
83
+
84
+ api = require('./api.js');
85
+ const result = await api.fetchUsage('bad-key');
86
+
87
+ assert.strictEqual(result.error, '401');
88
+ assert.strictEqual(result.fiveHour, null);
89
+ });
90
+
91
+ it('returns error object on network error', async () => {
92
+ let callCount = 0;
93
+ mockFetch.mock.mockImplementation(() => {
94
+ callCount++;
95
+ if (callCount === 1) {
96
+ return Promise.resolve({
97
+ ok: true,
98
+ status: 200,
99
+ json: () => Promise.resolve([{ uuid: 'org-123' }])
100
+ });
101
+ }
102
+ return Promise.reject(new Error('Network error'));
103
+ });
104
+
105
+ api = require('./api.js');
106
+ const result = await api.fetchUsage('sk-ant-test');
107
+
108
+ assert.ok(result.error.includes('Network'));
109
+ assert.strictEqual(result.fiveHour, null);
110
+ });
111
+
112
+ it('handles missing optional fields gracefully', async () => {
113
+ let callCount = 0;
114
+ mockFetch.mock.mockImplementation(() => {
115
+ callCount++;
116
+ if (callCount === 1) {
117
+ return Promise.resolve({
118
+ ok: true,
119
+ status: 200,
120
+ json: () => Promise.resolve([{ uuid: 'org-123' }])
121
+ });
122
+ }
123
+ return Promise.resolve({
124
+ ok: true,
125
+ status: 200,
126
+ json: () => Promise.resolve({
127
+ five_hour: { utilization: 50, resets_at: null },
128
+ seven_day: { utilization: 60, resets_at: null }
129
+ // No opus or sonnet fields
130
+ })
131
+ });
132
+ });
133
+
134
+ api = require('./api.js');
135
+ const result = await api.fetchUsage('sk-ant-test');
136
+
137
+ assert.strictEqual(result.error, null);
138
+ assert.strictEqual(result.fiveHour.percent, 50);
139
+ assert.strictEqual(result.sevenDayOpus, null);
140
+ assert.strictEqual(result.sevenDaySonnet, null);
141
+ });
142
+ });
143
+ });
package/src/auth.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Auth module - Keychain-based session key storage
3
+ */
4
+
5
+ let keytar = require('keytar');
6
+
7
+ const SERVICE = 'cc-usage';
8
+ const ACCOUNT = 'sessionKey';
9
+
10
+ /**
11
+ * For testing: inject mock keytar
12
+ * @param {object} mock - Mock keytar object
13
+ */
14
+ function _setKeytar(mock) {
15
+ keytar = mock;
16
+ }
17
+
18
+ /**
19
+ * Get session key from keychain
20
+ * @returns {Promise<string|null>} Session key or null if not found
21
+ */
22
+ async function getSessionKey() {
23
+ return await keytar.getPassword(SERVICE, ACCOUNT);
24
+ }
25
+
26
+ /**
27
+ * Store session key in keychain
28
+ * @param {string} key - Session key to store
29
+ */
30
+ async function setSessionKey(key) {
31
+ await keytar.setPassword(SERVICE, ACCOUNT, key);
32
+ }
33
+
34
+ /**
35
+ * Remove session key from keychain
36
+ */
37
+ async function clearSessionKey() {
38
+ await keytar.deletePassword(SERVICE, ACCOUNT);
39
+ }
40
+
41
+ /**
42
+ * Check if session key exists in keychain
43
+ * @returns {Promise<boolean>} True if key exists
44
+ */
45
+ async function hasSessionKey() {
46
+ const key = await getSessionKey();
47
+ return key !== null;
48
+ }
49
+
50
+ module.exports = {
51
+ getSessionKey,
52
+ setSessionKey,
53
+ clearSessionKey,
54
+ hasSessionKey,
55
+ _setKeytar
56
+ };
@@ -0,0 +1,78 @@
1
+ const { describe, it, mock, beforeEach } = require('node:test');
2
+ const assert = require('node:assert');
3
+
4
+ // Mock keytar before requiring auth
5
+ const mockKeytar = {
6
+ getPassword: mock.fn(),
7
+ setPassword: mock.fn(),
8
+ deletePassword: mock.fn()
9
+ };
10
+
11
+ // Use dynamic import pattern to allow mocking
12
+ let auth;
13
+
14
+ describe('auth', () => {
15
+ beforeEach(() => {
16
+ mockKeytar.getPassword.mock.resetCalls();
17
+ mockKeytar.setPassword.mock.resetCalls();
18
+ mockKeytar.deletePassword.mock.resetCalls();
19
+ });
20
+
21
+ describe('getSessionKey', () => {
22
+ it('returns null when no key stored', async () => {
23
+ mockKeytar.getPassword.mock.mockImplementation(() => Promise.resolve(null));
24
+ const auth = require('./auth.js');
25
+ // Inject mock
26
+ auth._setKeytar(mockKeytar);
27
+ const result = await auth.getSessionKey();
28
+ assert.strictEqual(result, null);
29
+ });
30
+
31
+ it('returns stored key when present', async () => {
32
+ mockKeytar.getPassword.mock.mockImplementation(() => Promise.resolve('sk-ant-123'));
33
+ const auth = require('./auth.js');
34
+ auth._setKeytar(mockKeytar);
35
+ const result = await auth.getSessionKey();
36
+ assert.strictEqual(result, 'sk-ant-123');
37
+ });
38
+ });
39
+
40
+ describe('setSessionKey', () => {
41
+ it('stores key in keychain', async () => {
42
+ mockKeytar.setPassword.mock.mockImplementation(() => Promise.resolve());
43
+ const auth = require('./auth.js');
44
+ auth._setKeytar(mockKeytar);
45
+ await auth.setSessionKey('sk-ant-456');
46
+ assert.strictEqual(mockKeytar.setPassword.mock.calls.length, 1);
47
+ assert.deepStrictEqual(mockKeytar.setPassword.mock.calls[0].arguments, ['cc-usage', 'sessionKey', 'sk-ant-456']);
48
+ });
49
+ });
50
+
51
+ describe('clearSessionKey', () => {
52
+ it('removes key from keychain', async () => {
53
+ mockKeytar.deletePassword.mock.mockImplementation(() => Promise.resolve(true));
54
+ const auth = require('./auth.js');
55
+ auth._setKeytar(mockKeytar);
56
+ await auth.clearSessionKey();
57
+ assert.strictEqual(mockKeytar.deletePassword.mock.calls.length, 1);
58
+ });
59
+ });
60
+
61
+ describe('hasSessionKey', () => {
62
+ it('returns true when key exists', async () => {
63
+ mockKeytar.getPassword.mock.mockImplementation(() => Promise.resolve('sk-ant-789'));
64
+ const auth = require('./auth.js');
65
+ auth._setKeytar(mockKeytar);
66
+ const result = await auth.hasSessionKey();
67
+ assert.strictEqual(result, true);
68
+ });
69
+
70
+ it('returns false when no key', async () => {
71
+ mockKeytar.getPassword.mock.mockImplementation(() => Promise.resolve(null));
72
+ const auth = require('./auth.js');
73
+ auth._setKeytar(mockKeytar);
74
+ const result = await auth.hasSessionKey();
75
+ assert.strictEqual(result, false);
76
+ });
77
+ });
78
+ });
package/src/main.js ADDED
@@ -0,0 +1,97 @@
1
+ const { app, Menu, Tray, powerMonitor, nativeImage } = require('electron');
2
+ const { getSessionKey, clearSessionKey } = require('./auth.js');
3
+ const { fetchUsage, clearCache } = require('./api.js');
4
+
5
+ const POLL_INTERVAL = 10 * 1000;
6
+ const BACKOFF_DELAYS = [5000, 10000, 30000];
7
+
8
+ let tray = null;
9
+ let pollingInterval = null;
10
+ let retryCount = 0;
11
+ let isPaused = false;
12
+
13
+ function formatTitle(usage) {
14
+ if (!usage || usage.error) {
15
+ return usage?.error === '401' ? '⚠️ Auth' : '...';
16
+ }
17
+ const h5 = Math.round(usage.fiveHour?.percent ?? 0);
18
+ const d7 = Math.round(usage.sevenDay?.percent ?? 0);
19
+ return `${h5}%|${d7}%`;
20
+ }
21
+
22
+ async function updateUsage() {
23
+ if (isPaused) return;
24
+
25
+ const sessionKey = await getSessionKey();
26
+ if (!sessionKey) {
27
+ tray.setTitle(formatTitle({ error: '401' }));
28
+ return;
29
+ }
30
+
31
+ const usage = await fetchUsage(sessionKey);
32
+
33
+ if (usage.error === '401') {
34
+ await clearSessionKey();
35
+ clearCache();
36
+ tray.setTitle(formatTitle(usage));
37
+ stopPolling();
38
+ return;
39
+ }
40
+
41
+ if (usage.error) {
42
+ retryCount = Math.min(retryCount + 1, BACKOFF_DELAYS.length - 1);
43
+ tray.setTitle(formatTitle(usage));
44
+ setTimeout(updateUsage, BACKOFF_DELAYS[retryCount]);
45
+ return;
46
+ }
47
+
48
+ retryCount = 0;
49
+ tray.setTitle(formatTitle(usage));
50
+ }
51
+
52
+ function startPolling() {
53
+ if (pollingInterval) return;
54
+ isPaused = false;
55
+ updateUsage();
56
+ pollingInterval = setInterval(updateUsage, POLL_INTERVAL);
57
+ }
58
+
59
+ function stopPolling() {
60
+ if (pollingInterval) {
61
+ clearInterval(pollingInterval);
62
+ pollingInterval = null;
63
+ }
64
+ }
65
+
66
+ function createContextMenu() {
67
+ return Menu.buildFromTemplate([
68
+ {
69
+ label: 'Reauth',
70
+ click: async () => {
71
+ await clearSessionKey();
72
+ clearCache();
73
+ app.quit();
74
+ }
75
+ },
76
+ { type: 'separator' },
77
+ { label: 'Quit', click: () => app.quit() }
78
+ ]);
79
+ }
80
+
81
+ app.whenReady().then(() => {
82
+ // 1x1 transparent PNG to hide icon, show only title text
83
+ const emptyIcon = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
84
+
85
+ tray = new Tray(emptyIcon);
86
+ tray.setTitle('...');
87
+ tray.setContextMenu(createContextMenu());
88
+
89
+ powerMonitor.on('suspend', () => { isPaused = true; });
90
+ powerMonitor.on('resume', () => { isPaused = false; updateUsage(); });
91
+
92
+ app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true });
93
+
94
+ startPolling();
95
+ });
96
+
97
+ app.on('window-all-closed', (e) => e.preventDefault());