vibe-pomo 0.1.4 → 0.2.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/package.json +1 -3
- package/src/daemon/db.mjs +102 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-pomo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|
|
@@ -33,14 +33,12 @@
|
|
|
33
33
|
"tui": "tsx src/tui/index.tsx"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"better-sqlite3": "^9.4.3",
|
|
37
36
|
"ink": "^5.1.0",
|
|
38
37
|
"ink-text-input": "^6.0.0",
|
|
39
38
|
"react": "^18.3.1",
|
|
40
39
|
"tsx": "^4.19.2"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
|
-
"@types/better-sqlite3": "^7.6.8",
|
|
44
42
|
"@types/react": "^18.3.1"
|
|
45
43
|
},
|
|
46
44
|
"engines": {
|
package/src/daemon/db.mjs
CHANGED
|
@@ -1,146 +1,130 @@
|
|
|
1
|
-
import Database from 'better-sqlite3'
|
|
2
1
|
import { homedir } from 'node:os'
|
|
3
2
|
import { join } from 'node:path'
|
|
4
|
-
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
5
4
|
|
|
6
|
-
const
|
|
5
|
+
const DATA_PATH = join(homedir(), '.claude', 'pomodoro-data.json')
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
// ── In-memory store (loaded once, flushed on every write) ─────────────────────
|
|
8
|
+
|
|
9
|
+
let _data = null
|
|
10
|
+
|
|
11
|
+
function load() {
|
|
12
|
+
if (_data) return _data
|
|
9
13
|
mkdirSync(join(homedir(), '.claude'), { recursive: true })
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
db.exec(`
|
|
14
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
15
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
-
project_hash TEXT NOT NULL,
|
|
17
|
-
project_path TEXT NOT NULL,
|
|
18
|
-
started_at INTEGER NOT NULL,
|
|
19
|
-
ended_at INTEGER,
|
|
20
|
-
planned_ms INTEGER NOT NULL,
|
|
21
|
-
actual_ms INTEGER,
|
|
22
|
-
cycles INTEGER DEFAULT 1,
|
|
23
|
-
status TEXT NOT NULL,
|
|
24
|
-
task TEXT,
|
|
25
|
-
agent_summary TEXT, -- from .claude/pomodoro-summary.md
|
|
26
|
-
user_activity TEXT -- what the user did during this session (entered at session end)
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
-- Migrate existing tables: add new columns if they don't exist yet
|
|
30
|
-
-- (SQLite doesn't support IF NOT EXISTS for columns, so we use a safe approach)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
CREATE TABLE IF NOT EXISTS notifications (
|
|
34
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
-
session_id INTEGER REFERENCES sessions(id),
|
|
36
|
-
received_at INTEGER NOT NULL,
|
|
37
|
-
released_at INTEGER,
|
|
38
|
-
notification_json TEXT NOT NULL
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
CREATE VIEW IF NOT EXISTS project_stats AS
|
|
42
|
-
SELECT
|
|
43
|
-
project_path,
|
|
44
|
-
COUNT(*) AS total_sessions,
|
|
45
|
-
SUM(cycles) AS total_cycles,
|
|
46
|
-
SUM(actual_ms) / 60000 AS total_minutes,
|
|
47
|
-
MAX(started_at) AS last_active
|
|
48
|
-
FROM sessions
|
|
49
|
-
WHERE status != 'broken'
|
|
50
|
-
GROUP BY project_path;
|
|
51
|
-
`)
|
|
52
|
-
|
|
53
|
-
// Safe column migrations for existing databases
|
|
54
|
-
const cols = db.prepare(`PRAGMA table_info(sessions)`).all().map((c) => c.name)
|
|
55
|
-
if (!cols.includes('agent_summary')) {
|
|
56
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN agent_summary TEXT`)
|
|
14
|
+
if (existsSync(DATA_PATH)) {
|
|
15
|
+
try { _data = JSON.parse(readFileSync(DATA_PATH, 'utf8')) } catch {}
|
|
57
16
|
}
|
|
58
|
-
if (!
|
|
59
|
-
|
|
17
|
+
if (!_data) _data = { sessions: [], notifications: [], nextId: 1 }
|
|
18
|
+
// Backfill nextId in case file was created without it
|
|
19
|
+
if (!_data.nextId) {
|
|
20
|
+
const maxId = Math.max(0, ...(_data.sessions ?? []).map((s) => s.id ?? 0))
|
|
21
|
+
_data.nextId = maxId + 1
|
|
60
22
|
}
|
|
61
|
-
|
|
62
|
-
return db
|
|
23
|
+
return _data
|
|
63
24
|
}
|
|
64
25
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!_db) _db = openDb()
|
|
68
|
-
return _db
|
|
26
|
+
function flush() {
|
|
27
|
+
writeFileSync(DATA_PATH, JSON.stringify(_data, null, 2), 'utf8')
|
|
69
28
|
}
|
|
70
29
|
|
|
30
|
+
// ── Public API (mirrors the old better-sqlite3 interface exactly) ─────────────
|
|
31
|
+
|
|
71
32
|
export function startSession({ projectHash, projectPath, plannedMs, task }) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
33
|
+
const data = load()
|
|
34
|
+
const id = data.nextId++
|
|
35
|
+
data.sessions.push({
|
|
36
|
+
id,
|
|
37
|
+
project_hash: projectHash,
|
|
38
|
+
project_path: projectPath,
|
|
39
|
+
started_at: Date.now(),
|
|
40
|
+
ended_at: null,
|
|
41
|
+
planned_ms: plannedMs,
|
|
42
|
+
actual_ms: null,
|
|
43
|
+
cycles: 1,
|
|
44
|
+
status: 'running',
|
|
45
|
+
task: task ?? null,
|
|
46
|
+
agent_summary: null,
|
|
47
|
+
user_activity: null,
|
|
48
|
+
})
|
|
49
|
+
flush()
|
|
50
|
+
return id
|
|
78
51
|
}
|
|
79
52
|
|
|
80
|
-
export function endSession(
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
53
|
+
export function endSession(id, { actualMs, status, agentSummary }) {
|
|
54
|
+
const data = load()
|
|
55
|
+
const s = data.sessions.find((s) => s.id === id)
|
|
56
|
+
if (!s) return
|
|
57
|
+
s.ended_at = Date.now()
|
|
58
|
+
s.actual_ms = actualMs
|
|
59
|
+
s.status = status
|
|
60
|
+
s.agent_summary = agentSummary ?? null
|
|
61
|
+
flush()
|
|
87
62
|
}
|
|
88
63
|
|
|
89
|
-
export function updateSessionActivity(
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
64
|
+
export function updateSessionActivity(id, { userActivity }) {
|
|
65
|
+
const data = load()
|
|
66
|
+
const s = data.sessions.find((s) => s.id === id)
|
|
67
|
+
if (!s) return
|
|
68
|
+
s.user_activity = userActivity ?? null
|
|
69
|
+
flush()
|
|
93
70
|
}
|
|
94
71
|
|
|
95
72
|
export function saveNotification(sessionId, { notificationJson, receivedAt, releasedAt }) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
73
|
+
const data = load()
|
|
74
|
+
data.notifications.push({
|
|
75
|
+
id: data.nextId++,
|
|
76
|
+
session_id: sessionId,
|
|
77
|
+
received_at: receivedAt,
|
|
78
|
+
released_at: releasedAt ?? null,
|
|
79
|
+
notification_json: notificationJson,
|
|
80
|
+
})
|
|
81
|
+
flush()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Aggregation helpers ───────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function projectStats() {
|
|
87
|
+
const data = load()
|
|
88
|
+
const map = new Map()
|
|
89
|
+
for (const s of data.sessions) {
|
|
90
|
+
if (s.status === 'broken') continue
|
|
91
|
+
const key = s.project_path
|
|
92
|
+
if (!map.has(key)) map.set(key, { project_path: key, total_sessions: 0, total_cycles: 0, total_minutes: 0, last_active: 0 })
|
|
93
|
+
const row = map.get(key)
|
|
94
|
+
row.total_sessions++
|
|
95
|
+
row.total_cycles += s.cycles ?? 1
|
|
96
|
+
row.total_minutes += Math.floor((s.actual_ms ?? 0) / 60000)
|
|
97
|
+
if (s.started_at > row.last_active) row.last_active = s.started_at
|
|
98
|
+
}
|
|
99
|
+
return [...map.values()].sort((a, b) => b.last_active - a.last_active)
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
/** Return stats for Dashboard TUI */
|
|
104
103
|
export function getStats() {
|
|
105
|
-
const
|
|
106
|
-
const projects =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
SELECT project_path, task, actual_ms, status, started_at, agent_summary, user_activity
|
|
113
|
-
FROM sessions ORDER BY started_at DESC LIMIT 8
|
|
114
|
-
`).all()
|
|
115
|
-
|
|
104
|
+
const data = load()
|
|
105
|
+
const projects = projectStats().slice(0, 10)
|
|
106
|
+
const recent = [...data.sessions]
|
|
107
|
+
.sort((a, b) => b.started_at - a.started_at)
|
|
108
|
+
.slice(0, 8)
|
|
109
|
+
.map(({ project_path, task, actual_ms, status, started_at, agent_summary, user_activity }) =>
|
|
110
|
+
({ project_path, task, actual_ms, status, started_at, agent_summary, user_activity }))
|
|
116
111
|
return { projects, recent }
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
/** Return all sessions for a project, for detailed review */
|
|
120
115
|
export function getProjectSessions(projectPath) {
|
|
121
|
-
const
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`).all(projectPath)
|
|
116
|
+
const data = load()
|
|
117
|
+
return data.sessions
|
|
118
|
+
.filter((s) => s.project_path === projectPath)
|
|
119
|
+
.sort((a, b) => b.started_at - a.started_at)
|
|
120
|
+
.map(({ id, task, started_at, ended_at, actual_ms, status, agent_summary, user_activity }) =>
|
|
121
|
+
({ id, task, started_at, ended_at, actual_ms, status, agent_summary, user_activity }))
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
/** Print stats to stdout as a formatted table */
|
|
131
125
|
export function printStats() {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const projects = db.prepare(`
|
|
135
|
-
SELECT
|
|
136
|
-
project_path,
|
|
137
|
-
total_sessions,
|
|
138
|
-
total_cycles,
|
|
139
|
-
total_minutes,
|
|
140
|
-
last_active
|
|
141
|
-
FROM project_stats
|
|
142
|
-
ORDER BY last_active DESC
|
|
143
|
-
`).all()
|
|
126
|
+
const data = load()
|
|
127
|
+
const projects = projectStats()
|
|
144
128
|
|
|
145
129
|
if (projects.length === 0) {
|
|
146
130
|
console.log('No completed sessions yet.')
|
|
@@ -165,18 +149,14 @@ export function printStats() {
|
|
|
165
149
|
truncated.padEnd(36) +
|
|
166
150
|
String(row.total_sessions).padStart(9) +
|
|
167
151
|
String(row.total_cycles).padStart(8) +
|
|
168
|
-
String(row.total_minutes
|
|
152
|
+
String(row.total_minutes).padStart(9) +
|
|
169
153
|
` ${date}`
|
|
170
154
|
)
|
|
171
155
|
}
|
|
172
156
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
FROM sessions
|
|
177
|
-
ORDER BY started_at DESC
|
|
178
|
-
LIMIT 10
|
|
179
|
-
`).all()
|
|
157
|
+
const recent = [...data.sessions]
|
|
158
|
+
.sort((a, b) => b.started_at - a.started_at)
|
|
159
|
+
.slice(0, 10)
|
|
180
160
|
|
|
181
161
|
console.log('\nRecent Sessions\n' + '─'.repeat(72))
|
|
182
162
|
console.log(
|
|
@@ -191,17 +171,15 @@ export function printStats() {
|
|
|
191
171
|
for (const s of recent) {
|
|
192
172
|
const taskLabel = (s.task ?? '(no task)').slice(0, 37).padEnd(38)
|
|
193
173
|
const mins = s.actual_ms ? String(Math.round(s.actual_ms / 60000)).padStart(5) : ' —'
|
|
194
|
-
const cyc = String(s.cycles).padStart(5)
|
|
174
|
+
const cyc = String(s.cycles ?? 1).padStart(5)
|
|
195
175
|
const status = s.status.padEnd(12)
|
|
196
176
|
const date = new Date(s.started_at).toLocaleDateString()
|
|
197
177
|
console.log(`${taskLabel}${mins}${cyc} ${status} ${date}`)
|
|
198
178
|
if (s.agent_summary) {
|
|
199
|
-
|
|
200
|
-
console.log(` 🤖 ${line}`)
|
|
179
|
+
console.log(` 🤖 ${s.agent_summary.split('\n')[0].slice(0, 68)}`)
|
|
201
180
|
}
|
|
202
181
|
if (s.user_activity) {
|
|
203
|
-
|
|
204
|
-
console.log(` 👤 ${line}`)
|
|
182
|
+
console.log(` 👤 ${s.user_activity.slice(0, 68)}`)
|
|
205
183
|
}
|
|
206
184
|
}
|
|
207
185
|
|