prjct-cli 0.10.14 → 0.11.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.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * SessionMetrics - Metrics Calculation and Aggregation
3
+ *
4
+ * Calculates productivity metrics from session data.
5
+ *
6
+ * @version 1.0.0
7
+ */
8
+
9
+ const fs = require('fs').promises
10
+ const path = require('path')
11
+ const pathManager = require('../infrastructure/path-manager')
12
+
13
+ class SessionMetrics {
14
+ constructor(projectId) {
15
+ this.projectId = projectId
16
+ }
17
+
18
+ /**
19
+ * Get aggregated metrics for a time period
20
+ * @param {'day'|'week'|'month'|'all'} period
21
+ * @returns {Promise<Object>}
22
+ */
23
+ async getMetrics(period = 'week') {
24
+ const sessions = await this.getSessionsForPeriod(period)
25
+
26
+ return {
27
+ period,
28
+ totalSessions: sessions.length,
29
+ totalDuration: this.sumDurations(sessions),
30
+ totalDurationFormatted: this.formatDuration(this.sumDurations(sessions)),
31
+ averageDuration: this.averageDuration(sessions),
32
+ averageDurationFormatted: this.formatDuration(this.averageDuration(sessions)),
33
+ tasksCompleted: sessions.filter(s => s.status === 'completed').length,
34
+ filesChanged: this.sumMetric(sessions, 'filesChanged'),
35
+ linesAdded: this.sumMetric(sessions, 'linesAdded'),
36
+ linesRemoved: this.sumMetric(sessions, 'linesRemoved'),
37
+ commits: this.sumMetric(sessions, 'commits'),
38
+ productivityScore: this.calculateProductivityScore(sessions),
39
+ streak: await this.calculateStreak(),
40
+ byDay: this.groupByDay(sessions)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get sessions for a given period
46
+ * @param {string} period
47
+ * @returns {Promise<Array>}
48
+ */
49
+ async getSessionsForPeriod(period) {
50
+ const globalPath = pathManager.getGlobalProjectPath(this.projectId)
51
+ const archiveDir = path.join(globalPath, 'sessions', 'archive')
52
+
53
+ const sessions = []
54
+ const cutoffDate = this.getCutoffDate(period)
55
+
56
+ try {
57
+ const months = await fs.readdir(archiveDir)
58
+
59
+ for (const month of months) {
60
+ const monthDir = path.join(archiveDir, month)
61
+ const files = await fs.readdir(monthDir)
62
+
63
+ for (const file of files) {
64
+ if (!file.endsWith('.json')) continue
65
+
66
+ const content = await fs.readFile(path.join(monthDir, file), 'utf-8')
67
+ const session = JSON.parse(content)
68
+
69
+ if (new Date(session.completedAt) >= cutoffDate) {
70
+ sessions.push(session)
71
+ }
72
+ }
73
+ }
74
+ } catch {
75
+ // Archive might not exist
76
+ }
77
+
78
+ // Also check current session
79
+ try {
80
+ const currentPath = path.join(globalPath, 'sessions', 'current.json')
81
+ const content = await fs.readFile(currentPath, 'utf-8')
82
+ const current = JSON.parse(content)
83
+ if (new Date(current.startedAt) >= cutoffDate) {
84
+ sessions.push(current)
85
+ }
86
+ } catch {
87
+ // No current session
88
+ }
89
+
90
+ return sessions
91
+ }
92
+
93
+ /**
94
+ * Get cutoff date for period
95
+ * @param {string} period
96
+ * @returns {Date}
97
+ */
98
+ getCutoffDate(period) {
99
+ const now = new Date()
100
+
101
+ switch (period) {
102
+ case 'day':
103
+ return new Date(now.setHours(0, 0, 0, 0))
104
+ case 'week':
105
+ const weekAgo = new Date(now)
106
+ weekAgo.setDate(weekAgo.getDate() - 7)
107
+ return weekAgo
108
+ case 'month':
109
+ const monthAgo = new Date(now)
110
+ monthAgo.setMonth(monthAgo.getMonth() - 1)
111
+ return monthAgo
112
+ case 'all':
113
+ default:
114
+ return new Date(0) // Beginning of time
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Sum durations from sessions
120
+ * @param {Array} sessions
121
+ * @returns {number} Total seconds
122
+ */
123
+ sumDurations(sessions) {
124
+ return sessions.reduce((sum, s) => sum + (s.duration || 0), 0)
125
+ }
126
+
127
+ /**
128
+ * Calculate average duration
129
+ * @param {Array} sessions
130
+ * @returns {number} Average seconds
131
+ */
132
+ averageDuration(sessions) {
133
+ if (sessions.length === 0) return 0
134
+ return Math.round(this.sumDurations(sessions) / sessions.length)
135
+ }
136
+
137
+ /**
138
+ * Sum a specific metric
139
+ * @param {Array} sessions
140
+ * @param {string} metric
141
+ * @returns {number}
142
+ */
143
+ sumMetric(sessions, metric) {
144
+ return sessions.reduce((sum, s) => {
145
+ return sum + (s.metrics?.[metric] || 0)
146
+ }, 0)
147
+ }
148
+
149
+ /**
150
+ * Calculate productivity score (0-100)
151
+ * Based on consistency, duration, and output
152
+ * @param {Array} sessions
153
+ * @returns {number}
154
+ */
155
+ calculateProductivityScore(sessions) {
156
+ if (sessions.length === 0) return 0
157
+
158
+ // Factors:
159
+ // 1. Session count (more = better, up to a point)
160
+ const sessionScore = Math.min(sessions.length / 10, 1) * 30
161
+
162
+ // 2. Average duration (25-90 min is optimal)
163
+ const avgMins = this.averageDuration(sessions) / 60
164
+ let durationScore = 0
165
+ if (avgMins >= 25 && avgMins <= 90) {
166
+ durationScore = 30
167
+ } else if (avgMins > 0) {
168
+ durationScore = Math.max(0, 30 - Math.abs(avgMins - 57.5) / 2)
169
+ }
170
+
171
+ // 3. Completion rate
172
+ const completedCount = sessions.filter(s => s.status === 'completed').length
173
+ const completionScore = (completedCount / sessions.length) * 20
174
+
175
+ // 4. Output (commits + files changed)
176
+ const totalOutput = this.sumMetric(sessions, 'commits') + this.sumMetric(sessions, 'filesChanged')
177
+ const outputScore = Math.min(totalOutput / 50, 1) * 20
178
+
179
+ return Math.round(sessionScore + durationScore + completionScore + outputScore)
180
+ }
181
+
182
+ /**
183
+ * Calculate current streak (consecutive days with sessions)
184
+ * @returns {Promise<number>}
185
+ */
186
+ async calculateStreak() {
187
+ const sessions = await this.getSessionsForPeriod('month')
188
+
189
+ // Get unique dates with sessions
190
+ const dates = new Set(
191
+ sessions.map(s => {
192
+ const d = new Date(s.completedAt || s.startedAt)
193
+ return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`
194
+ })
195
+ )
196
+
197
+ // Count consecutive days from today
198
+ let streak = 0
199
+ const today = new Date()
200
+
201
+ for (let i = 0; i < 30; i++) {
202
+ const checkDate = new Date(today)
203
+ checkDate.setDate(checkDate.getDate() - i)
204
+ const key = `${checkDate.getFullYear()}-${checkDate.getMonth()}-${checkDate.getDate()}`
205
+
206
+ if (dates.has(key)) {
207
+ streak++
208
+ } else if (i > 0) {
209
+ break // Streak broken
210
+ }
211
+ }
212
+
213
+ return streak
214
+ }
215
+
216
+ /**
217
+ * Group sessions by day
218
+ * @param {Array} sessions
219
+ * @returns {Object}
220
+ */
221
+ groupByDay(sessions) {
222
+ const byDay = {}
223
+
224
+ for (const session of sessions) {
225
+ const date = new Date(session.completedAt || session.startedAt)
226
+ const key = date.toISOString().split('T')[0]
227
+
228
+ if (!byDay[key]) {
229
+ byDay[key] = {
230
+ sessions: 0,
231
+ duration: 0,
232
+ commits: 0
233
+ }
234
+ }
235
+
236
+ byDay[key].sessions++
237
+ byDay[key].duration += session.duration || 0
238
+ byDay[key].commits += session.metrics?.commits || 0
239
+ }
240
+
241
+ return byDay
242
+ }
243
+
244
+ /**
245
+ * Format duration as human readable
246
+ * @param {number} seconds
247
+ * @returns {string}
248
+ */
249
+ formatDuration(seconds) {
250
+ if (seconds < 60) return `${seconds}s`
251
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`
252
+
253
+ const hours = Math.floor(seconds / 3600)
254
+ const minutes = Math.round((seconds % 3600) / 60)
255
+
256
+ if (minutes === 0) return `${hours}h`
257
+ return `${hours}h ${minutes}m`
258
+ }
259
+
260
+ /**
261
+ * Generate metrics summary for display
262
+ * @param {'day'|'week'|'month'} period
263
+ * @returns {Promise<string>}
264
+ */
265
+ async generateSummary(period = 'week') {
266
+ const m = await this.getMetrics(period)
267
+
268
+ const periodLabel = {
269
+ day: 'Today',
270
+ week: 'This Week',
271
+ month: 'This Month',
272
+ all: 'All Time'
273
+ }[period]
274
+
275
+ return `
276
+ ## ${periodLabel}
277
+
278
+ | Metric | Value |
279
+ |--------|-------|
280
+ | Sessions | ${m.totalSessions} |
281
+ | Total Time | ${m.totalDurationFormatted} |
282
+ | Avg Session | ${m.averageDurationFormatted} |
283
+ | Tasks Done | ${m.tasksCompleted} |
284
+ | Files Changed | ${m.filesChanged} |
285
+ | Lines +/- | +${m.linesAdded} / -${m.linesRemoved} |
286
+ | Commits | ${m.commits} |
287
+ | Streak | ${m.streak} days |
288
+ | Score | ${m.productivityScore}/100 |
289
+ `.trim()
290
+ }
291
+ }
292
+
293
+ module.exports = SessionMetrics
package/package.json CHANGED
@@ -1,16 +1,24 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.10.14",
3
+ "version": "0.11.0",
4
4
  "description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
7
- "prjct": "bin/prjct"
7
+ "prjct": "bin/prjct",
8
+ "prjct-dev": "bin/dev.js",
9
+ "prjct-serve": "bin/serve.js"
8
10
  },
11
+ "workspaces": [
12
+ "packages/*"
13
+ ],
9
14
  "publishConfig": {
10
15
  "access": "public",
11
16
  "registry": "https://registry.npmjs.org"
12
17
  },
13
18
  "scripts": {
19
+ "dev": "turbo run dev",
20
+ "build": "turbo run build",
21
+ "serve": "turbo run dev --filter=@prjct/server --filter=@prjct/web",
14
22
  "postinstall": "node scripts/postinstall.js",
15
23
  "update-commands": "node -e \"const installer = require('./core/infrastructure/command-installer'); installer.syncCommands().then(r => console.log('Commands updated:', r)).catch(e => console.error('Error:', e.message))\"",
16
24
  "install-global": "./scripts/install.sh",
@@ -41,6 +49,7 @@
41
49
  "dependencies": {
42
50
  "chalk": "^4.1.2",
43
51
  "glob": "^10.3.10",
52
+ "lightningcss": "^1.30.2",
44
53
  "prompts": "^2.4.2"
45
54
  },
46
55
  "devDependencies": {
@@ -52,6 +61,7 @@
52
61
  "eslint-plugin-n": "^16.6.2",
53
62
  "eslint-plugin-promise": "^6.6.0",
54
63
  "prettier": "^3.6.2",
64
+ "turbo": "^2.3.0",
55
65
  "vitest": "^3.2.4"
56
66
  },
57
67
  "repository": {
@@ -62,6 +72,7 @@
62
72
  "url": "https://github.com/jlopezlira/prjct-cli/issues"
63
73
  },
64
74
  "homepage": "https://prjct.app",
75
+ "packageManager": "npm@10.2.4",
65
76
  "engines": {
66
77
  "node": ">=18.0.0"
67
78
  },
@@ -79,5 +90,8 @@
79
90
  "trustedDependencies": [
80
91
  "chalk",
81
92
  "prompts"
82
- ]
83
- }
93
+ ],
94
+ "overrides": {
95
+ "esbuild": "^0.25.0"
96
+ }
97
+ }
@@ -1,15 +1,17 @@
1
1
  ---
2
- allowed-tools: [Read, Write]
3
- description: 'Complete current task'
2
+ allowed-tools: [Read, Write, Bash]
3
+ description: 'Complete current task with session metrics'
4
4
  timestamp-rule: 'GetTimestamp() for all timestamps'
5
5
  ---
6
6
 
7
- # /p:done - Complete Current Task
7
+ # /p:done - Complete Current Task with Session Metrics
8
8
 
9
9
  ## Context Variables
10
10
  - `{projectId}`: From `.prjct/prjct.config.json`
11
11
  - `{globalPath}`: `~/.prjct-cli/projects/{projectId}`
12
12
  - `{nowPath}`: `{globalPath}/core/now.md`
13
+ - `{sessionPath}`: `{globalPath}/sessions/current.json`
14
+ - `{archiveDir}`: `{globalPath}/sessions/archive`
13
15
  - `{memoryPath}`: `{globalPath}/memory/context.jsonl`
14
16
  - `{metricsPath}`: `{globalPath}/progress/metrics.md`
15
17
 
@@ -22,116 +24,236 @@ IF file not found:
22
24
  OUTPUT: "No prjct project. Run /p:init first."
23
25
  STOP
24
26
 
25
- ## Step 2: Validate Active Task
27
+ ## Step 2: Check Session State
26
28
 
29
+ ### Try structured session first
30
+ READ: `{sessionPath}`
31
+
32
+ IF file exists:
33
+ PARSE as JSON
34
+ EXTRACT: {session} object
35
+ GOTO Step 3 (Session Completion)
36
+
37
+ ### Fallback to legacy now.md
27
38
  READ: `{nowPath}`
28
39
 
29
40
  IF empty OR contains "No current task":
30
41
  OUTPUT: "⚠️ No active task to complete. Use /p:now to start one."
31
42
  STOP
32
43
 
33
- ## Step 3: Extract Task Data
44
+ ## Step 3: Session Completion
34
45
 
35
- From NOW file content, extract:
46
+ ### Calculate Final Duration
47
+ SET: {now} = GetTimestamp()
36
48
 
37
- 1. **Task name**: Text between `**` markers
38
- - Pattern: `**(.+?)**`
39
- - Example: `**implement auth**` → "implement auth"
49
+ For each event in {session.timeline}:
50
+ Track start/resume/pause/complete times
51
+ Calculate total active time
40
52
 
41
- 2. **Start time**: Text after "Started:"
42
- - Pattern: `Started: (.+)`
43
- - Example: `Started: 11/28/2025, 2:30:00 PM`
53
+ SET: {duration} = total active seconds
54
+ SET: {durationFormatted} = format as "Xh Ym" or "Xm"
44
55
 
45
- 3. **Calculate duration**:
46
- - Current: GetTimestamp()
47
- - Duration: current - started
48
- - Format: "Xh Ym" (e.g., "2h 15m")
49
- - If < 1 hour: "Xm"
50
- - If < 1 minute: "< 1m"
56
+ ### Calculate Git Metrics
57
+ BASH: `git rev-list --count --since="{session.startedAt}" HEAD 2>/dev/null || echo "0"`
58
+ CAPTURE as {commits}
51
59
 
52
- ## Step 4: Clear Task File
60
+ BASH: `git diff --stat HEAD~{commits} 2>/dev/null || git diff --stat`
61
+ PARSE output for:
62
+ - {filesChanged}: number of files
63
+ - {linesAdded}: insertions
64
+ - {linesRemoved}: deletions
53
65
 
54
- WRITE: `{nowPath}`
66
+ ### Update Session Object
67
+ ```json
68
+ {
69
+ "id": "{session.id}",
70
+ "projectId": "{projectId}",
71
+ "task": "{session.task}",
72
+ "status": "completed",
73
+ "startedAt": "{session.startedAt}",
74
+ "pausedAt": null,
75
+ "completedAt": "{now}",
76
+ "duration": {duration},
77
+ "metrics": {
78
+ "filesChanged": {filesChanged},
79
+ "linesAdded": {linesAdded},
80
+ "linesRemoved": {linesRemoved},
81
+ "commits": {commits},
82
+ "snapshots": {session.metrics.snapshots}
83
+ },
84
+ "timeline": [
85
+ ...{session.timeline},
86
+ {"type": "complete", "at": "{now}"}
87
+ ]
88
+ }
89
+ ```
90
+
91
+ ## Step 4: Archive Session
92
+
93
+ ### Create Archive Directory
94
+ GET: {yearMonth} = YYYY-MM from {now}
95
+ ENSURE: `{archiveDir}/{yearMonth}` exists
96
+
97
+ BASH: `mkdir -p {archiveDir}/{yearMonth}`
98
+
99
+ ### Write Archived Session
100
+ WRITE: `{archiveDir}/{yearMonth}/{session.id}.json`
101
+ Content: Updated session object from Step 3
102
+
103
+ ## Step 5: Clear Current Session
104
+
105
+ DELETE: `{sessionPath}`
55
106
 
56
- Content (exact):
107
+ OR WRITE empty state:
108
+ WRITE: `{sessionPath}`
109
+ Content:
110
+ ```json
111
+ {}
112
+ ```
113
+
114
+ ## Step 6: Update Legacy now.md
115
+
116
+ WRITE: `{nowPath}`
117
+ Content:
57
118
  ```markdown
58
119
  # NOW
59
120
 
60
121
  No current task. Use `/p:now` to set focus.
61
122
  ```
62
123
 
63
- ## Step 5: Log to Memory
124
+ ## Step 7: Log to Memory
64
125
 
65
126
  APPEND to: `{memoryPath}`
66
127
 
67
128
  Single line (JSONL format):
68
129
  ```json
69
- {"timestamp":"{GetTimestamp()}","action":"task_completed","task":"{task}","duration":"{duration}"}
130
+ {"timestamp":"{now}","action":"session_completed","sessionId":"{session.id}","task":"{session.task}","duration":{duration},"metrics":{"files":{filesChanged},"added":{linesAdded},"removed":{linesRemoved},"commits":{commits}}}
70
131
  ```
71
132
 
72
- ## Step 6: Update Metrics (Optional)
133
+ ## Step 8: Update Metrics Summary
134
+
135
+ READ: `{metricsPath}` (create if not exists)
73
136
 
74
- IF `{metricsPath}` exists:
75
- READ current content
76
- APPEND new entry with task and duration
137
+ ### Append Daily Entry
138
+ GET: {date} = YYYY-MM-DD from {now}
139
+
140
+ INSERT or UPDATE entry for {date}:
141
+ ```markdown
142
+ ### {date}
143
+ - **{session.task}** ({durationFormatted})
144
+ - Files: {filesChanged} | +{linesAdded}/-{linesRemoved} | Commits: {commits}
145
+ ```
77
146
 
78
147
  ## Output
79
148
 
80
149
  SUCCESS:
81
150
  ```
82
- ✅ {task} ({duration})
151
+ ✅ {session.task} ({durationFormatted})
152
+
153
+ Session: {session.id}
154
+ Files: {filesChanged} | +{linesAdded}/-{linesRemoved}
155
+ Commits: {commits}
83
156
 
84
157
  Next:
85
158
  • /p:now - Start next task
86
159
  • /p:ship - Ship completed work
87
- • /p:next - See priority queue
160
+ • /p:progress - View metrics
88
161
  ```
89
162
 
90
163
  ## Error Handling
91
164
 
92
- | Error | Response |
93
- |-------|----------|
94
- | Config not found | "No prjct project. Run /p:init first." |
95
- | Now.md empty | "⚠️ No active task. Use /p:now to start." |
96
- | Parse fails | Use task = "task", duration = "unknown", continue |
97
- | Write fails | Log warning, continue (non-critical) |
165
+ | Error | Response | Action |
166
+ |-------|----------|--------|
167
+ | Config not found | "No prjct project" | STOP |
168
+ | No session/task | "No active task" | STOP |
169
+ | Git fails | Use zeros for metrics | CONTINUE |
170
+ | Archive fails | Log warning | CONTINUE |
171
+ | Write fails | Log warning | CONTINUE |
98
172
 
99
173
  ## Examples
100
174
 
101
- ### Example 1: Success
102
- **now.md content:**
175
+ ### Example 1: Full Session Completion
176
+ **Session:**
177
+ ```json
178
+ {
179
+ "id": "sess_abc12345",
180
+ "task": "implement authentication",
181
+ "status": "active",
182
+ "startedAt": "2025-12-07T10:00:00.000Z",
183
+ "timeline": [
184
+ {"type": "start", "at": "2025-12-07T10:00:00.000Z"}
185
+ ]
186
+ }
103
187
  ```
104
- # NOW
105
188
 
106
- **implement authentication**
189
+ **Git activity:**
190
+ - 3 commits
191
+ - 5 files changed
192
+ - +120/-30 lines
107
193
 
108
- Started: 11/28/2025, 12:15:00 PM
194
+ **Output:**
109
195
  ```
196
+ ✅ implement authentication (2h 15m)
110
197
 
111
- **Current time:** 11/28/2025, 2:30:00 PM
112
- **Duration:** 2h 15m
113
- **Output:** `✅ implement authentication (2h 15m)`
198
+ Session: sess_abc12345
199
+ Files: 5 | +120/-30
200
+ Commits: 3
114
201
 
115
- ### Example 2: No Task
116
- **now.md content:**
202
+ Next:
203
+ • /p:now - Start next task
204
+ • /p:ship - Ship completed work
205
+ • /p:progress - View metrics
117
206
  ```
118
- # NOW
119
207
 
120
- No current task.
208
+ ### Example 2: Session with Pauses
209
+ **Session with multiple pause/resume:**
210
+ ```json
211
+ {
212
+ "id": "sess_xyz98765",
213
+ "task": "fix login bug",
214
+ "timeline": [
215
+ {"type": "start", "at": "2025-12-07T09:00:00.000Z"},
216
+ {"type": "pause", "at": "2025-12-07T10:00:00.000Z"},
217
+ {"type": "resume", "at": "2025-12-07T14:00:00.000Z"},
218
+ {"type": "pause", "at": "2025-12-07T15:30:00.000Z"},
219
+ {"type": "resume", "at": "2025-12-07T16:00:00.000Z"}
220
+ ]
221
+ }
222
+ ```
223
+
224
+ **Completion at 17:00:**
225
+ - Active time: 1h + 1.5h + 1h = 3.5h
226
+ - Duration: 3h 30m
227
+
228
+ **Output:**
121
229
  ```
230
+ ✅ fix login bug (3h 30m)
122
231
 
123
- **Output:** `⚠️ No active task to complete. Use /p:now to start one.`
232
+ Session: sess_xyz98765
233
+ Files: 2 | +45/-12
234
+ Commits: 1
124
235
 
125
- ### Example 3: Quick Task
236
+ Next:
237
+ • /p:now - Start next task
238
+ • /p:ship - Ship completed work
239
+ • /p:progress - View metrics
240
+ ```
241
+
242
+ ### Example 3: Legacy Fallback (No Session)
126
243
  **now.md content:**
127
244
  ```
128
245
  # NOW
129
246
 
130
- **fix typo in readme**
247
+ **quick fix**
131
248
 
132
- Started: 11/28/2025, 2:25:00 PM
249
+ Started: 2025-12-07T16:45:00.000Z
133
250
  ```
134
251
 
135
- **Current time:** 11/28/2025, 2:30:00 PM
136
- **Duration:** 5m
137
- **Output:** `✅ fix typo in readme (5m)`
252
+ **Output:**
253
+ ```
254
+ quick fix (15m)
255
+
256
+ Next:
257
+ • /p:now - Start next task
258
+ • /p:ship - Ship completed work
259
+ ```