clocktopus 1.0.6 → 1.1.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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  CLI-based time-tracking automation for Clockify with idle monitoring, Jira integration, Google Calendar sync, and a web dashboard.
8
8
 
9
- ## Quick Start (Dashboard User)
9
+ ## Quick Start
10
10
 
11
11
  Most users only need the dashboard — a web UI to manage timers, connect integrations, and monitor idle time.
12
12
 
@@ -45,14 +45,20 @@ Open [http://localhost:4001](http://localhost:4001) in your browser.
45
45
 
46
46
  That's it. Start/stop timers from the Home tab.
47
47
 
48
- ### Dashboard Commands
49
-
50
- | Command | Description |
51
- | ----------------------- | ------------------------------------ |
52
- | `clocktopus dash` | Start dashboard (foreground) |
53
- | `clocktopus serve` | Start dashboard as background daemon |
54
- | `clocktopus serve:stop` | Stop the dashboard daemon |
55
- | `clocktopus serve:logs` | View dashboard daemon logs |
48
+ ### Commands
49
+
50
+ | Command | Description |
51
+ | ------------------------- | --------------------------------------- |
52
+ | `clocktopus dash` | Start dashboard (foreground) |
53
+ | `clocktopus serve` | Start dashboard as background daemon |
54
+ | `clocktopus serve:stop` | Stop the dashboard daemon |
55
+ | `clocktopus serve:logs` | View dashboard daemon logs |
56
+ | `clocktopus start` | Start a timer (interactive) |
57
+ | `clocktopus stop` | Stop the current timer |
58
+ | `clocktopus status` | Check timer status |
59
+ | `clocktopus monitor` | Start idle monitor as background daemon |
60
+ | `clocktopus monitor:stop` | Stop the idle monitor |
61
+ | `clocktopus monitor:logs` | View idle monitor logs |
56
62
 
57
63
  ### Desktop App (macOS)
58
64
 
@@ -68,9 +74,7 @@ The dashboard server must be running (`clocktopus serve`). See [desktop/README.m
68
74
 
69
75
  ---
70
76
 
71
- ## Power User Guide
72
-
73
- For CLI-based workflows, scripting, and advanced features.
77
+ ## Development
74
78
 
75
79
  ### Install from Source
76
80
 
@@ -81,105 +85,32 @@ bun install
81
85
  bun run build
82
86
  ```
83
87
 
84
- ### Local Development
88
+ ### Local Commands
85
89
 
86
90
  When running from source, use `bun run clock` instead of `clocktopus`:
87
91
 
88
92
  ```bash
89
- # Build first
90
- bun run build
93
+ bun run build # Build TypeScript
91
94
 
92
- # Dashboard
93
95
  bun run dashboard # Start dashboard (foreground)
94
-
95
- # Timer
96
96
  bun run clock start "Task" # Start a timer
97
97
  bun run clock start -j PROJ-1 # Start with Jira ticket
98
98
  bun run clock stop # Stop timer
99
99
  bun run clock status # Check timer status
100
100
 
101
- # Monitor
102
101
  bun run monitor # Start idle monitor (PM2 daemon)
103
102
  bun run monitor:stop # Stop monitor
104
103
  bun run monitor:restart # Restart monitor
105
- bun run monitor:status # Check monitor status
106
104
  bun run monitor:logs # View monitor logs
107
105
 
108
- # Google Calendar
109
106
  bun run google-auth # Authenticate Google account
110
107
  bun run log-calendar -t # Log today's events
111
-
112
- # Database
113
108
  bun run db:cleanup # Clean old session logs
114
109
  ```
115
110
 
116
- ### CLI Commands
117
-
118
- #### Timer Management
119
-
120
- ```bash
121
- # Start a timer (interactive project selection)
122
- clocktopus start "Task description"
123
-
124
- # Start with a Jira ticket (auto-fetches ticket title)
125
- clocktopus start -j TICKET-123
126
-
127
- # Stop the current timer
128
- clocktopus stop
129
-
130
- # Check timer status
131
- clocktopus status
132
- ```
133
-
134
- #### Idle Monitor
135
-
136
- Automatically stops timers when you're idle (5 min) or lock your screen, and restarts when you're back.
137
-
138
- ```bash
139
- # Run in foreground
140
- clocktopus monitor
141
-
142
- # Or manage via dashboard UI (Start/Stop/Restart buttons)
143
- ```
144
-
145
- The dashboard's Idle Monitor buttons use PM2 under the hood:
146
-
147
- | Action | What it does |
148
- | ----------- | ------------------------------ |
149
- | **Start** | Launches monitor as PM2 daemon |
150
- | **Stop** | Stops the monitor daemon |
151
- | **Restart** | Restarts after code changes |
152
-
153
- #### Google Calendar Integration
154
-
155
- Log Google Calendar events as Clockify time entries.
156
-
157
- ```bash
158
- # Authenticate (one-time)
159
- clocktopus google-auth
160
-
161
- # Log events for a date range
162
- clocktopus log-calendar -s 2025-07-21 -e 2025-07-22
163
-
164
- # Log today's events
165
- clocktopus log-calendar -t
166
- ```
167
-
168
- For each event, you'll be prompted to select a Clockify project. Selections are cached by event name for recurring meetings.
169
-
170
- #### Database Cleanup
171
-
172
- ```bash
173
- # Delete session logs older than 5 days (default)
174
- clocktopus db:cleanup
175
-
176
- # Delete logs older than N days
177
- clocktopus db:cleanup 10
178
- ```
179
-
180
111
  ### Configuration
181
112
 
182
- All configuration is stored in a local SQLite database (`data/sessions.db`) and managed through the dashboard Settings tab. No `.env` file is needed.
113
+ All configuration is stored in a local SQLite database and managed through the dashboard Settings tab.
183
114
 
184
115
  | Setting | How to configure |
185
116
  | ---------------- | ------------------------------------------------ |
@@ -188,56 +119,23 @@ All configuration is stored in a local SQLite database (`data/sessions.db`) and
188
119
  | Jira (API token) | Dashboard > Settings > "or use API token" |
189
120
  | Google Calendar | Dashboard > Settings > Click "Connect Google" |
190
121
 
191
- #### OAuth Architecture
192
-
193
- - **Jira**: OAuth tokens are exchanged through a [Cloudflare Worker proxy](docs/atlassian-proxy-flow.md) that holds the client secret securely. Users just click Connect.
194
- - **Google**: Uses a Desktop-type OAuth client. Credentials are handled transparently.
195
- - **Clockify**: Each user provides their own API key.
196
-
197
- #### Environment Variables (Optional Override)
198
-
199
- Power users can override credentials via environment variables or a `.env` file:
200
-
201
- ```
202
- CLOCKIFY_API_KEY="your_key"
203
- ATLASSIAN_CLIENT_ID="your_id"
204
- ATLASSIAN_CLIENT_SECRET="your_secret"
205
- GOOGLE_CLIENT_ID="your_id"
206
- GOOGLE_CLIENT_SECRET="your_secret"
207
- ```
208
-
209
- The app checks the database first, then falls back to environment variables.
122
+ OAuth for Jira and Google is handled transparently through a [Cloudflare Worker proxy](docs/atlassian-proxy-flow.md) — no client credentials needed from users.
210
123
 
211
- ### Local Project Filtering (CLI only)
124
+ ### Project Structure
212
125
 
213
- On first `clocktopus start`, all projects are saved to `data/local-projects.json`. Edit this file to keep only your frequently used projects:
214
-
215
- ```json
216
- [
217
- { "id": "671b783fbd91bc5e5ddcb944", "name": "Project A" },
218
- { "id": "another_id", "name": "Project B" }
219
- ]
220
126
  ```
221
-
222
- ### Shell Aliases
223
-
224
- For quick access, add to `~/.zshrc`:
225
-
226
- ```bash
227
- CLOCKTOPUS_PATH="$HOME/Projects/Personal/clocktopus"
228
-
229
- clockto() {
230
- cd "$CLOCKTOPUS_PATH" || return
231
- bun run "$@"
232
- }
233
-
234
- alias cbuild="clockto build"
235
- alias cstart="clockto clock start"
236
- alias cstop="clockto clock stop"
237
- alias mstart="clockto monitor"
238
- alias mstop="clockto monitor:stop"
239
- alias mrestart="clockto monitor:restart"
240
- alias mlogs="clockto monitor:logs"
127
+ clocktopus/
128
+ ├── index.ts # CLI entry point (Commander)
129
+ ├── clockify.ts # Clockify API client
130
+ ├── lib/ # Core libraries (db, auth, credentials)
131
+ ├── dashboard/ # Web dashboard (Hono server)
132
+ │ ├── server.ts # Dashboard server
133
+ │ ├── views.ts # HTML/CSS/JS (single-page app)
134
+ │ └── routes/ # API routes
135
+ ├── desktop/ # Tauri macOS menu bar app
136
+ ├── proxy/ # Cloudflare Worker (OAuth proxy)
137
+ ├── scripts/ # Google auth & calendar scripts
138
+ └── data/ # SQLite DB & config (gitignored)
241
139
  ```
242
140
 
243
141
  ---
@@ -246,37 +144,18 @@ alias mlogs="clockto monitor:logs"
246
144
 
247
145
  ### No notifications on macOS
248
146
 
249
- Go to **System Settings > Notifications** and ensure **terminal-notifier** (or your terminal app) has notifications enabled.
147
+ Go to **System Settings > Notifications** and ensure **terminal-notifier** has notifications enabled.
250
148
 
251
149
  ### Monitor not detecting display off
252
150
 
253
- The idle monitor detects screen lock and system idle (5 min). If your Mac's display turns off without locking, enable **Require password immediately** in System Settings > Lock Screen.
151
+ Enable **Require password immediately** in System Settings > Lock Screen.
254
152
 
255
- ### Linux Requirements
153
+ ### Linux
256
154
 
257
155
  ```bash
258
156
  apt install libxss-dev pkg-config build-essential
259
157
  ```
260
158
 
261
- ---
262
-
263
- ## Project Structure
264
-
265
- ```
266
- clocktopus/
267
- ├── index.ts # CLI entry point (Commander)
268
- ├── clockify.ts # Clockify API client
269
- ├── lib/ # Core libraries (db, auth, credentials)
270
- ├── dashboard/ # Web dashboard (Hono server)
271
- │ ├── server.ts # Dashboard server
272
- │ ├── views.ts # HTML/CSS/JS (single-page app)
273
- │ └── routes/ # API routes
274
- ├── desktop/ # Tauri macOS menu bar app
275
- ├── proxy/ # Cloudflare Worker (OAuth proxy)
276
- ├── scripts/ # Google auth & calendar scripts
277
- └── data/ # SQLite DB & config (gitignored)
278
- ```
279
-
280
159
  ## License
281
160
 
282
161
  MIT
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
  const SCRIPT_PATH = path.resolve(__dirname, '../../index.js');
8
+ const isDev = SCRIPT_PATH.includes('/Projects/') || SCRIPT_PATH.includes('/src/');
9
+ const PM2_NAME = isDev ? 'clocktopus-monitor-dev' : 'clocktopus-monitor';
8
10
  const monitorRoutes = new Hono();
9
11
  function pm2Exec(command) {
10
12
  try {
@@ -20,15 +22,15 @@ monitorRoutes.get('/monitor/status', (c) => {
20
22
  try {
21
23
  const output = execSync('bunx pm2 jlist', { encoding: 'utf-8', timeout: 10000 });
22
24
  const processes = JSON.parse(output);
23
- const clocktopus = processes.find((p) => p.name === 'clocktopus');
24
- if (!clocktopus) {
25
+ const proc = processes.find((p) => p.name === PM2_NAME);
26
+ if (!proc) {
25
27
  return c.json({ running: false, status: 'not found' });
26
28
  }
27
29
  return c.json({
28
- running: clocktopus.pm2_env.status === 'online',
29
- status: clocktopus.pm2_env.status,
30
- uptime: clocktopus.pm2_env.pm_uptime,
31
- restarts: clocktopus.pm2_env.restart_time,
30
+ running: proc.pm2_env.status === 'online',
31
+ status: proc.pm2_env.status,
32
+ uptime: proc.pm2_env.pm_uptime,
33
+ restarts: proc.pm2_env.restart_time,
32
34
  });
33
35
  }
34
36
  catch {
@@ -37,15 +39,20 @@ monitorRoutes.get('/monitor/status', (c) => {
37
39
  });
38
40
  monitorRoutes.post('/monitor/start', (c) => {
39
41
  const bunPath = execSync('which bun', { encoding: 'utf-8' }).trim();
40
- const result = pm2Exec(`bunx pm2 start ${SCRIPT_PATH} --name clocktopus --interpreter ${bunPath} -- monitor`);
42
+ // Delete any existing process to avoid duplicates
43
+ try {
44
+ execSync(`bunx pm2 delete ${PM2_NAME}`, { stdio: 'ignore' });
45
+ }
46
+ catch { }
47
+ const result = pm2Exec(`bunx pm2 start ${SCRIPT_PATH} --name ${PM2_NAME} --interpreter ${bunPath} -- monitor:run`);
41
48
  return c.json(result);
42
49
  });
43
50
  monitorRoutes.post('/monitor/stop', (c) => {
44
- const result = pm2Exec('bunx pm2 stop clocktopus');
51
+ const result = pm2Exec(`bunx pm2 stop ${PM2_NAME}`);
45
52
  return c.json(result);
46
53
  });
47
54
  monitorRoutes.post('/monitor/restart', (c) => {
48
- const result = pm2Exec('bunx pm2 restart clocktopus');
55
+ const result = pm2Exec(`bunx pm2 restart ${PM2_NAME}`);
49
56
  return c.json(result);
50
57
  });
51
58
  export default monitorRoutes;
package/dist/index.js CHANGED
@@ -137,8 +137,8 @@ function sleep(ms) {
137
137
  return new Promise((res) => setTimeout(res, ms));
138
138
  }
139
139
  program
140
- .command('monitor')
141
- .description('Monitor system idle time and screen state, and stop the Clockify timer if idle or screen is off.')
140
+ .command('monitor:run', { hidden: true })
141
+ .description('Run monitor in foreground (used by PM2).')
142
142
  .action(async () => {
143
143
  const { workspaceId, userId } = await getWorkspaceAndUser();
144
144
  async function stopTimerAndLog(reason) {
@@ -275,6 +275,56 @@ program
275
275
  .action(() => {
276
276
  startDashboard();
277
277
  });
278
+ const isDev = __dirname.includes('/Projects/') || __dirname.includes('/src/');
279
+ const MONITOR_PM2_NAME = isDev ? 'clocktopus-monitor-dev' : 'clocktopus-monitor';
280
+ const DASH_PM2_NAME = isDev ? 'clocktopus-dash-dev' : 'clocktopus-dash';
281
+ program
282
+ .command('monitor')
283
+ .description('Start idle monitor as a background daemon.')
284
+ .action(async () => {
285
+ const { execSync } = await import('child_process');
286
+ const bunPath = execSync('which bun', { encoding: 'utf-8' }).trim();
287
+ const scriptPath = path.join(__dirname, 'index.js');
288
+ try {
289
+ try {
290
+ execSync(`bunx pm2 delete ${MONITOR_PM2_NAME}`, { stdio: 'ignore' });
291
+ }
292
+ catch { }
293
+ execSync(`bunx pm2 start ${scriptPath} --name ${MONITOR_PM2_NAME} --interpreter ${bunPath} -- monitor:run`, {
294
+ stdio: 'inherit',
295
+ });
296
+ console.log(chalk.green('Idle monitor started in background.'));
297
+ console.log(chalk.gray(' Stop: clocktopus monitor:stop'));
298
+ console.log(chalk.gray(' Logs: clocktopus monitor:logs'));
299
+ }
300
+ catch {
301
+ console.error(chalk.red('Failed to start monitor.'));
302
+ }
303
+ });
304
+ program
305
+ .command('monitor:stop')
306
+ .description('Stop the idle monitor daemon.')
307
+ .action(async () => {
308
+ const { execSync } = await import('child_process');
309
+ try {
310
+ execSync(`bunx pm2 stop ${MONITOR_PM2_NAME}`, { stdio: 'inherit' });
311
+ }
312
+ catch {
313
+ console.log(chalk.yellow('Monitor is not running.'));
314
+ }
315
+ });
316
+ program
317
+ .command('monitor:logs')
318
+ .description('Show idle monitor logs.')
319
+ .action(async () => {
320
+ const { execSync } = await import('child_process');
321
+ try {
322
+ execSync(`bunx pm2 logs ${MONITOR_PM2_NAME} --lines 50`, { stdio: 'inherit' });
323
+ }
324
+ catch {
325
+ console.log(chalk.yellow('Monitor is not running.'));
326
+ }
327
+ });
278
328
  program
279
329
  .command('serve')
280
330
  .description('Start dashboard as a background daemon (PM2).')
@@ -283,12 +333,11 @@ program
283
333
  const bunPath = execSync('which bun', { encoding: 'utf-8' }).trim();
284
334
  const scriptPath = path.join(__dirname, 'index.js');
285
335
  try {
286
- // Stop existing if running
287
336
  try {
288
- execSync('bunx pm2 delete clocktopus-dash', { stdio: 'ignore' });
337
+ execSync(`bunx pm2 delete ${DASH_PM2_NAME}`, { stdio: 'ignore' });
289
338
  }
290
339
  catch { }
291
- execSync(`bunx pm2 start ${scriptPath} --name clocktopus-dash --interpreter ${bunPath} -- dash`, {
340
+ execSync(`bunx pm2 start ${scriptPath} --name ${DASH_PM2_NAME} --interpreter ${bunPath} -- dash`, {
292
341
  stdio: 'inherit',
293
342
  });
294
343
  console.log(chalk.green('Dashboard running at http://localhost:4001'));
@@ -305,7 +354,7 @@ program
305
354
  .action(async () => {
306
355
  const { execSync } = await import('child_process');
307
356
  try {
308
- execSync('bunx pm2 stop clocktopus-dash', { stdio: 'inherit' });
357
+ execSync(`bunx pm2 stop ${DASH_PM2_NAME}`, { stdio: 'inherit' });
309
358
  }
310
359
  catch {
311
360
  console.log(chalk.yellow('Dashboard is not running.'));
@@ -317,7 +366,7 @@ program
317
366
  .action(async () => {
318
367
  const { execSync } = await import('child_process');
319
368
  try {
320
- execSync('bunx pm2 logs clocktopus-dash --lines 50', { stdio: 'inherit' });
369
+ execSync(`bunx pm2 logs ${DASH_PM2_NAME} --lines 50`, { stdio: 'inherit' });
321
370
  }
322
371
  catch {
323
372
  console.log(chalk.yellow('Dashboard is not running.'));
package/dist/lib/db.js CHANGED
@@ -2,7 +2,15 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { Database } from 'bun:sqlite';
4
4
  import { z } from 'zod';
5
- const DB_DIR = path.join(process.cwd(), 'data/db');
5
+ function getDataDir() {
6
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
7
+ const isDev = scriptDir.includes('/Projects/') || scriptDir.includes('/src/');
8
+ if (isDev) {
9
+ return path.join(process.cwd(), 'data/db');
10
+ }
11
+ return path.join(process.env.HOME || '~', '.clocktopus', 'data');
12
+ }
13
+ const DB_DIR = getDataDir();
6
14
  const DB_PATH = path.join(DB_DIR, 'sessions.db');
7
15
  if (!fs.existsSync(DB_DIR)) {
8
16
  fs.mkdirSync(DB_DIR, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clocktopus",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -18,12 +18,11 @@
18
18
  "postinstall": "cd node_modules/macos-notification-state && node-gyp rebuild 2>/dev/null; cd ../desktop-idle && node-gyp rebuild 2>/dev/null; true",
19
19
  "lint": "eslint . --ext .ts",
20
20
  "clock": "bun dist/index.js",
21
- "clockd": "bunx pm2 start dist/index.js --name clocktopus --",
22
- "monitor": "bun run clockd monitor",
23
- "monitor:restart": "bunx pm2 restart clocktopus",
24
- "monitor:stop": "bunx pm2 stop clocktopus",
25
- "monitor:logs": "bunx pm2 logs clocktopus",
26
- "monitor:status": "bunx pm2 status clocktopus",
21
+ "monitor": "bun dist/index.js monitor",
22
+ "monitor:stop": "bun dist/index.js monitor:stop",
23
+ "monitor:restart": "bunx pm2 restart clocktopus-monitor-dev",
24
+ "monitor:logs": "bun dist/index.js monitor:logs",
25
+ "monitor:status": "bunx pm2 status clocktopus-monitor-dev",
27
26
  "prepare": "husky",
28
27
  "dashboard": "bun -e \"import('./dist/dashboard/server.js').then(m => m.startDashboard())\"",
29
28
  "db:cleanup": "bun dist/scripts/db-cleanup.js",