vibe-pomo 0.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/LICENSE +21 -0
- package/README.md +202 -0
- package/bin/pomodoro.mjs +249 -0
- package/install.mjs +170 -0
- package/package.json +49 -0
- package/src/daemon/db.mjs +209 -0
- package/src/daemon/index.mjs +212 -0
- package/src/daemon/ipc.mjs +96 -0
- package/src/daemon/notificationQueue.mjs +39 -0
- package/src/daemon/session.mjs +109 -0
- package/src/daemon/timer.mjs +100 -0
- package/src/hooks/notification.mjs +53 -0
- package/src/hooks/preToolUse.mjs +38 -0
- package/src/hooks/stop.mjs +70 -0
- package/src/shared/config.mjs +58 -0
- package/src/shared/ipcClient.mjs +73 -0
- package/src/shared/lockfile.mjs +51 -0
- package/src/shared/protocol.mjs +44 -0
- package/src/tui/App.tsx +149 -0
- package/src/tui/components/NotificationLog.tsx +60 -0
- package/src/tui/components/TaskBar.tsx +23 -0
- package/src/tui/components/Timer.tsx +47 -0
- package/src/tui/dashboard/ActiveSessions.tsx +67 -0
- package/src/tui/dashboard/DashboardApp.tsx +110 -0
- package/src/tui/dashboard/ProjectChart.tsx +60 -0
- package/src/tui/dashboard/RecentSessions.tsx +79 -0
- package/src/tui/dashboard/index.tsx +31 -0
- package/src/tui/index.tsx +56 -0
- package/src/tui/ipcClient.mjs +52 -0
- package/src/tui/timer/TimerApp.tsx +164 -0
- package/src/tui/timer/index.tsx +51 -0
- package/src/watcher.mjs +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 vibe-pomo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# vibe-pomo 🍅
|
|
2
|
+
|
|
3
|
+
> You and your agent, both in flow.
|
|
4
|
+
|
|
5
|
+
<!-- screenshot: dashboard terminal showing active sessions + project stats -->
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why vibe-pomo
|
|
10
|
+
|
|
11
|
+
Most AI coding tools are built around one assumption: you're always watching. Every tool call, every decision, every completion — the agent pings you, waits for you, interrupts you. Each exchange is small, but the cumulative cost is enormous: you never get more than a few minutes of unbroken attention.
|
|
12
|
+
|
|
13
|
+
**vibe-pomo flips this.** Start a Pomodoro, hand the agent a task, and step away. The agent works autonomously — notifications silenced, decisions queued, no interruptions. When the timer ends, *you* decide when to come back. Not the agent.
|
|
14
|
+
|
|
15
|
+
**Deep focus, on both sides.**
|
|
16
|
+
Block out distraction-free time for yourself while the agent runs its own uninterrupted work session. No context switches. No reactive loops. Just two parallel flows converging when you're ready.
|
|
17
|
+
|
|
18
|
+
**Know where your time goes.**
|
|
19
|
+
Every session is logged with what the agent accomplished and what you worked on. Review per-project focus time, browse session history, and see exactly how your hours were spent — a clear record for personal retrospectives and project planning.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## How It Works
|
|
24
|
+
|
|
25
|
+
Two terminals, two roles:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Terminal A — daemon (always open) Terminal B — Claude Code conversation
|
|
29
|
+
───────────────────────────────── ───────────────────────────────────────
|
|
30
|
+
$ pomodoro daemon /pomodoro 25m Fix auth bug
|
|
31
|
+
|
|
|
32
|
+
🍅 Pomodoro daemon running +---> Timer window opens
|
|
33
|
+
Agent starts working
|
|
34
|
+
Active Sessions Notifications silenced
|
|
35
|
+
23:41 my-project Fix auth bug Tool calls auto-approved
|
|
36
|
+
|
|
37
|
+
Project Focus Time
|
|
38
|
+
my-project ████████████░░ 3h 45m
|
|
39
|
+
|
|
40
|
+
Recent Sessions
|
|
41
|
+
my-project Fix auth bug
|
|
42
|
+
🤖 Rewrote JWT middleware
|
|
43
|
+
👤 Had a planning call
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Timer window (per session)
|
|
48
|
+
──────────────────────────────────
|
|
49
|
+
🍅 Pomodoro
|
|
50
|
+
|
|
51
|
+
+02:13 OVERTIME
|
|
52
|
+
|
|
53
|
+
Task: Fix auth bug
|
|
54
|
+
|
|
55
|
+
Notifications
|
|
56
|
+
┌──────────────────────────────┐
|
|
57
|
+
│ Build passed │
|
|
58
|
+
│ Tests: 42 passed │
|
|
59
|
+
└──────────────────────────────┘
|
|
60
|
+
|
|
61
|
+
[E] End Session [B] Break [Q] Quit
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When the timer ends, queued notifications are released and you're prompted to log what *you* did during that time:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
What did you do during this session?
|
|
68
|
+
(optional — press Enter to skip)
|
|
69
|
+
|
|
70
|
+
> Reviewed the RFC, had a planning call with the team
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This gets saved alongside the agent's summary, giving you a dual-perspective record of every session.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
**Prerequisites:** Node.js 20+, Claude Code CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install -g vibe-pomo
|
|
83
|
+
pomodoro install
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`pomodoro install` registers three Claude Code hooks in `~/.claude/settings.json` and installs the `/pomodoro`, `/pomodoro-stats`, and `/pomodoro-stop` slash commands:
|
|
87
|
+
|
|
88
|
+
- **PreToolUse** — auto-approves all tool calls during an active session
|
|
89
|
+
- **Notification** — silently queues notifications until the timer ends
|
|
90
|
+
- **Stop** — holds the agent in place until you end the session
|
|
91
|
+
|
|
92
|
+
> All three hooks **exit immediately with no effect** when no Pomodoro is active. They won't interfere with any other Claude Code setup or slash command frameworks.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
### 1. Start the daemon (once, keep this terminal open)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pomodoro daemon
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Shows a live dashboard: active sessions with countdowns, per-project focus time, and recent session history.
|
|
105
|
+
|
|
106
|
+
### 2. Start a session
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# From Claude Code (recommended)
|
|
110
|
+
/pomodoro 25m Refactor the auth module
|
|
111
|
+
|
|
112
|
+
# From any terminal
|
|
113
|
+
pomodoro start 25m Refactor the auth module
|
|
114
|
+
pomodoro start Refactor the auth module # uses default duration
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A timer window opens. The agent starts working. You're free.
|
|
118
|
+
|
|
119
|
+
### 3. During the session
|
|
120
|
+
|
|
121
|
+
The agent works autonomously — tool calls approved, notifications queued, decisions logged. You can check in from Claude Code without exiting:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
/pomodoro-stats view time tracking statistics
|
|
125
|
+
/pomodoro-stop break the current session
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4. When the timer ends
|
|
129
|
+
|
|
130
|
+
Timer switches to overtime. Queued notifications appear. Press **E** to end, log what you did, then review the agent's summary and any pending decisions in `.claude/pomodoro-summary.md` and `.claude/pomodoro-pending.md`.
|
|
131
|
+
|
|
132
|
+
### 5. Review your stats
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pomodoro stats
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Project Focus Time
|
|
140
|
+
──────────────────────────────────────────────────────────────────
|
|
141
|
+
my-project ████████████████░░░░░░░░░░░░░░ 4h 20m
|
|
142
|
+
side-project ██████░░░░░░░░░░░░░░░░░░░░░░░░ 1h 45m
|
|
143
|
+
|
|
144
|
+
Recent Sessions
|
|
145
|
+
──────────────────────────────────────────────────────────────────
|
|
146
|
+
4/13 my-project Refactor auth module 28m completed
|
|
147
|
+
🤖 Rewrote JWT middleware, pending: refresh token expiry strategy
|
|
148
|
+
👤 Read RFC, had planning call with team
|
|
149
|
+
|
|
150
|
+
4/13 my-project Fix payment webhook 18m completed
|
|
151
|
+
🤖 Found and fixed Stripe signature validation bug
|
|
152
|
+
👤 Coffee, cleared inbox
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
`~/.claude/pomodoro.json` is created on first run:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"defaultDurationMs": 1500000,
|
|
164
|
+
"decisionStrategy": "wait",
|
|
165
|
+
"terminalEmulator": "auto",
|
|
166
|
+
"soundOnOvertime": true
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| Option | Values | Description |
|
|
171
|
+
|--------|--------|-------------|
|
|
172
|
+
| `defaultDurationMs` | ms | Default session duration (25 min = `1500000`) |
|
|
173
|
+
| `decisionStrategy` | `"wait"` / `"break"` | When the agent is blocked: wait silently until you end the session (default), or end immediately |
|
|
174
|
+
| `terminalEmulator` | `"auto"` / name | Terminal for the timer window. Auto-detects from `$TERM_PROGRAM`, `$KITTY_WINDOW_ID`, etc. |
|
|
175
|
+
| `soundOnOvertime` | bool | Play a sound when the timer hits zero |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Commands
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
pomodoro daemon Start daemon + live dashboard
|
|
183
|
+
pomodoro start [dur] [task] Start a session
|
|
184
|
+
pomodoro stop Break the current session
|
|
185
|
+
pomodoro stats Show time tracking statistics
|
|
186
|
+
pomodoro install Register hooks with Claude Code
|
|
187
|
+
pomodoro stop-daemon Stop the global daemon
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Duration formats: `25m`, `1h`, `90s`, or a plain number (treated as minutes).
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Compatibility
|
|
195
|
+
|
|
196
|
+
vibe-pomo hooks activate only when the daemon is running. When no session is active, all three hooks exit immediately — no output, no side effects. One global daemon handles all your Claude Code projects simultaneously.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
package/bin/pomodoro.mjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { readLock, removeLock } from '../src/shared/lockfile.mjs'
|
|
6
|
+
import { sendAndReceive } from '../src/shared/ipcClient.mjs'
|
|
7
|
+
import { loadConfig, parseDuration, ensureConfigFile } from '../src/shared/config.mjs'
|
|
8
|
+
import { MSG, DECISION } from '../src/shared/protocol.mjs'
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const ROOT = join(__dirname, '..')
|
|
12
|
+
const TSX = join(ROOT, 'node_modules', '.bin', 'tsx')
|
|
13
|
+
|
|
14
|
+
const [,, cmd, ...args] = process.argv
|
|
15
|
+
|
|
16
|
+
const HELP = `Usage: pomodoro <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
daemon Start daemon + live dashboard (run this first)
|
|
20
|
+
start [duration] [task...] Start a Pomodoro session
|
|
21
|
+
stop [sessionId] Break the current (or specified) session
|
|
22
|
+
stats Show time tracking statistics
|
|
23
|
+
install Register hooks in ~/.claude/settings.json
|
|
24
|
+
stop-daemon Stop the global daemon
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
pomodoro daemon (keep this terminal open)
|
|
28
|
+
pomodoro start 25m Fix login bug
|
|
29
|
+
pomodoro start Fix login bug (uses default 25m)
|
|
30
|
+
pomodoro stop
|
|
31
|
+
pomodoro stats
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
const COMMANDS = {
|
|
35
|
+
daemon: cmdDaemon,
|
|
36
|
+
start: cmdStart,
|
|
37
|
+
stop: cmdStop,
|
|
38
|
+
stats: cmdStats,
|
|
39
|
+
stat: cmdStats, // alias
|
|
40
|
+
install: cmdInstall,
|
|
41
|
+
'stop-daemon': cmdStopDaemon,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handler = COMMANDS[cmd]
|
|
45
|
+
if (!handler) {
|
|
46
|
+
process.stdout.write(HELP)
|
|
47
|
+
process.exit(cmd ? 1 : 0)
|
|
48
|
+
}
|
|
49
|
+
await handler(args)
|
|
50
|
+
|
|
51
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
async function cmdDaemon() {
|
|
54
|
+
// Launch dashboard TUI (which also starts the daemon in-process)
|
|
55
|
+
const dashboardEntry = join(ROOT, 'src', 'tui', 'dashboard', 'index.tsx')
|
|
56
|
+
const child = spawn(TSX, [dashboardEntry], { stdio: 'inherit' })
|
|
57
|
+
child.on('exit', (code) => process.exit(code ?? 0))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function cmdStart(args) {
|
|
61
|
+
ensureConfigFile()
|
|
62
|
+
const config = loadConfig()
|
|
63
|
+
|
|
64
|
+
const lock = readLock()
|
|
65
|
+
if (!lock) {
|
|
66
|
+
console.error('Pomodoro daemon is not running.')
|
|
67
|
+
console.error('Start it first with: pomodoro daemon')
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse optional leading duration, rest is task
|
|
72
|
+
let durationMs = config.defaultDurationMs
|
|
73
|
+
let taskParts = [...args]
|
|
74
|
+
|
|
75
|
+
if (taskParts[0] && !taskParts[0].startsWith('-')) {
|
|
76
|
+
const parsed = parseDuration(taskParts[0])
|
|
77
|
+
if (parsed) { durationMs = parsed; taskParts.shift() }
|
|
78
|
+
}
|
|
79
|
+
const task = taskParts.join(' ')
|
|
80
|
+
|
|
81
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
82
|
+
|
|
83
|
+
let resp
|
|
84
|
+
try {
|
|
85
|
+
resp = await sendAndReceive({
|
|
86
|
+
type: MSG.SESSION_CREATE,
|
|
87
|
+
projectDir,
|
|
88
|
+
task,
|
|
89
|
+
plannedMs: durationMs,
|
|
90
|
+
decisionStrategy: config.decisionStrategy,
|
|
91
|
+
})
|
|
92
|
+
} catch {
|
|
93
|
+
console.error('Could not reach daemon. Is `pomodoro daemon` running?')
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (resp.error) {
|
|
98
|
+
console.error('Error:', resp.error)
|
|
99
|
+
process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`Session started (${formatMs(durationMs)})`)
|
|
103
|
+
if (task) console.log(`Task: ${task}`)
|
|
104
|
+
|
|
105
|
+
// If running inside Claude Code, start a PPID watcher for auto-break on exit
|
|
106
|
+
if (isInsideClaudeCode()) {
|
|
107
|
+
spawnWatcher(resp.sessionId)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Launch timer TUI in a new terminal window
|
|
111
|
+
const timerEntry = join(ROOT, 'src', 'tui', 'timer', 'index.tsx')
|
|
112
|
+
const launched = await launchTerminal(TSX, timerEntry, resp.sessionId, config.terminalEmulator)
|
|
113
|
+
|
|
114
|
+
if (!launched) {
|
|
115
|
+
console.log(`\nOpen the timer in a new terminal:`)
|
|
116
|
+
console.log(` tsx ${timerEntry} ${resp.sessionId}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function cmdStop(args) {
|
|
121
|
+
const lock = readLock()
|
|
122
|
+
if (!lock) { console.log('No daemon running.'); process.exit(0) }
|
|
123
|
+
|
|
124
|
+
// If sessionId given, stop that session; otherwise stop first active
|
|
125
|
+
let sessionId = args[0]
|
|
126
|
+
if (!sessionId) {
|
|
127
|
+
let state
|
|
128
|
+
try { state = await sendAndReceive({ type: MSG.QUERY }) } catch {
|
|
129
|
+
console.error('Daemon unreachable.')
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
133
|
+
const active = state.sessions?.find((s) => s.projectDir === projectDir)
|
|
134
|
+
?? state.sessions?.[0]
|
|
135
|
+
if (!active) { console.log('No active sessions.'); process.exit(0) }
|
|
136
|
+
sessionId = active.sessionId
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await sendAndReceive({ type: MSG.SESSION_BREAK, sessionId })
|
|
141
|
+
console.log('Session broken.')
|
|
142
|
+
} catch {
|
|
143
|
+
console.error('Could not reach daemon.')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function cmdStats() {
|
|
148
|
+
const lock = readLock()
|
|
149
|
+
if (!lock) {
|
|
150
|
+
// Show static stats from DB
|
|
151
|
+
const { printStats } = await import('../src/daemon/db.mjs')
|
|
152
|
+
printStats()
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
// Launch the dashboard in read-only mode (connect to existing daemon)
|
|
156
|
+
// For now, print stats — a full "attach to dashboard" is future work
|
|
157
|
+
const { printStats } = await import('../src/daemon/db.mjs')
|
|
158
|
+
printStats()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function cmdInstall() {
|
|
162
|
+
const { runInstall } = await import('../install.mjs')
|
|
163
|
+
runInstall()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function cmdStopDaemon() {
|
|
167
|
+
const lock = readLock()
|
|
168
|
+
if (!lock) { console.log('No daemon running.'); process.exit(0) }
|
|
169
|
+
try {
|
|
170
|
+
process.kill(lock.pid, 'SIGTERM')
|
|
171
|
+
removeLock()
|
|
172
|
+
console.log('Daemon stopped.')
|
|
173
|
+
} catch {
|
|
174
|
+
removeLock()
|
|
175
|
+
console.log('Daemon was already stopped; lock cleaned up.')
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Terminal launch helpers ───────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
async function launchTerminal(tsx, entryFile, sessionId, termPref) {
|
|
182
|
+
const terminals = termPref && termPref !== 'auto'
|
|
183
|
+
? [termPref]
|
|
184
|
+
: detectTerminals()
|
|
185
|
+
|
|
186
|
+
for (const term of terminals) {
|
|
187
|
+
const ok = await trySpawn(term, tsx, entryFile, sessionId)
|
|
188
|
+
if (ok) return true
|
|
189
|
+
}
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function detectTerminals() {
|
|
194
|
+
const list = []
|
|
195
|
+
if (process.env.KITTY_WINDOW_ID) list.push('kitty')
|
|
196
|
+
if (process.env.TERM_PROGRAM === 'WezTerm') list.push('wezterm')
|
|
197
|
+
list.push('gnome-terminal', 'xfce4-terminal', 'konsole', 'xterm', 'alacritty', 'wezterm', 'kitty')
|
|
198
|
+
return list
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildCmd(term, tsx, entryFile, sessionId) {
|
|
202
|
+
const inner = `${tsx} ${entryFile} ${sessionId}`
|
|
203
|
+
switch (term) {
|
|
204
|
+
case 'gnome-terminal': return ['gnome-terminal', '--', 'bash', '-c', inner]
|
|
205
|
+
case 'xfce4-terminal': return ['xfce4-terminal', '-e', inner]
|
|
206
|
+
case 'konsole': return ['konsole', '-e', inner]
|
|
207
|
+
case 'xterm': return ['xterm', '-e', inner]
|
|
208
|
+
case 'alacritty': return ['alacritty', '-e', 'bash', '-c', inner]
|
|
209
|
+
case 'wezterm': return ['wezterm', 'start', '--', 'bash', '-c', inner]
|
|
210
|
+
case 'kitty': return ['kitty', 'bash', '-c', inner]
|
|
211
|
+
default: return null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function trySpawn(term, tsx, entryFile, sessionId) {
|
|
216
|
+
const cmd = buildCmd(term, tsx, entryFile, sessionId)
|
|
217
|
+
if (!cmd) return Promise.resolve(false)
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
let done = false
|
|
220
|
+
try {
|
|
221
|
+
const child = spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore' })
|
|
222
|
+
child.on('error', () => { if (!done) { done = true; resolve(false) } })
|
|
223
|
+
child.on('spawn', () => { if (!done) { done = true; child.unref(); resolve(true) } })
|
|
224
|
+
setTimeout(() => { if (!done) { done = true; child.unref(); resolve(true) } }, 300)
|
|
225
|
+
} catch { resolve(false) }
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isInsideClaudeCode() {
|
|
230
|
+
// Claude Code sets CLAUDE_PROJECT_DIR; also check for CLAUDE_CODE_ENTRYPOINT
|
|
231
|
+
return !!(process.env.CLAUDE_PROJECT_DIR || process.env.CLAUDE_CODE_ENTRYPOINT)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function spawnWatcher(sessionId) {
|
|
235
|
+
const watcherPath = join(ROOT, 'src', 'watcher.mjs')
|
|
236
|
+
// Watch the grandparent process (Claude Code's Node.js process)
|
|
237
|
+
const watchPid = process.ppid
|
|
238
|
+
const child = spawn(process.execPath, [watcherPath, String(watchPid), sessionId], {
|
|
239
|
+
detached: true,
|
|
240
|
+
stdio: 'ignore',
|
|
241
|
+
})
|
|
242
|
+
child.unref()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function formatMs(ms) {
|
|
246
|
+
const m = Math.floor(ms / 60000)
|
|
247
|
+
const s = Math.floor((ms % 60000) / 1000)
|
|
248
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`
|
|
249
|
+
}
|
package/install.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install.mjs — Register Pomodoro hooks in ~/.claude/settings.json
|
|
3
|
+
*
|
|
4
|
+
* Run once: node install.mjs or pomodoro install
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join, dirname } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { createRequire } from 'node:module'
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const HOOKS_DIR = join(__dirname, 'src', 'hooks')
|
|
14
|
+
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json')
|
|
15
|
+
|
|
16
|
+
export function runInstall() {
|
|
17
|
+
const nodeExec = process.execPath
|
|
18
|
+
|
|
19
|
+
const hookDefs = {
|
|
20
|
+
PreToolUse: [
|
|
21
|
+
{
|
|
22
|
+
matcher: '.*',
|
|
23
|
+
hooks: [{
|
|
24
|
+
type: 'command',
|
|
25
|
+
command: `${nodeExec} ${join(HOOKS_DIR, 'preToolUse.mjs')}`,
|
|
26
|
+
}],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
Notification: [
|
|
30
|
+
{
|
|
31
|
+
hooks: [{
|
|
32
|
+
type: 'command',
|
|
33
|
+
command: `${nodeExec} ${join(HOOKS_DIR, 'notification.mjs')}`,
|
|
34
|
+
}],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
Stop: [
|
|
38
|
+
{
|
|
39
|
+
hooks: [{
|
|
40
|
+
type: 'command',
|
|
41
|
+
command: `${nodeExec} ${join(HOOKS_DIR, 'stop.mjs')}`,
|
|
42
|
+
}],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read existing settings
|
|
48
|
+
let settings = {}
|
|
49
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
50
|
+
try {
|
|
51
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'))
|
|
52
|
+
} catch {
|
|
53
|
+
console.error(`Could not parse ${SETTINGS_PATH}. Aborting.`)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
mkdirSync(dirname(SETTINGS_PATH), { recursive: true })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Merge hooks — avoid duplicates by checking command paths
|
|
61
|
+
settings.hooks = settings.hooks ?? {}
|
|
62
|
+
|
|
63
|
+
let added = 0
|
|
64
|
+
for (const [event, defs] of Object.entries(hookDefs)) {
|
|
65
|
+
settings.hooks[event] = settings.hooks[event] ?? []
|
|
66
|
+
for (const def of defs) {
|
|
67
|
+
const commandToAdd = def.hooks[0].command
|
|
68
|
+
const alreadyRegistered = settings.hooks[event].some(
|
|
69
|
+
(existing) => existing.hooks?.some((h) => h.command === commandToAdd)
|
|
70
|
+
)
|
|
71
|
+
if (!alreadyRegistered) {
|
|
72
|
+
settings.hooks[event].push(def)
|
|
73
|
+
added++
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8')
|
|
79
|
+
|
|
80
|
+
if (added > 0) {
|
|
81
|
+
console.log(`✓ Registered ${added} hook(s) in ${SETTINGS_PATH}`)
|
|
82
|
+
} else {
|
|
83
|
+
console.log(`Hooks already registered in ${SETTINGS_PATH}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Also install project-local slash command
|
|
87
|
+
installSlashCommand()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function installSlashCommand() {
|
|
91
|
+
const globalCommandsDir = join(homedir(), '.claude', 'commands')
|
|
92
|
+
mkdirSync(globalCommandsDir, { recursive: true })
|
|
93
|
+
|
|
94
|
+
const commands = [
|
|
95
|
+
{ file: 'pomodoro.md', writer: writePomodoroCommand },
|
|
96
|
+
{ file: 'pomodoro-stats.md', writer: writeStatsCommand },
|
|
97
|
+
{ file: 'pomodoro-stop.md', writer: writeStopCommand },
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
for (const { file, writer } of commands) {
|
|
101
|
+
const dest = join(globalCommandsDir, file)
|
|
102
|
+
writer(dest)
|
|
103
|
+
console.log(`✓ Slash command: ~/.claude/commands/${file}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writePomodoroCommand(dest) {
|
|
108
|
+
const pomodoroPath = join(__dirname, 'bin', 'pomodoro.mjs')
|
|
109
|
+
const nodeExec = process.execPath
|
|
110
|
+
writeFileSync(dest, `---
|
|
111
|
+
description: Start a Pomodoro focus session — agent works autonomously until timer ends
|
|
112
|
+
argument-hint: "[duration e.g. 25m] [task description]"
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Step 1: Start the Pomodoro timer
|
|
116
|
+
|
|
117
|
+
\`\`\`bash
|
|
118
|
+
${nodeExec} ${pomodoroPath} start $ARGUMENTS
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
## Step 2: Work autonomously during the focus session
|
|
122
|
+
|
|
123
|
+
The user has started a Pomodoro focus session and **will not be checking the screen** until the timer ends. Do not interrupt them.
|
|
124
|
+
|
|
125
|
+
**Task**: $ARGUMENTS
|
|
126
|
+
|
|
127
|
+
- Focus only on what is **unambiguously clear** from the task description
|
|
128
|
+
- If you encounter something that requires a user decision, **stop and record it** in \`.claude/pomodoro-pending.md\` — do NOT make assumptions or proceed on the user's behalf
|
|
129
|
+
- Do not send notifications or ask questions — the user is in focus mode
|
|
130
|
+
- When you have done all you can, write a summary to \`.claude/pomodoro-summary.md\`
|
|
131
|
+
- Then wait quietly — the Pomodoro hook will manage the session lifecycle
|
|
132
|
+
`, 'utf8')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writeStatsCommand(dest) {
|
|
136
|
+
const pomodoroPath = join(__dirname, 'bin', 'pomodoro.mjs')
|
|
137
|
+
const nodeExec = process.execPath
|
|
138
|
+
writeFileSync(dest, `---
|
|
139
|
+
description: Show Pomodoro time tracking statistics
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
Run the following command and display the output to the user:
|
|
143
|
+
|
|
144
|
+
\`\`\`bash
|
|
145
|
+
${nodeExec} ${pomodoroPath} stats
|
|
146
|
+
\`\`\`
|
|
147
|
+
`, 'utf8')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function writeStopCommand(dest) {
|
|
151
|
+
const pomodoroPath = join(__dirname, 'bin', 'pomodoro.mjs')
|
|
152
|
+
const nodeExec = process.execPath
|
|
153
|
+
writeFileSync(dest, `---
|
|
154
|
+
description: Break (stop) the current Pomodoro session
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
Run the following command to break the active Pomodoro session:
|
|
158
|
+
|
|
159
|
+
\`\`\`bash
|
|
160
|
+
${nodeExec} ${pomodoroPath} stop
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
Then confirm to the user that the session has been stopped.
|
|
164
|
+
`, 'utf8')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Run directly if called as script
|
|
168
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
169
|
+
runInstall()
|
|
170
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibe-pomo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "You and your agent, both in flow. A Pomodoro timer for Claude Code that keeps agents working autonomously while you stay deep in focus — uninterrupted.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pomodoro": "bin/pomodoro.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"install.mjs",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"pomodoro",
|
|
19
|
+
"focus",
|
|
20
|
+
"productivity",
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"tui"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Weilong Qin <qinweilong0813@gmail.com>",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Weilong-Qin/vibe-pomo.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"start": "node bin/pomodoro.mjs",
|
|
33
|
+
"tui": "tsx src/tui/index.tsx"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"better-sqlite3": "^9.4.3",
|
|
37
|
+
"ink": "^5.1.0",
|
|
38
|
+
"ink-text-input": "^6.0.0",
|
|
39
|
+
"react": "^18.3.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/better-sqlite3": "^7.6.8",
|
|
43
|
+
"@types/react": "^18.3.1",
|
|
44
|
+
"tsx": "^4.19.2"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
}
|
|
49
|
+
}
|