vibe-pomo 0.1.0 → 0.1.1

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
@@ -2,8 +2,17 @@
2
2
 
3
3
  > You and your agent, both in flow.
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/vibe-pomo)](https://www.npmjs.com/package/vibe-pomo)
6
+ [![npm downloads](https://img.shields.io/npm/dm/vibe-pomo)](https://www.npmjs.com/package/vibe-pomo)
7
+
5
8
  <!-- screenshot: dashboard terminal showing active sessions + project stats -->
6
9
 
10
+ <!-- README-I18N:START -->
11
+
12
+ **English** | [汉语](./README.zh.md)
13
+
14
+ <!-- README-I18N:END -->
15
+
7
16
  ---
8
17
 
9
18
  ## Why vibe-pomo
@@ -79,7 +88,7 @@ This gets saved alongside the agent's summary, giving you a dual-perspective rec
79
88
  **Prerequisites:** Node.js 20+, Claude Code CLI
80
89
 
81
90
  ```bash
82
- npm install -g vibe-pomo
91
+ npm i -g vibe-pomo
83
92
  pomodoro install
84
93
  ```
85
94
 
package/README.zh.md ADDED
@@ -0,0 +1,208 @@
1
+ # vibe-pomo 🍅
2
+
3
+ > 你和你的 AI 代理,同时进入心流。
4
+
5
+ <!-- screenshot: dashboard terminal showing active sessions + project stats -->
6
+
7
+ <!-- README-I18N:START -->
8
+
9
+ [English](./README.md) | **汉语**
10
+
11
+ <!-- README-I18N:END -->
12
+
13
+ ---
14
+
15
+ ## 为什么选择 vibe-pomo
16
+
17
+ 大多数 AI 编码工具都建立在一个假设上:你始终在旁边盯着。每一次工具调用、每一个决策、每一次完成——代理 ping 你、等你、打断你。每次交互看似微小,但累积起来代价巨大:你从未能获得超过几分钟的不间断专注。
18
+
19
+ **vibe-pomo 反转了这一切。** 启动一个番茄钟,把任务交给代理,然后离开。代理自主工作——通知静音、决策排队、零打扰。计时结束后,由*你*决定何时回来,而不是代理。
20
+
21
+ **双向深度专注。**
22
+ 为自己屏蔽干扰,让代理同时运行不间断的工作会话。没有上下文切换,没有被动响应循环。两条并行的心流,在你准备好时汇聚。
23
+
24
+ **清楚地知道时间去哪了。**
25
+ 每次会话都会记录代理完成的工作以及你自己做的事情。查看每个项目的专注时间、浏览会话历史,精确了解时间的流向——为个人复盘和项目规划提供清晰的记录。
26
+
27
+ ---
28
+
29
+ ## 工作原理
30
+
31
+ 两个终端,两个角色:
32
+
33
+ ```
34
+ 终端 A — 守护进程(保持打开) 终端 B — Claude Code 对话
35
+ ───────────────────────────────── ───────────────────────────────────────
36
+ $ pomodoro daemon /pomodoro 25m Fix auth bug
37
+ |
38
+ 🍅 Pomodoro daemon running +---> 计时器窗口打开
39
+ 代理开始工作
40
+ Active Sessions 通知静音
41
+ 23:41 my-project Fix auth bug 工具调用自动批准
42
+
43
+ Project Focus Time
44
+ my-project ████████████░░ 3h 45m
45
+
46
+ Recent Sessions
47
+ my-project Fix auth bug
48
+ 🤖 Rewrote JWT middleware
49
+ 👤 Had a planning call
50
+ ```
51
+
52
+ ```
53
+ 计时器窗口(每个会话)
54
+ ──────────────────────────────────
55
+ 🍅 Pomodoro
56
+
57
+ +02:13 OVERTIME
58
+
59
+ Task: Fix auth bug
60
+
61
+ Notifications
62
+ ┌──────────────────────────────┐
63
+ │ Build passed │
64
+ │ Tests: 42 passed │
65
+ └──────────────────────────────┘
66
+
67
+ [E] End Session [B] Break [Q] Quit
68
+ ```
69
+
70
+ 计时结束后,排队的通知会被释放,并提示你记录在此期间*你*做了什么:
71
+
72
+ ```
73
+ What did you do during this session?
74
+ (optional — press Enter to skip)
75
+
76
+ > Reviewed the RFC, had a planning call with the team
77
+ ```
78
+
79
+ 这条记录会与代理的总结一同保存,为每次会话提供双视角记录。
80
+
81
+ ---
82
+
83
+ ## 安装
84
+
85
+ **前置条件:** Node.js 20+、Claude Code CLI
86
+
87
+ ```bash
88
+ npm install -g vibe-pomo
89
+ pomodoro install
90
+ ```
91
+
92
+ `pomodoro install` 会在 `~/.claude/settings.json` 中注册三个 Claude Code 钩子,并安装 `/pomodoro`、`/pomodoro-stats` 和 `/pomodoro-stop` 斜杠命令:
93
+
94
+ - **PreToolUse** — 在会话期间自动批准所有工具调用
95
+ - **Notification** — 静默排队通知,直到计时结束
96
+ - **Stop** — 在会话结束前让代理保持等待状态
97
+
98
+ > 这三个钩子在**没有活跃番茄钟时会立即退出且无任何效果**,不会干扰其他任何 Claude Code 配置或斜杠命令框架。
99
+
100
+ ---
101
+
102
+ ## 使用方法
103
+
104
+ ### 1. 启动守护进程(一次,保持该终端打开)
105
+
106
+ ```bash
107
+ pomodoro daemon
108
+ ```
109
+
110
+ 显示实时仪表板:带倒计时的活跃会话、各项目专注时间及最近会话历史。
111
+
112
+ ### 2. 开始一个会话
113
+
114
+ ```bash
115
+ # 在 Claude Code 中(推荐)
116
+ /pomodoro 25m Refactor the auth module
117
+
118
+ # 在任意终端中
119
+ pomodoro start 25m Refactor the auth module
120
+ pomodoro start Refactor the auth module # 使用默认时长
121
+ ```
122
+
123
+ 计时器窗口打开,代理开始工作,你自由了。
124
+
125
+ ### 3. 会话进行中
126
+
127
+ 代理自主工作——工具调用自动批准、通知排队、决策记录。你可以在不退出的情况下通过 Claude Code 查看进度:
128
+
129
+ ```
130
+ /pomodoro-stats 查看时间追踪统计
131
+ /pomodoro-stop 中断当前会话
132
+ ```
133
+
134
+ ### 4. 计时结束后
135
+
136
+ 计时器切换为超时模式,排队的通知显示出来。按 **E** 结束会话,记录你的工作内容,然后在 `.claude/pomodoro-summary.md` 和 `.claude/pomodoro-pending.md` 中查看代理的总结及待处理决策。
137
+
138
+ ### 5. 查看统计数据
139
+
140
+ ```bash
141
+ pomodoro stats
142
+ ```
143
+
144
+ ```
145
+ Project Focus Time
146
+ ──────────────────────────────────────────────────────────────────
147
+ my-project ████████████████░░░░░░░░░░░░░░ 4h 20m
148
+ side-project ██████░░░░░░░░░░░░░░░░░░░░░░░░ 1h 45m
149
+
150
+ Recent Sessions
151
+ ──────────────────────────────────────────────────────────────────
152
+ 4/13 my-project Refactor auth module 28m completed
153
+ 🤖 Rewrote JWT middleware, pending: refresh token expiry strategy
154
+ 👤 Read RFC, had planning call with team
155
+
156
+ 4/13 my-project Fix payment webhook 18m completed
157
+ 🤖 Found and fixed Stripe signature validation bug
158
+ 👤 Coffee, cleared inbox
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 配置
164
+
165
+ 首次运行时会创建 `~/.claude/pomodoro.json`:
166
+
167
+ ```json
168
+ {
169
+ "defaultDurationMs": 1500000,
170
+ "decisionStrategy": "wait",
171
+ "terminalEmulator": "auto",
172
+ "soundOnOvertime": true
173
+ }
174
+ ```
175
+
176
+ | 选项 | 取值 | 说明 |
177
+ |------|------|------|
178
+ | `defaultDurationMs` | 毫秒数 | 默认会话时长(25 分钟 = `1500000`) |
179
+ | `decisionStrategy` | `"wait"` / `"break"` | 代理被阻塞时的策略:静默等待直到你结束会话(默认),或立即结束 |
180
+ | `terminalEmulator` | `"auto"` / 名称 | 计时器窗口使用的终端。从 `$TERM_PROGRAM`、`$KITTY_WINDOW_ID` 等自动检测 |
181
+ | `soundOnOvertime` | 布尔值 | 计时归零时播放提示音 |
182
+
183
+ ---
184
+
185
+ ## 命令
186
+
187
+ ```
188
+ pomodoro daemon 启动守护进程及实时仪表板
189
+ pomodoro start [dur] [task] 开始一个会话
190
+ pomodoro stop 中断当前会话
191
+ pomodoro stats 显示时间追踪统计
192
+ pomodoro install 向 Claude Code 注册钩子
193
+ pomodoro stop-daemon 停止全局守护进程
194
+ ```
195
+
196
+ 时长格式:`25m`、`1h`、`90s`,或纯数字(按分钟处理)。
197
+
198
+ ---
199
+
200
+ ## 兼容性
201
+
202
+ vibe-pomo 的钩子仅在守护进程运行时激活。没有活跃会话时,三个钩子均立即退出——无输出、无副作用。一个全局守护进程可同时处理你所有的 Claude Code 项目。
203
+
204
+ ---
205
+
206
+ ## 许可证
207
+
208
+ MIT
package/bin/pomodoro.mjs CHANGED
@@ -9,7 +9,8 @@ import { MSG, DECISION } from '../src/shared/protocol.mjs'
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url))
11
11
  const ROOT = join(__dirname, '..')
12
- const TSX = join(ROOT, 'node_modules', '.bin', 'tsx')
12
+ const isWin = process.platform === 'win32'
13
+ const TSX = join(ROOT, 'node_modules', '.bin', isWin ? 'tsx.cmd' : 'tsx')
13
14
 
14
15
  const [,, cmd, ...args] = process.argv
15
16
 
@@ -53,7 +54,7 @@ await handler(args)
53
54
  async function cmdDaemon() {
54
55
  // Launch dashboard TUI (which also starts the daemon in-process)
55
56
  const dashboardEntry = join(ROOT, 'src', 'tui', 'dashboard', 'index.tsx')
56
- const child = spawn(TSX, [dashboardEntry], { stdio: 'inherit' })
57
+ const child = spawn(TSX, [dashboardEntry], { stdio: 'inherit', shell: isWin })
57
58
  child.on('exit', (code) => process.exit(code ?? 0))
58
59
  }
59
60
 
@@ -192,6 +193,10 @@ async function launchTerminal(tsx, entryFile, sessionId, termPref) {
192
193
 
193
194
  function detectTerminals() {
194
195
  const list = []
196
+ if (isWin) {
197
+ list.push('wt', 'cmd')
198
+ return list
199
+ }
195
200
  if (process.env.KITTY_WINDOW_ID) list.push('kitty')
196
201
  if (process.env.TERM_PROGRAM === 'WezTerm') list.push('wezterm')
197
202
  list.push('gnome-terminal', 'xfce4-terminal', 'konsole', 'xterm', 'alacritty', 'wezterm', 'kitty')
@@ -199,16 +204,22 @@ function detectTerminals() {
199
204
  }
200
205
 
201
206
  function buildCmd(term, tsx, entryFile, sessionId) {
202
- const inner = `${tsx} ${entryFile} ${sessionId}`
207
+ // Quote paths for Windows (handles spaces in paths like AppData\Roaming\npm\...)
208
+ const q = isWin ? (p) => `"${p}"` : (p) => p
209
+ const inner = isWin
210
+ ? `${q(tsx)} ${q(entryFile)} ${sessionId}`
211
+ : `${tsx} ${entryFile} ${sessionId}`
203
212
  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
213
+ case 'wt': return ['wt', 'new-tab', '--', 'cmd.exe', '/k', inner]
214
+ case 'cmd': return ['cmd.exe', '/c', `start cmd.exe /k "${inner}"`]
215
+ case 'gnome-terminal': return ['gnome-terminal', '--', 'bash', '-c', inner]
216
+ case 'xfce4-terminal': return ['xfce4-terminal', '-e', inner]
217
+ case 'konsole': return ['konsole', '-e', inner]
218
+ case 'xterm': return ['xterm', '-e', inner]
219
+ case 'alacritty': return ['alacritty', '-e', 'bash', '-c', inner]
220
+ case 'wezterm': return ['wezterm', 'start', '--', 'bash', '-c', inner]
221
+ case 'kitty': return ['kitty', 'bash', '-c', inner]
222
+ default: return null
212
223
  }
213
224
  }
214
225
 
@@ -218,7 +229,7 @@ function trySpawn(term, tsx, entryFile, sessionId) {
218
229
  return new Promise((resolve) => {
219
230
  let done = false
220
231
  try {
221
- const child = spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore' })
232
+ const child = spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore', shell: isWin })
222
233
  child.on('error', () => { if (!done) { done = true; resolve(false) } })
223
234
  child.on('spawn', () => { if (!done) { done = true; child.unref(); resolve(true) } })
224
235
  setTimeout(() => { if (!done) { done = true; child.unref(); resolve(true) } }, 300)
package/install.mjs CHANGED
@@ -15,6 +15,8 @@ const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json')
15
15
 
16
16
  export function runInstall() {
17
17
  const nodeExec = process.execPath
18
+ // Quote paths containing spaces (common on Windows)
19
+ const q = (p) => p.includes(' ') ? `"${p}"` : p
18
20
 
19
21
  const hookDefs = {
20
22
  PreToolUse: [
@@ -22,7 +24,7 @@ export function runInstall() {
22
24
  matcher: '.*',
23
25
  hooks: [{
24
26
  type: 'command',
25
- command: `${nodeExec} ${join(HOOKS_DIR, 'preToolUse.mjs')}`,
27
+ command: `${q(nodeExec)} ${q(join(HOOKS_DIR, 'preToolUse.mjs'))}`,
26
28
  }],
27
29
  },
28
30
  ],
@@ -30,7 +32,7 @@ export function runInstall() {
30
32
  {
31
33
  hooks: [{
32
34
  type: 'command',
33
- command: `${nodeExec} ${join(HOOKS_DIR, 'notification.mjs')}`,
35
+ command: `${q(nodeExec)} ${q(join(HOOKS_DIR, 'notification.mjs'))}`,
34
36
  }],
35
37
  },
36
38
  ],
@@ -38,7 +40,7 @@ export function runInstall() {
38
40
  {
39
41
  hooks: [{
40
42
  type: 'command',
41
- command: `${nodeExec} ${join(HOOKS_DIR, 'stop.mjs')}`,
43
+ command: `${q(nodeExec)} ${q(join(HOOKS_DIR, 'stop.mjs'))}`,
42
44
  }],
43
45
  },
44
46
  ],
@@ -107,6 +109,7 @@ function installSlashCommand() {
107
109
  function writePomodoroCommand(dest) {
108
110
  const pomodoroPath = join(__dirname, 'bin', 'pomodoro.mjs')
109
111
  const nodeExec = process.execPath
112
+ const q = (p) => p.includes(' ') ? `"${p}"` : p
110
113
  writeFileSync(dest, `---
111
114
  description: Start a Pomodoro focus session — agent works autonomously until timer ends
112
115
  argument-hint: "[duration e.g. 25m] [task description]"
@@ -115,7 +118,7 @@ argument-hint: "[duration e.g. 25m] [task description]"
115
118
  ## Step 1: Start the Pomodoro timer
116
119
 
117
120
  \`\`\`bash
118
- ${nodeExec} ${pomodoroPath} start $ARGUMENTS
121
+ ${q(nodeExec)} ${q(pomodoroPath)} start $ARGUMENTS
119
122
  \`\`\`
120
123
 
121
124
  ## Step 2: Work autonomously during the focus session
@@ -135,6 +138,7 @@ The user has started a Pomodoro focus session and **will not be checking the scr
135
138
  function writeStatsCommand(dest) {
136
139
  const pomodoroPath = join(__dirname, 'bin', 'pomodoro.mjs')
137
140
  const nodeExec = process.execPath
141
+ const q = (p) => p.includes(' ') ? `"${p}"` : p
138
142
  writeFileSync(dest, `---
139
143
  description: Show Pomodoro time tracking statistics
140
144
  ---
@@ -142,7 +146,7 @@ description: Show Pomodoro time tracking statistics
142
146
  Run the following command and display the output to the user:
143
147
 
144
148
  \`\`\`bash
145
- ${nodeExec} ${pomodoroPath} stats
149
+ ${q(nodeExec)} ${q(pomodoroPath)} stats
146
150
  \`\`\`
147
151
  `, 'utf8')
148
152
  }
@@ -150,6 +154,7 @@ ${nodeExec} ${pomodoroPath} stats
150
154
  function writeStopCommand(dest) {
151
155
  const pomodoroPath = join(__dirname, 'bin', 'pomodoro.mjs')
152
156
  const nodeExec = process.execPath
157
+ const q = (p) => p.includes(' ') ? `"${p}"` : p
153
158
  writeFileSync(dest, `---
154
159
  description: Break (stop) the current Pomodoro session
155
160
  ---
@@ -157,7 +162,7 @@ description: Break (stop) the current Pomodoro session
157
162
  Run the following command to break the active Pomodoro session:
158
163
 
159
164
  \`\`\`bash
160
- ${nodeExec} ${pomodoroPath} stop
165
+ ${q(nodeExec)} ${q(pomodoroPath)} stop
161
166
  \`\`\`
162
167
 
163
168
  Then confirm to the user that the session has been stopped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-pomo",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
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
5
  "type": "module",
6
6
  "bin": {
@@ -20,8 +20,8 @@ export class IpcServer {
20
20
  }
21
21
 
22
22
  async listen(socketPath) {
23
- // Clean up stale socket file
24
- if (existsSync(socketPath)) {
23
+ // Clean up stale socket file (Unix only — Windows named pipes don't need this)
24
+ if (process.platform !== 'win32' && existsSync(socketPath)) {
25
25
  unlinkSync(socketPath)
26
26
  }
27
27
 
@@ -5,7 +5,10 @@ import { join } from 'node:path'
5
5
 
6
6
  const CLAUDE_DIR = join(homedir(), '.claude')
7
7
  const LOCK_PATH = join(CLAUDE_DIR, 'pomodoro.lock')
8
- const SOCKET_PATH = join(CLAUDE_DIR, 'pomodoro.sock')
8
+ // Windows requires named pipe paths; Unix uses a socket file
9
+ const SOCKET_PATH = process.platform === 'win32'
10
+ ? '\\\\.\\pipe\\vibe-pomo'
11
+ : join(CLAUDE_DIR, 'pomodoro.sock')
9
12
 
10
13
  export function getSocketPath() {
11
14
  return SOCKET_PATH
@@ -30,7 +33,8 @@ export function readLock() {
30
33
  process.kill(data.pid, 0)
31
34
  } catch {
32
35
  try { unlinkSync(LOCK_PATH) } catch {}
33
- try { unlinkSync(SOCKET_PATH) } catch {}
36
+ // Named pipes on Windows are kernel-managed; only unlink on Unix
37
+ if (process.platform !== 'win32') try { unlinkSync(SOCKET_PATH) } catch {}
34
38
  return null
35
39
  }
36
40
  return data
@@ -42,7 +46,8 @@ export function writeLock(data) {
42
46
 
43
47
  export function removeLock() {
44
48
  try { unlinkSync(LOCK_PATH) } catch {}
45
- try { unlinkSync(SOCKET_PATH) } catch {}
49
+ // Named pipes on Windows are kernel-managed; only unlink on Unix
50
+ if (process.platform !== 'win32') try { unlinkSync(SOCKET_PATH) } catch {}
46
51
  }
47
52
 
48
53
  /** Stable short ID for a project directory */