prjct-cli 0.13.3 → 0.15.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.
Files changed (195) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/setup.md +18 -3
  160. package/templates/commands/ship.md +103 -231
  161. package/templates/commands/spec.md +98 -8
  162. package/templates/commands/suggest.md +22 -2
  163. package/templates/commands/sync.md +192 -203
  164. package/templates/commands/undo.md +6 -4
  165. package/templates/mcp-config.json +20 -1
  166. package/core/data/agents-manager.ts +0 -76
  167. package/core/data/analysis-manager.ts +0 -83
  168. package/core/data/base-manager.ts +0 -156
  169. package/core/data/ideas-manager.ts +0 -81
  170. package/core/data/outcomes-manager.ts +0 -96
  171. package/core/data/project-manager.ts +0 -75
  172. package/core/data/roadmap-manager.ts +0 -118
  173. package/core/data/shipped-manager.ts +0 -65
  174. package/core/data/state-manager.ts +0 -214
  175. package/core/state/index.ts +0 -25
  176. package/core/state/manager.ts +0 -376
  177. package/core/state/types.ts +0 -185
  178. package/core/utils/project-capabilities.ts +0 -156
  179. package/core/view-generator.ts +0 -536
  180. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  181. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  182. package/templates/agent-assignment.md +0 -72
  183. package/templates/analysis/project-analysis.md +0 -78
  184. package/templates/checklists/accessibility.md +0 -33
  185. package/templates/commands/build.md +0 -17
  186. package/templates/commands/decision.md +0 -226
  187. package/templates/commands/fix.md +0 -79
  188. package/templates/commands/help.md +0 -61
  189. package/templates/commands/progress.md +0 -14
  190. package/templates/commands/recap.md +0 -14
  191. package/templates/commands/roadmap.md +0 -52
  192. package/templates/commands/status.md +0 -17
  193. package/templates/commands/task.md +0 -63
  194. package/templates/commands/work.md +0 -44
  195. package/templates/commands/workflow.md +0 -12
@@ -0,0 +1,235 @@
1
+ /**
2
+ * State Storage
3
+ *
4
+ * Manages current task state via storage/state.json
5
+ * Generates context/now.md for Claude
6
+ */
7
+
8
+ import { StorageManager } from './storage-manager'
9
+ import { generateUUID } from '../schemas'
10
+ import type { StateJson, CurrentTask, PreviousTask } from '../schemas/state'
11
+
12
+ class StateStorage extends StorageManager<StateJson> {
13
+ constructor() {
14
+ super('state.json')
15
+ }
16
+
17
+ protected getDefault(): StateJson {
18
+ return {
19
+ currentTask: null,
20
+ previousTask: null,
21
+ lastUpdated: ''
22
+ }
23
+ }
24
+
25
+ protected getMdFilename(): string {
26
+ return 'now.md'
27
+ }
28
+
29
+ protected getEventType(action: 'update' | 'create' | 'delete'): string {
30
+ return `state.${action}d`
31
+ }
32
+
33
+ protected toMarkdown(data: StateJson): string {
34
+ const lines = ['# NOW', '']
35
+
36
+ // Show current task if exists
37
+ if (data.currentTask) {
38
+ const task = data.currentTask
39
+ lines.push(`**${task.description}**`)
40
+ lines.push('')
41
+ lines.push(`Started: ${task.startedAt}`)
42
+ lines.push(`Session: ${task.sessionId}`)
43
+ if (task.featureId) {
44
+ lines.push(`Feature: ${task.featureId}`)
45
+ }
46
+ } else {
47
+ lines.push('*No active task. Use /p:work to start.*')
48
+ }
49
+
50
+ // Show paused task section if exists
51
+ if (data.previousTask) {
52
+ lines.push('')
53
+ lines.push('---')
54
+ lines.push('')
55
+ lines.push('## Paused')
56
+ lines.push('')
57
+ lines.push(`**${data.previousTask.description}**`)
58
+ lines.push(`Paused: ${data.previousTask.pausedAt}`)
59
+ if (data.previousTask.pauseReason) {
60
+ lines.push(`Reason: ${data.previousTask.pauseReason}`)
61
+ }
62
+ lines.push('')
63
+ lines.push('*Use /p:resume to continue*')
64
+ }
65
+
66
+ lines.push('')
67
+ return lines.join('\n')
68
+ }
69
+
70
+ // =========== Domain Methods ===========
71
+
72
+ /**
73
+ * Get current active task
74
+ */
75
+ async getCurrentTask(projectId: string): Promise<CurrentTask | null> {
76
+ const state = await this.read(projectId)
77
+ return state.currentTask
78
+ }
79
+
80
+ /**
81
+ * Start a new task
82
+ */
83
+ async startTask(
84
+ projectId: string,
85
+ task: Omit<CurrentTask, 'startedAt'>
86
+ ): Promise<CurrentTask> {
87
+ const currentTask: CurrentTask = {
88
+ ...task,
89
+ startedAt: new Date().toISOString()
90
+ }
91
+
92
+ await this.update(projectId, (state) => ({
93
+ ...state,
94
+ currentTask,
95
+ lastUpdated: new Date().toISOString()
96
+ }))
97
+
98
+ // Publish incremental event
99
+ await this.publishEvent(projectId, 'task.started', {
100
+ taskId: currentTask.id,
101
+ description: currentTask.description,
102
+ startedAt: currentTask.startedAt,
103
+ sessionId: currentTask.sessionId
104
+ })
105
+
106
+ return currentTask
107
+ }
108
+
109
+ /**
110
+ * Complete current task
111
+ */
112
+ async completeTask(projectId: string): Promise<CurrentTask | null> {
113
+ const state = await this.read(projectId)
114
+ const completedTask = state.currentTask
115
+
116
+ if (!completedTask) {
117
+ return null
118
+ }
119
+
120
+ await this.update(projectId, () => ({
121
+ currentTask: null,
122
+ previousTask: null,
123
+ lastUpdated: new Date().toISOString()
124
+ }))
125
+
126
+ // Publish incremental event
127
+ await this.publishEvent(projectId, 'task.completed', {
128
+ taskId: completedTask.id,
129
+ description: completedTask.description,
130
+ startedAt: completedTask.startedAt,
131
+ completedAt: new Date().toISOString()
132
+ })
133
+
134
+ return completedTask
135
+ }
136
+
137
+ /**
138
+ * Pause current task
139
+ */
140
+ async pauseTask(projectId: string, reason?: string): Promise<PreviousTask | null> {
141
+ const state = await this.read(projectId)
142
+
143
+ if (!state.currentTask) {
144
+ return null
145
+ }
146
+
147
+ const previousTask: PreviousTask = {
148
+ id: state.currentTask.id,
149
+ description: state.currentTask.description,
150
+ status: 'paused',
151
+ startedAt: state.currentTask.startedAt,
152
+ pausedAt: new Date().toISOString(),
153
+ pauseReason: reason
154
+ }
155
+
156
+ await this.update(projectId, () => ({
157
+ currentTask: null,
158
+ previousTask,
159
+ lastUpdated: new Date().toISOString()
160
+ }))
161
+
162
+ // Publish incremental event
163
+ await this.publishEvent(projectId, 'task.paused', {
164
+ taskId: previousTask.id,
165
+ description: previousTask.description,
166
+ pausedAt: previousTask.pausedAt,
167
+ reason
168
+ })
169
+
170
+ return previousTask
171
+ }
172
+
173
+ /**
174
+ * Resume paused task
175
+ */
176
+ async resumeTask(projectId: string): Promise<CurrentTask | null> {
177
+ const state = await this.read(projectId)
178
+
179
+ if (!state.previousTask) {
180
+ return null
181
+ }
182
+
183
+ const currentTask: CurrentTask = {
184
+ id: state.previousTask.id,
185
+ description: state.previousTask.description,
186
+ startedAt: new Date().toISOString(),
187
+ sessionId: generateUUID()
188
+ }
189
+
190
+ await this.update(projectId, () => ({
191
+ currentTask,
192
+ previousTask: null,
193
+ lastUpdated: new Date().toISOString()
194
+ }))
195
+
196
+ // Publish incremental event
197
+ await this.publishEvent(projectId, 'task.resumed', {
198
+ taskId: currentTask.id,
199
+ description: currentTask.description,
200
+ resumedAt: currentTask.startedAt
201
+ })
202
+
203
+ return currentTask
204
+ }
205
+
206
+ /**
207
+ * Clear all task state
208
+ */
209
+ async clearTask(projectId: string): Promise<void> {
210
+ await this.update(projectId, () => ({
211
+ currentTask: null,
212
+ previousTask: null,
213
+ lastUpdated: new Date().toISOString()
214
+ }))
215
+ }
216
+
217
+ /**
218
+ * Check if there's an active or paused task
219
+ */
220
+ async hasTask(projectId: string): Promise<boolean> {
221
+ const state = await this.read(projectId)
222
+ return state.currentTask !== null || state.previousTask !== null
223
+ }
224
+
225
+ /**
226
+ * Get paused task
227
+ */
228
+ async getPausedTask(projectId: string): Promise<PreviousTask | null> {
229
+ const state = await this.read(projectId)
230
+ return state.previousTask || null
231
+ }
232
+ }
233
+
234
+ export const stateStorage = new StateStorage()
235
+ export default stateStorage
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Storage Manager Base Class
3
+ *
4
+ * Write-through pattern:
5
+ * 1. Write JSON to storage/
6
+ * 2. Regenerate MD in context/
7
+ * 3. Publish event for backend sync
8
+ *
9
+ * Subclasses implement specific data types (state, queue, ideas, shipped).
10
+ */
11
+
12
+ import fs from 'fs/promises'
13
+ import path from 'path'
14
+ import os from 'os'
15
+ import { eventBus, type SyncEvent } from '../events'
16
+
17
+ export abstract class StorageManager<T> {
18
+ protected filename: string
19
+ protected cache: Map<string, T> = new Map()
20
+ protected cacheTimeout = 5000 // 5 seconds
21
+
22
+ constructor(filename: string) {
23
+ this.filename = filename
24
+ }
25
+
26
+ /**
27
+ * Get file path for storage JSON
28
+ */
29
+ protected getStoragePath(projectId: string): string {
30
+ return path.join(
31
+ os.homedir(),
32
+ '.prjct-cli/projects',
33
+ projectId,
34
+ 'storage',
35
+ this.filename
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Get file path for context MD
41
+ */
42
+ protected getContextPath(projectId: string, mdFilename: string): string {
43
+ return path.join(
44
+ os.homedir(),
45
+ '.prjct-cli/projects',
46
+ projectId,
47
+ 'context',
48
+ mdFilename
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Get default data structure
54
+ */
55
+ protected abstract getDefault(): T
56
+
57
+ /**
58
+ * Convert data to markdown for Claude
59
+ */
60
+ protected abstract toMarkdown(data: T): string
61
+
62
+ /**
63
+ * Get MD filename for context generation
64
+ */
65
+ protected abstract getMdFilename(): string
66
+
67
+ /**
68
+ * Get event type for sync
69
+ */
70
+ protected abstract getEventType(action: 'update' | 'create' | 'delete'): string
71
+
72
+ /**
73
+ * Read data from storage
74
+ */
75
+ async read(projectId: string): Promise<T> {
76
+ // Check cache first
77
+ const cached = this.cache.get(projectId)
78
+ if (cached) {
79
+ return cached
80
+ }
81
+
82
+ const filePath = this.getStoragePath(projectId)
83
+
84
+ try {
85
+ const content = await fs.readFile(filePath, 'utf-8')
86
+ const data = JSON.parse(content) as T
87
+ this.cache.set(projectId, data)
88
+ return data
89
+ } catch {
90
+ // Return default if file doesn't exist
91
+ return this.getDefault()
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Write data to storage + regenerate context + publish event
97
+ */
98
+ async write(projectId: string, data: T): Promise<void> {
99
+ const storagePath = this.getStoragePath(projectId)
100
+ const contextPath = this.getContextPath(projectId, this.getMdFilename())
101
+
102
+ // Ensure directories exist
103
+ await fs.mkdir(path.dirname(storagePath), { recursive: true })
104
+ await fs.mkdir(path.dirname(contextPath), { recursive: true })
105
+
106
+ // 1. Write JSON (atomic via temp file)
107
+ const tempPath = `${storagePath}.${Date.now()}.tmp`
108
+ await fs.writeFile(tempPath, JSON.stringify(data, null, 2), 'utf-8')
109
+ await fs.rename(tempPath, storagePath)
110
+
111
+ // 2. Regenerate MD for Claude
112
+ const md = this.toMarkdown(data)
113
+ await fs.writeFile(contextPath, md, 'utf-8')
114
+
115
+ // 3. Update cache
116
+ this.cache.set(projectId, data)
117
+
118
+ // 4. Publish event for backend sync (NOT included in this call - subclass handles)
119
+ }
120
+
121
+ /**
122
+ * Update data with a transform function
123
+ */
124
+ async update(projectId: string, updater: (current: T) => T): Promise<T> {
125
+ const current = await this.read(projectId)
126
+ const updated = updater(current)
127
+ await this.write(projectId, updated)
128
+ return updated
129
+ }
130
+
131
+ /**
132
+ * Publish sync event to eventBus
133
+ */
134
+ protected async publishEvent(
135
+ projectId: string,
136
+ eventType: string,
137
+ eventData: unknown
138
+ ): Promise<void> {
139
+ const event: SyncEvent = {
140
+ type: eventType,
141
+ path: [this.filename.replace('.json', '')],
142
+ data: eventData,
143
+ timestamp: new Date().toISOString(),
144
+ projectId
145
+ }
146
+
147
+ await eventBus.publish(event)
148
+ }
149
+
150
+ /**
151
+ * Check if storage file exists
152
+ */
153
+ async exists(projectId: string): Promise<boolean> {
154
+ const filePath = this.getStoragePath(projectId)
155
+ try {
156
+ await fs.access(filePath)
157
+ return true
158
+ } catch {
159
+ return false
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Clear cache for a project
165
+ */
166
+ clearCache(projectId?: string): void {
167
+ if (projectId) {
168
+ this.cache.delete(projectId)
169
+ } else {
170
+ this.cache.clear()
171
+ }
172
+ }
173
+ }
174
+
175
+ export default StorageManager
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.13.3",
3
+ "version": "0.15.1",
4
4
  "description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {
@@ -0,0 +1,257 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { promises as fs } from 'fs'
3
+ import { join } from 'path'
4
+ import { homedir } from 'os'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ interface SessionEvent {
9
+ ts?: string
10
+ timestamp?: string
11
+ type?: string
12
+ action?: string
13
+ }
14
+
15
+ type MomentumStatus = 'hot' | 'active' | 'cooling' | 'cold'
16
+
17
+ interface MomentumData {
18
+ dailyTasks: number[]
19
+ totalTasks: number
20
+ totalShips: number
21
+ lastActivityDate: string | null
22
+ daysSinceActivity: number
23
+ streak: number
24
+ status: MomentumStatus
25
+ message: string
26
+ }
27
+
28
+ function getStatus(
29
+ daysSinceActivity: number,
30
+ totalTasks: number,
31
+ dailyTasks: number[],
32
+ streak: number
33
+ ): { status: MomentumStatus; message: string } {
34
+ // Abandoned - 7+ days without activity
35
+ if (daysSinceActivity >= 7) {
36
+ return { status: 'cold', message: 'Miss you!' }
37
+ }
38
+
39
+ // No activity ever
40
+ if (totalTasks === 0) {
41
+ return { status: 'active', message: 'Start building!' }
42
+ }
43
+
44
+ // Check if trending up (recent days > earlier days)
45
+ const recentDays = dailyTasks.slice(-3).reduce((a, b) => a + b, 0)
46
+ const earlierDays = dailyTasks.slice(0, 4).reduce((a, b) => a + b, 0)
47
+ const isTrendingUp = recentDays > earlierDays || streak >= 2
48
+
49
+ // Hot - trending up or on a streak
50
+ if (isTrendingUp && daysSinceActivity <= 1) {
51
+ return { status: 'hot', message: streak >= 2 ? `${streak} day streak!` : 'On fire!' }
52
+ }
53
+
54
+ // Normal activity - neutral
55
+ if (daysSinceActivity <= 3) {
56
+ return { status: 'active', message: `${totalTasks} this week` }
57
+ }
58
+
59
+ // Cooling down but not abandoned
60
+ return { status: 'cooling', message: `${daysSinceActivity}d ago` }
61
+ }
62
+
63
+ export async function GET(
64
+ request: Request,
65
+ { params }: { params: Promise<{ id: string }> }
66
+ ) {
67
+ try {
68
+ const { id: projectId } = await params
69
+ const globalStorage = join(homedir(), '.prjct-cli', 'projects')
70
+ const projectPath = join(globalStorage, projectId)
71
+
72
+ // Check if project exists
73
+ try {
74
+ await fs.access(projectPath)
75
+ } catch {
76
+ return NextResponse.json(
77
+ { success: false, error: 'Project not found' },
78
+ { status: 404 }
79
+ )
80
+ }
81
+
82
+ // Calculate date range (last 7 days)
83
+ const endDate = new Date()
84
+ const startDate = new Date()
85
+ startDate.setDate(startDate.getDate() - 6) // 7 days including today
86
+ startDate.setHours(0, 0, 0, 0)
87
+
88
+ const dailyMap = new Map<string, { tasks: number; ships: number }>()
89
+ let lastActivityDate: Date | null = null
90
+
91
+ // Read from memory/context.jsonl (legacy)
92
+ const contextPath = join(projectPath, 'memory', 'context.jsonl')
93
+ try {
94
+ const content = await fs.readFile(contextPath, 'utf-8')
95
+ const lines = content.trim().split('\n').filter(Boolean)
96
+
97
+ for (const line of lines) {
98
+ try {
99
+ const event: SessionEvent = JSON.parse(line)
100
+ const timestamp = event.ts || event.timestamp
101
+ if (!timestamp) continue
102
+
103
+ const eventDate = new Date(timestamp)
104
+ if (isNaN(eventDate.getTime())) continue
105
+
106
+ const eventType = event.type || event.action
107
+
108
+ // Track last activity for any relevant event
109
+ if (eventType === 'task_complete' || eventType === 'task_completed' ||
110
+ eventType === 'feature_ship' || eventType === 'feature_shipped') {
111
+ if (!lastActivityDate || eventDate > lastActivityDate) {
112
+ lastActivityDate = eventDate
113
+ }
114
+ }
115
+
116
+ // Only count last 7 days for sparkline
117
+ if (eventDate < startDate || eventDate > endDate) continue
118
+
119
+ const dateKey = eventDate.toISOString().split('T')[0]
120
+ const current = dailyMap.get(dateKey) || { tasks: 0, ships: 0 }
121
+
122
+ if (eventType === 'task_complete' || eventType === 'task_completed') {
123
+ current.tasks++
124
+ }
125
+ if (eventType === 'feature_ship' || eventType === 'feature_shipped') {
126
+ current.ships++
127
+ }
128
+
129
+ dailyMap.set(dateKey, current)
130
+ } catch {
131
+ // Skip malformed lines
132
+ }
133
+ }
134
+ } catch {
135
+ // No context.jsonl
136
+ }
137
+
138
+ // Read from progress/sessions/{YYYY-MM}/{date}.jsonl (new format)
139
+ const sessionsDir = join(projectPath, 'progress', 'sessions')
140
+ try {
141
+ const monthDirs = await fs.readdir(sessionsDir)
142
+ for (const monthDir of monthDirs) {
143
+ if (!monthDir.match(/^\d{4}-\d{2}$/)) continue
144
+
145
+ const monthPath = join(sessionsDir, monthDir)
146
+ try {
147
+ const dayFiles = await fs.readdir(monthPath)
148
+ for (const dayFile of dayFiles) {
149
+ if (!dayFile.endsWith('.jsonl')) continue
150
+
151
+ const dayPath = join(monthPath, dayFile)
152
+ try {
153
+ const content = await fs.readFile(dayPath, 'utf-8')
154
+ const lines = content.trim().split('\n').filter(Boolean)
155
+
156
+ for (const line of lines) {
157
+ try {
158
+ const event: SessionEvent = JSON.parse(line)
159
+ const timestamp = event.ts || event.timestamp
160
+ if (!timestamp) continue
161
+
162
+ const eventDate = new Date(timestamp)
163
+ if (isNaN(eventDate.getTime())) continue
164
+
165
+ const eventType = event.type || event.action
166
+
167
+ // Track last activity
168
+ if (eventType === 'task_complete' || eventType === 'task_completed' ||
169
+ eventType === 'feature_ship' || eventType === 'feature_shipped') {
170
+ if (!lastActivityDate || eventDate > lastActivityDate) {
171
+ lastActivityDate = eventDate
172
+ }
173
+ }
174
+
175
+ // Only count last 7 days for sparkline
176
+ if (eventDate < startDate || eventDate > endDate) continue
177
+
178
+ const dateKey = eventDate.toISOString().split('T')[0]
179
+ const current = dailyMap.get(dateKey) || { tasks: 0, ships: 0 }
180
+
181
+ if (eventType === 'task_complete' || eventType === 'task_completed') {
182
+ current.tasks++
183
+ }
184
+ if (eventType === 'feature_ship' || eventType === 'feature_shipped') {
185
+ current.ships++
186
+ }
187
+
188
+ dailyMap.set(dateKey, current)
189
+ } catch {
190
+ // Skip malformed lines
191
+ }
192
+ }
193
+ } catch {
194
+ // Skip unreadable files
195
+ }
196
+ }
197
+ } catch {
198
+ // Skip unreadable month directories
199
+ }
200
+ }
201
+ } catch {
202
+ // No sessions directory
203
+ }
204
+
205
+ // Generate daily tasks array for sparkline (7 days)
206
+ const dailyTasks: number[] = []
207
+ let totalTasks = 0
208
+ let totalShips = 0
209
+ let streak = 0
210
+ let streakBroken = false
211
+
212
+ const currentDate = new Date(startDate)
213
+ while (currentDate <= endDate) {
214
+ const dateKey = currentDate.toISOString().split('T')[0]
215
+ const stats = dailyMap.get(dateKey) || { tasks: 0, ships: 0 }
216
+ dailyTasks.push(stats.tasks + stats.ships)
217
+ totalTasks += stats.tasks
218
+ totalShips += stats.ships
219
+ currentDate.setDate(currentDate.getDate() + 1)
220
+ }
221
+
222
+ // Calculate streak (consecutive days with activity from today backwards)
223
+ for (let i = dailyTasks.length - 1; i >= 0; i--) {
224
+ if (dailyTasks[i] > 0 && !streakBroken) {
225
+ streak++
226
+ } else if (dailyTasks[i] === 0 && i < dailyTasks.length - 1) {
227
+ streakBroken = true
228
+ }
229
+ }
230
+
231
+ // Calculate days since last activity
232
+ const daysSinceActivity = lastActivityDate
233
+ ? Math.floor((endDate.getTime() - lastActivityDate.getTime()) / (1000 * 60 * 60 * 24))
234
+ : 999
235
+
236
+ const { status, message } = getStatus(daysSinceActivity, totalTasks, dailyTasks, streak)
237
+
238
+ const data: MomentumData = {
239
+ dailyTasks,
240
+ totalTasks,
241
+ totalShips,
242
+ lastActivityDate: lastActivityDate?.toISOString() || null,
243
+ daysSinceActivity,
244
+ streak,
245
+ status,
246
+ message
247
+ }
248
+
249
+ return NextResponse.json({ success: true, data })
250
+ } catch (error) {
251
+ console.error('Momentum API error:', error)
252
+ return NextResponse.json(
253
+ { success: false, error: 'Failed to fetch momentum data' },
254
+ { status: 500 }
255
+ )
256
+ }
257
+ }