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 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
@@ -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
+ }