cc-caffeine 0.2.0 → 0.3.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/CLAUDE.md CHANGED
@@ -14,6 +14,7 @@ The system consists of a modular architecture with the following components:
14
14
  4. **src/server.js** - Handles server process management and Electron integration
15
15
  5. **src/system-tray.js** - Manages system tray functionality and sleep prevention
16
16
  6. **src/electron.js** - Wraps Electron-specific functionality and provides cross-platform support
17
+ 7. **src/config.js** - Reads user configuration from `~/.claude/plugins/cc-caffeine/config.json`
17
18
 
18
19
  ### User Commands
19
20
 
@@ -27,7 +28,7 @@ The system consists of a modular architecture with the following components:
27
28
  - Cross-platform support (Linux, macOS, Windows)
28
29
  - Headless Electron system tray (no windows, only system tray)
29
30
  - JSON file for session persistence with proper-lockfile for concurrency
30
- - Automatic session timeout (15 minutes of inactivity)
31
+ - Configurable session timeout (default: 15 minutes of inactivity)
31
32
  - Auto-server startup when not running
32
33
  - Multiple concurrent session support
33
34
  - Real-time status monitoring
@@ -35,6 +36,24 @@ The system consists of a modular architecture with the following components:
35
36
  - Native sleep prevention using Electron's powerSaveBlocker API
36
37
  - Hidden from macOS dock using app.dock.hide()
37
38
 
39
+ ## Configuration
40
+
41
+ User configuration is stored at `~/.claude/plugins/cc-caffeine/config.json`. All settings are optional and have sensible defaults.
42
+
43
+ ```json
44
+ {
45
+ "session_timeout_minutes": 15,
46
+ "icon_theme": "orange"
47
+ }
48
+ ```
49
+
50
+ ### Options
51
+
52
+ | Setting | Default | Description |
53
+ |---------|---------|-------------|
54
+ | `session_timeout_minutes` | `15` | Minutes of inactivity before a session expires |
55
+ | `icon_theme` | `"orange"` | Tray icon theme: `"orange"` (colored) or `"monochrome"` (black/white, auto-adapts to macOS dark mode) |
56
+
38
57
  ## Technical Stack
39
58
 
40
59
  - **Node.js 14+** - Runtime environment with modern JavaScript features
@@ -177,7 +196,7 @@ Sessions are removed automatically after 15 minutes of inactivity.
177
196
 
178
197
  ## Session Management
179
198
 
180
- - Sessions auto-expire after 15 minutes of inactivity
199
+ - Sessions auto-expire after the configured timeout (default: 15 minutes of inactivity)
181
200
  - Automatic cleanup of expired sessions during every add/remove operation
182
201
  - Server polls JSON file every 10 seconds for active sessions (with file locking)
183
202
  - Multiple sessions can be active simultaneously
@@ -235,9 +254,11 @@ npm run format # Format code with Prettier (if installed)
235
254
  The application uses CommonJS modules with clear dependency hierarchy:
236
255
 
237
256
  - `caffeine.js` imports from `src/commands.js` and `src/server.js`
238
- - `src/commands.js` imports from `src/session.js`
257
+ - `src/commands.js` imports from `src/session.js` and `src/config.js`
239
258
  - `src/server.js` imports from `src/session.js`, `src/system-tray.js`, and `src/electron.js`
240
- - `src/system-tray.js` imports from `src/session.js` and `src/electron.js`
259
+ - `src/system-tray.js` imports from `src/session.js`, `src/electron.js`, and `src/config.js`
260
+ - `src/session.js` imports from `src/config.js`
261
+ - `src/config.js` reads `~/.claude/plugins/cc-caffeine/config.json`
241
262
  - `src/electron.js` provides Electron functionality on-demand
242
263
 
243
264
  ## Sleep Prevention
@@ -258,13 +279,19 @@ src/
258
279
  └── server.js - Server process management and Electron integration
259
280
  └── system-tray.js - System tray functionality and sleep prevention
260
281
  └── electron.js - Electron-specific functionality wrapper
282
+ └── config.js - User configuration reader
261
283
  package.json - Node.js dependencies and scripts
262
284
  icon-coffee-full.png - Active caffeine icon (PNG)
263
285
  icon-coffee-empty.png- Inactive caffeine icon (PNG)
264
286
  icon-coffee-full.svg - Active caffeine icon (SVG)
265
287
  icon-coffee-empty.svg- Inactive caffeine icon (SVG)
288
+ icon-coffee-full-mono.png - Active caffeine icon monochrome (PNG)
289
+ icon-coffee-empty-mono.png- Inactive caffeine icon monochrome (PNG)
290
+ icon-coffee-full-mono.svg - Active caffeine icon monochrome (SVG)
291
+ icon-coffee-empty-mono.svg- Inactive caffeine icon monochrome (SVG)
266
292
  ~/.claude/plugins/cc-caffeine/
267
293
  └── sessions.json - JSON file with session data
294
+ └── config.json - User configuration (optional)
268
295
  ```
269
296
 
270
297
  **Modular architecture benefits:**
package/README.md CHANGED
@@ -15,6 +15,8 @@ Tired of your laptop going to sleep during that perfect coding session because y
15
15
  - 🚴‍♂️ Pedal to the coworking space while maintaining your active connection
16
16
  - 📱 Respond to your girlfriend calls during work hours, without Claude Code interruptions
17
17
 
18
+ <img width="4032" height="1152" alt="image" src="https://github.com/user-attachments/assets/e1db7f4c-bd49-4ec5-8da4-2595c7f9b80a" />
19
+
18
20
  ## 🎯 Installation
19
21
 
20
22
  ```bash
@@ -22,6 +24,12 @@ Tired of your laptop going to sleep during that perfect coding session because y
22
24
  /plugin install cc-caffeine@samber
23
25
  ```
24
26
 
27
+ *cc-caffine status*:
28
+
29
+ ![](./assets/icon-coffee-empty.png) - Claude Code is idle
30
+
31
+ ![](./assets/icon-coffee-full.png) - Claude Code is working hard
32
+
25
33
  ## 🌟 The Nomad Developer Manifesto
26
34
 
27
35
  > "I'll never choose between coding and traveling again. With cc-caffeine, I can do both. My laptop will never sleep while I traverse cities in 5G, my Claude Code will stay connected in my backpack, and my productivity will soar. The future of mobile development is here, and it smells like coffee."
@@ -0,0 +1,10 @@
1
+ <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Coffee cup outline - smaller cup with more margin -->
3
+ <path d="M8 10 L8 22 L10 24 L22 24 L24 22 L24 10 Z"
4
+ fill="none" stroke="#000000" stroke-width="2" stroke-linejoin="miter"/>
5
+ <!-- Coffee handle - smaller handle with margin -->
6
+ <path d="M24 14 L28 14 L28 18 L24 18 Z"
7
+ fill="none" stroke="#000000" stroke-width="2" stroke-linejoin="miter"/>
8
+ <!-- Coffee residue - smaller with more margin -->
9
+ <line x1="10" y1="22" x2="22" y2="22" stroke="#000000" stroke-width="1" opacity="0.5"/>
10
+ </svg>
Binary file
@@ -0,0 +1,15 @@
1
+ <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Coffee cup - smaller cup with more margin -->
3
+ <path d="M8 10 L8 22 L10 24 L22 24 L24 22 L24 10 Z"
4
+ fill="#000000"/>
5
+ <!-- Coffee liquid - smaller liquid with margin -->
6
+ <path d="M10 14 L10 18 L12 20 L20 20 L22 18 L22 14 Z"
7
+ fill="#333333"/>
8
+ <!-- Coffee handle - smaller handle with margin -->
9
+ <path d="M24 14 L28 14 L28 18 L24 18 Z"
10
+ fill="#000000"/>
11
+ <!-- Steam - simple flat lines with more margin -->
12
+ <line x1="12" y1="6" x2="12" y2="2" stroke="#000000" stroke-width="1" opacity="0.7"/>
13
+ <line x1="16" y1="6" x2="16" y2="2" stroke="#000000" stroke-width="1" opacity="0.7"/>
14
+ <line x1="20" y1="6" x2="20" y2="2" stroke="#000000" stroke-width="1" opacity="0.7"/>
15
+ </svg>
package/caffeine.js CHANGED
@@ -11,7 +11,13 @@ const fs = require('fs');
11
11
  * All functionality has been split into separate modules for better organization.
12
12
  */
13
13
 
14
- const { handleCaffeinate, handleUncaffeinate, handleStatus, handleVersion, handleUsage } = require('./src/commands');
14
+ const {
15
+ handleCaffeinate,
16
+ handleUncaffeinate,
17
+ handleStatus,
18
+ handleVersion,
19
+ handleUsage
20
+ } = require('./src/commands');
15
21
  const { handleServer } = require('./src/server');
16
22
 
17
23
  const CONFIG_DIR = path.join(os.homedir(), '.claude', 'plugins', 'cc-caffeine');
@@ -36,23 +42,23 @@ const main = async () => {
36
42
  const command = process.argv[2];
37
43
 
38
44
  switch (command) {
39
- case 'caffeinate':
40
- await handleCaffeinate();
41
- break;
42
- case 'uncaffeinate':
43
- await handleUncaffeinate();
44
- break;
45
- case 'server':
46
- await handleServer();
47
- break;
48
- case 'status':
49
- await handleStatus();
50
- break;
51
- case 'version':
52
- await handleVersion();
53
- break;
54
- default:
55
- await handleUsage();
45
+ case 'caffeinate':
46
+ await handleCaffeinate();
47
+ break;
48
+ case 'uncaffeinate':
49
+ await handleUncaffeinate();
50
+ break;
51
+ case 'server':
52
+ await handleServer();
53
+ break;
54
+ case 'status':
55
+ await handleStatus();
56
+ break;
57
+ case 'version':
58
+ await handleVersion();
59
+ break;
60
+ default:
61
+ await handleUsage();
56
62
  }
57
63
  };
58
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-caffeine",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Prevents your computer from sleeping while Claude Code works hard",
5
5
  "main": "caffeine.js",
6
6
  "bin": {
@@ -35,11 +35,11 @@
35
35
  },
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
- "electron": "^38.2.2",
38
+ "electron": "^40.1.0",
39
39
  "proper-lockfile": "^4.1.2"
40
40
  },
41
41
  "devDependencies": {
42
- "eslint": "^9.37.0",
42
+ "eslint": "^10.0.1",
43
43
  "prettier": "^3.6.2"
44
44
  },
45
45
  "engines": {
@@ -54,4 +54,4 @@
54
54
  "linux",
55
55
  "win32"
56
56
  ]
57
- }
57
+ }
package/src/commands.js CHANGED
@@ -7,9 +7,14 @@
7
7
  const path = require('path');
8
8
  const fs = require('fs');
9
9
 
10
- const { addSessionWithLock, removeSessionWithLock, getActiveSessionsWithLock } = require('./session');
10
+ const {
11
+ addSessionWithLock,
12
+ removeSessionWithLock,
13
+ getActiveSessionsWithLock
14
+ } = require('./session');
11
15
  const { isServerRunningWithLock } = require('./pid');
12
16
  const { runServerProcessIfNotStarted } = require('./server');
17
+ const { getConfig } = require('./config');
13
18
 
14
19
  /**
15
20
  * Handle session commands with JSON input from Claude Code hooks
@@ -132,7 +137,9 @@ const handleStatus = async () => {
132
137
  });
133
138
  }
134
139
 
135
- console.error('\nSession timeout: 15 minutes of inactivity');
140
+ console.error(
141
+ `\nSession timeout: ${getConfig().session_timeout_minutes} minutes of inactivity`
142
+ );
136
143
  } catch (error) {
137
144
  console.error('Error getting status:', error.message);
138
145
  process.exit(1);
@@ -150,7 +157,9 @@ const handleUsage = () => {
150
157
  console.error(' uncaffeinate [session_id] - Disable caffeine for current session');
151
158
  console.error(' server - Start caffeine server with system tray');
152
159
  console.error(' status - Show current status and active sessions');
153
- console.error(' version - Show version information from package.json and plugin.json');
160
+ console.error(
161
+ ' version - Show version information from package.json and plugin.json'
162
+ );
154
163
  process.exit(1);
155
164
  };
156
165
 
package/src/config.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Config module - Reads user configuration from config.json
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ const CONFIG_DIR = path.join(os.homedir(), '.claude', 'plugins', 'cc-caffeine');
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
+
12
+ const DEFAULTS = {
13
+ session_timeout_minutes: 15,
14
+ icon_theme: 'orange' // 'orange' | 'monochrome'
15
+ };
16
+
17
+ let cachedConfig = null;
18
+
19
+ const getConfig = () => {
20
+ if (cachedConfig) {
21
+ return cachedConfig;
22
+ }
23
+
24
+ let userConfig = {};
25
+ try {
26
+ if (fs.existsSync(CONFIG_FILE)) {
27
+ userConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
28
+ }
29
+ } catch (error) {
30
+ console.error('Warning: Failed to read config file, using defaults:', error.message);
31
+ }
32
+
33
+ cachedConfig = { ...DEFAULTS, ...userConfig };
34
+ return cachedConfig;
35
+ };
36
+
37
+ module.exports = { getConfig };
package/src/pid.js CHANGED
@@ -18,7 +18,7 @@ const lockfile = require('proper-lockfile');
18
18
  const CONFIG_DIR = path.join(os.homedir(), '.claude', 'plugins', 'cc-caffeine');
19
19
  const PID_FILE = path.join(CONFIG_DIR, 'server.pid');
20
20
 
21
- const withPidLock = async (fn) => {
21
+ const withPidLock = async fn => {
22
22
  // create if not exists
23
23
  try {
24
24
  const fd = fs.openSync(PID_FILE, 'wx');
@@ -138,8 +138,8 @@ const validatePid = async pid => {
138
138
  const isWindows = os.platform() === 'win32';
139
139
  const psCommand = isWindows
140
140
  ? spawn('wmic', ['process', 'where', `processid=${pid}`, 'get', 'commandline'], {
141
- stdio: 'pipe'
142
- })
141
+ stdio: 'pipe'
142
+ })
143
143
  : spawn('ps', ['-p', pid, '-o', 'command='], { stdio: 'pipe' });
144
144
 
145
145
  let output = '';
@@ -157,7 +157,8 @@ const validatePid = async pid => {
157
157
  const commandLine = output.trim().toLowerCase();
158
158
  for (const line of commandLine.split('\n')) {
159
159
  // Check if command line contains both "caffeine" and "server"
160
- const isCaffeineServer = line.includes('caffeine server') || line.includes('caffeine.js server');
160
+ const isCaffeineServer =
161
+ line.includes('caffeine server') || line.includes('caffeine.js server');
161
162
  const isElectron = line.includes('electron');
162
163
 
163
164
  if (isCaffeineServer && isElectron) {
package/src/session.js CHANGED
@@ -2,10 +2,11 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const lockfile = require('proper-lockfile');
5
+ const { getConfig } = require('./config');
5
6
 
6
7
  const CONFIG_DIR = path.join(os.homedir(), '.claude', 'plugins', 'cc-caffeine');
7
8
  const SESSIONS_FILE = path.join(CONFIG_DIR, 'sessions.json');
8
- const SESSION_TIMEOUT = 15 * 60 * 1000; // 15 minutes
9
+ const getSessionTimeout = () => getConfig().session_timeout_minutes * 60 * 1000;
9
10
  const MAX_RETRIES = 10;
10
11
 
11
12
  const initSessionsFile = async () => {
@@ -66,7 +67,7 @@ const addSessionWithLock = async sessionId => {
66
67
  const lastActivity = new Date(sessionData.last_activity);
67
68
  const timeDiff = nowDate - lastActivity;
68
69
 
69
- if (timeDiff >= SESSION_TIMEOUT) {
70
+ if (timeDiff >= getSessionTimeout()) {
70
71
  delete data.sessions[existingSessionId];
71
72
  removedCount++;
72
73
  }
@@ -124,7 +125,7 @@ const removeSessionWithLock = async sessionId => {
124
125
  const lastActivity = new Date(sessionData.last_activity);
125
126
  const timeDiff = nowDate - lastActivity;
126
127
 
127
- if (timeDiff >= SESSION_TIMEOUT) {
128
+ if (timeDiff >= getSessionTimeout()) {
128
129
  delete data.sessions[existingSessionId];
129
130
  cleanedCount++;
130
131
  }
@@ -169,7 +170,7 @@ const getActiveSessionsWithLock = async () => {
169
170
  const lastActivity = new Date(sessionData.last_activity);
170
171
  const timeDiff = now - lastActivity;
171
172
 
172
- if (timeDiff < SESSION_TIMEOUT) {
173
+ if (timeDiff < getSessionTimeout()) {
173
174
  activeSessions.push({
174
175
  id: sessionId,
175
176
  ...sessionData
@@ -200,7 +201,7 @@ const cleanupExpiredSessionsWithLock = async () => {
200
201
  const lastActivity = new Date(sessionData.last_activity);
201
202
  const timeDiff = now - lastActivity;
202
203
 
203
- if (timeDiff >= SESSION_TIMEOUT) {
204
+ if (timeDiff >= getSessionTimeout()) {
204
205
  delete data.sessions[sessionId];
205
206
  removedCount++;
206
207
  }
@@ -6,6 +6,7 @@ const path = require('path');
6
6
 
7
7
  const { getActiveSessionsWithLock, cleanupExpiredSessionsWithLock } = require('./session');
8
8
  const { getElectron } = require('./electron');
9
+ const { getConfig } = require('./config');
9
10
  const { removePidFileWithLock } = require('./pid');
10
11
  const package = require('../package.json');
11
12
 
@@ -15,10 +16,19 @@ let trayState = null;
15
16
  * Create icon for system tray
16
17
  */
17
18
  const createIcon = isActive => {
18
- const icon = isActive ? '../assets/icon-coffee-full.png' : '../assets/icon-coffee-empty.png';
19
+ const { icon_theme } = getConfig();
20
+ const isMono = icon_theme === 'monochrome';
21
+ const suffix = isMono ? '-mono' : '';
22
+ const icon = isActive
23
+ ? `../assets/icon-coffee-full${suffix}.png`
24
+ : `../assets/icon-coffee-empty${suffix}.png`;
19
25
  const iconPath = path.join(__dirname, icon);
20
26
  const { nativeImage } = getElectron();
21
- return nativeImage.createFromPath(iconPath);
27
+ const image = nativeImage.createFromPath(iconPath);
28
+ if (isMono && process.platform === 'darwin') {
29
+ image.setTemplateImage(true);
30
+ }
31
+ return image;
22
32
  };
23
33
 
24
34
  /**
@@ -54,13 +64,13 @@ const createSystemTray = () => {
54
64
  {
55
65
  label: 'Github',
56
66
  click: () => {
57
- getElectron().shell.openExternal('https://github.com/samber/cc-caffeine')
67
+ getElectron().shell.openExternal('https://github.com/samber/cc-caffeine');
58
68
  }
59
69
  },
60
70
  {
61
71
  label: '💖 Sponsor',
62
72
  click: () => {
63
- getElectron().shell.openExternal('https://github.com/sponsors/samber')
73
+ getElectron().shell.openExternal('https://github.com/sponsors/samber');
64
74
  }
65
75
  },
66
76
  {