vibe-pomo 0.1.3 → 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/install.mjs +58 -81
- package/package.json +1 -3
- package/src/daemon/db.mjs +102 -124
package/install.mjs
CHANGED
|
@@ -7,45 +7,45 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
8
|
import { join, dirname } from 'node:path'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
|
-
import { createRequire } from 'node:module'
|
|
11
10
|
|
|
12
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
12
|
const HOOKS_DIR = join(__dirname, 'src', 'hooks')
|
|
14
13
|
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json')
|
|
15
14
|
|
|
15
|
+
// Normalize path separators for bash compatibility on Windows.
|
|
16
|
+
// Claude Code runs hooks via bash even on Windows, so backslashes must
|
|
17
|
+
// become forward slashes. E.g. E:\path\node.exe → E:/path/node.exe
|
|
18
|
+
const normPath = (p) => p.replace(/\\/g, '/')
|
|
19
|
+
|
|
20
|
+
// Quote a path only if it contains spaces.
|
|
21
|
+
const q = (p) => { const n = normPath(p); return n.includes(' ') ? `"${n}"` : n }
|
|
22
|
+
|
|
23
|
+
// The hook script files vibe-pomo owns — used to detect and replace stale entries.
|
|
24
|
+
const HOOK_SCRIPTS = ['preToolUse.mjs', 'notification.mjs', 'stop.mjs']
|
|
25
|
+
|
|
26
|
+
function isVibePomоHook(hookEntry) {
|
|
27
|
+
return hookEntry.hooks?.some((h) =>
|
|
28
|
+
HOOK_SCRIPTS.some((f) => h.command?.includes(f))
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
export function runInstall() {
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const q = (p) => { const n = normPath(p); return n.includes(' ') ? `"${n}"` : n }
|
|
33
|
+
// Use 'node' from PATH rather than process.execPath so the command works
|
|
34
|
+
// across all shells (bash, PowerShell, cmd) without platform-specific path issues.
|
|
35
|
+
const nodeCmd = 'node'
|
|
36
|
+
const scriptPath = (name) => q(join(HOOKS_DIR, name))
|
|
22
37
|
|
|
23
38
|
const hookDefs = {
|
|
24
|
-
PreToolUse: [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
hooks: [{
|
|
36
|
-
type: 'command',
|
|
37
|
-
command: `${q(nodeExec)} ${q(join(HOOKS_DIR, 'notification.mjs'))}`,
|
|
38
|
-
}],
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
Stop: [
|
|
42
|
-
{
|
|
43
|
-
hooks: [{
|
|
44
|
-
type: 'command',
|
|
45
|
-
command: `${q(nodeExec)} ${q(join(HOOKS_DIR, 'stop.mjs'))}`,
|
|
46
|
-
}],
|
|
47
|
-
},
|
|
48
|
-
],
|
|
39
|
+
PreToolUse: [{
|
|
40
|
+
matcher: '.*',
|
|
41
|
+
hooks: [{ type: 'command', command: `${nodeCmd} ${scriptPath('preToolUse.mjs')}` }],
|
|
42
|
+
}],
|
|
43
|
+
Notification: [{
|
|
44
|
+
hooks: [{ type: 'command', command: `${nodeCmd} ${scriptPath('notification.mjs')}` }],
|
|
45
|
+
}],
|
|
46
|
+
Stop: [{
|
|
47
|
+
hooks: [{ type: 'command', command: `${nodeCmd} ${scriptPath('stop.mjs')}` }],
|
|
48
|
+
}],
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Read existing settings
|
|
@@ -61,33 +61,21 @@ export function runInstall() {
|
|
|
61
61
|
mkdirSync(dirname(SETTINGS_PATH), { recursive: true })
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Merge hooks — avoid duplicates by checking command paths
|
|
65
64
|
settings.hooks = settings.hooks ?? {}
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
// Replace any existing vibe-pomo hooks (removes stale/duplicate entries
|
|
67
|
+
// from previous installs that may have used backslash paths or full node paths).
|
|
68
68
|
for (const [event, defs] of Object.entries(hookDefs)) {
|
|
69
|
-
settings.hooks[event] = settings.hooks[event] ?? []
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
(existing) => existing.hooks?.some((h) => h.command === commandToAdd)
|
|
74
|
-
)
|
|
75
|
-
if (!alreadyRegistered) {
|
|
76
|
-
settings.hooks[event].push(def)
|
|
77
|
-
added++
|
|
78
|
-
}
|
|
79
|
-
}
|
|
69
|
+
settings.hooks[event] = (settings.hooks[event] ?? []).filter(
|
|
70
|
+
(entry) => !isVibePomоHook(entry)
|
|
71
|
+
)
|
|
72
|
+
settings.hooks[event].push(...defs)
|
|
80
73
|
}
|
|
81
74
|
|
|
82
75
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8')
|
|
76
|
+
console.log(`✓ Registered hooks in ${SETTINGS_PATH}`)
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
console.log(`✓ Registered ${added} hook(s) in ${SETTINGS_PATH}`)
|
|
86
|
-
} else {
|
|
87
|
-
console.log(`Hooks already registered in ${SETTINGS_PATH}`)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Also install project-local slash command
|
|
78
|
+
// Also install project-local slash commands
|
|
91
79
|
installSlashCommand()
|
|
92
80
|
}
|
|
93
81
|
|
|
@@ -95,25 +83,22 @@ function installSlashCommand() {
|
|
|
95
83
|
const globalCommandsDir = join(homedir(), '.claude', 'commands')
|
|
96
84
|
mkdirSync(globalCommandsDir, { recursive: true })
|
|
97
85
|
|
|
86
|
+
const pomodoroPath = q(join(__dirname, 'bin', 'pomodoro.mjs'))
|
|
87
|
+
|
|
98
88
|
const commands = [
|
|
99
|
-
{ file: 'pomodoro.md',
|
|
100
|
-
{ file: 'pomodoro-stats.md',
|
|
101
|
-
{ file: 'pomodoro-stop.md',
|
|
89
|
+
{ file: 'pomodoro.md', content: pomodoroCommand(pomodoroPath) },
|
|
90
|
+
{ file: 'pomodoro-stats.md', content: statsCommand(pomodoroPath) },
|
|
91
|
+
{ file: 'pomodoro-stop.md', content: stopCommand(pomodoroPath) },
|
|
102
92
|
]
|
|
103
93
|
|
|
104
|
-
for (const { file,
|
|
105
|
-
|
|
106
|
-
writer(dest)
|
|
94
|
+
for (const { file, content } of commands) {
|
|
95
|
+
writeFileSync(join(globalCommandsDir, file), content, 'utf8')
|
|
107
96
|
console.log(`✓ Slash command: ~/.claude/commands/${file}`)
|
|
108
97
|
}
|
|
109
98
|
}
|
|
110
99
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
const nodeExec = process.execPath
|
|
114
|
-
const normPath = (p) => p.replace(/\\/g, '/')
|
|
115
|
-
const q = (p) => { const n = normPath(p); return n.includes(' ') ? `"${n}"` : n }
|
|
116
|
-
writeFileSync(dest, `---
|
|
100
|
+
function pomodoroCommand(pomodoroPath) {
|
|
101
|
+
return `---
|
|
117
102
|
description: Start a Pomodoro focus session — agent works autonomously until timer ends
|
|
118
103
|
argument-hint: "[duration e.g. 25m] [task description]"
|
|
119
104
|
---
|
|
@@ -121,7 +106,7 @@ argument-hint: "[duration e.g. 25m] [task description]"
|
|
|
121
106
|
## Step 1: Start the Pomodoro timer
|
|
122
107
|
|
|
123
108
|
\`\`\`bash
|
|
124
|
-
|
|
109
|
+
node ${pomodoroPath} start $ARGUMENTS
|
|
125
110
|
\`\`\`
|
|
126
111
|
|
|
127
112
|
## Step 2: Work autonomously during the focus session
|
|
@@ -135,43 +120,35 @@ The user has started a Pomodoro focus session and **will not be checking the scr
|
|
|
135
120
|
- Do not send notifications or ask questions — the user is in focus mode
|
|
136
121
|
- When you have done all you can, write a summary to \`.claude/pomodoro-summary.md\`
|
|
137
122
|
- Then wait quietly — the Pomodoro hook will manage the session lifecycle
|
|
138
|
-
|
|
123
|
+
`
|
|
139
124
|
}
|
|
140
125
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
const nodeExec = process.execPath
|
|
144
|
-
const normPath = (p) => p.replace(/\\/g, '/')
|
|
145
|
-
const q = (p) => { const n = normPath(p); return n.includes(' ') ? `"${n}"` : n }
|
|
146
|
-
writeFileSync(dest, `---
|
|
126
|
+
function statsCommand(pomodoroPath) {
|
|
127
|
+
return `---
|
|
147
128
|
description: Show Pomodoro time tracking statistics
|
|
148
129
|
---
|
|
149
130
|
|
|
150
131
|
Run the following command and display the output to the user:
|
|
151
132
|
|
|
152
133
|
\`\`\`bash
|
|
153
|
-
|
|
134
|
+
node ${pomodoroPath} stats
|
|
154
135
|
\`\`\`
|
|
155
|
-
|
|
136
|
+
`
|
|
156
137
|
}
|
|
157
138
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
const nodeExec = process.execPath
|
|
161
|
-
const normPath = (p) => p.replace(/\\/g, '/')
|
|
162
|
-
const q = (p) => { const n = normPath(p); return n.includes(' ') ? `"${n}"` : n }
|
|
163
|
-
writeFileSync(dest, `---
|
|
139
|
+
function stopCommand(pomodoroPath) {
|
|
140
|
+
return `---
|
|
164
141
|
description: Break (stop) the current Pomodoro session
|
|
165
142
|
---
|
|
166
143
|
|
|
167
144
|
Run the following command to break the active Pomodoro session:
|
|
168
145
|
|
|
169
146
|
\`\`\`bash
|
|
170
|
-
|
|
147
|
+
node ${pomodoroPath} stop
|
|
171
148
|
\`\`\`
|
|
172
149
|
|
|
173
150
|
Then confirm to the user that the session has been stopped.
|
|
174
|
-
|
|
151
|
+
`
|
|
175
152
|
}
|
|
176
153
|
|
|
177
154
|
// Run directly if called as script
|
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
|
|