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.
- package/CHANGELOG.md +122 -0
- package/bin/prjct +10 -13
- package/core/agentic/memory-system/semantic-memories.ts +2 -1
- package/core/agentic/plan-mode/plan-mode.ts +2 -1
- package/core/agentic/prompt-builder.ts +22 -43
- package/core/agentic/services.ts +5 -5
- package/core/agentic/smart-context.ts +7 -2
- package/core/command-registry/core-commands.ts +54 -29
- package/core/command-registry/optional-commands.ts +64 -0
- package/core/command-registry/setup-commands.ts +18 -3
- package/core/commands/analysis.ts +21 -68
- package/core/commands/analytics.ts +247 -213
- package/core/commands/base.ts +1 -1
- package/core/commands/index.ts +41 -36
- package/core/commands/maintenance.ts +300 -31
- package/core/commands/planning.ts +233 -22
- package/core/commands/setup.ts +3 -8
- package/core/commands/shipping.ts +14 -18
- package/core/commands/types.ts +8 -6
- package/core/commands/workflow.ts +105 -100
- package/core/context/generator.ts +317 -0
- package/core/context-sync.ts +7 -350
- package/core/data/index.ts +13 -32
- package/core/data/md-ideas-manager.ts +155 -0
- package/core/data/md-queue-manager.ts +4 -3
- package/core/data/md-shipped-manager.ts +90 -0
- package/core/data/md-state-manager.ts +11 -7
- package/core/domain/agent-generator.ts +23 -63
- package/core/events/index.ts +143 -0
- package/core/index.ts +17 -14
- package/core/infrastructure/capability-installer.ts +13 -149
- package/core/infrastructure/migrator/project-scanner.ts +2 -1
- package/core/infrastructure/path-manager.ts +4 -6
- package/core/infrastructure/setup.ts +3 -0
- package/core/infrastructure/uuid-migration.ts +750 -0
- package/core/outcomes/recorder.ts +2 -1
- package/core/plugin/loader.ts +4 -7
- package/core/plugin/registry.ts +3 -3
- package/core/schemas/index.ts +23 -25
- package/core/schemas/state.ts +1 -0
- package/core/serializers/ideas-serializer.ts +187 -0
- package/core/serializers/index.ts +16 -0
- package/core/serializers/shipped-serializer.ts +108 -0
- package/core/session/utils.ts +3 -9
- package/core/storage/ideas-storage.ts +273 -0
- package/core/storage/index.ts +204 -0
- package/core/storage/queue-storage.ts +297 -0
- package/core/storage/shipped-storage.ts +223 -0
- package/core/storage/state-storage.ts +235 -0
- package/core/storage/storage-manager.ts +175 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
- package/packages/web/app/api/sessions/current/route.ts +132 -0
- package/packages/web/app/api/sessions/history/route.ts +96 -14
- package/packages/web/app/globals.css +5 -0
- package/packages/web/app/layout.tsx +2 -0
- package/packages/web/app/project/[id]/code/layout.tsx +18 -0
- package/packages/web/app/project/[id]/code/page.tsx +408 -0
- package/packages/web/app/project/[id]/page.tsx +359 -389
- package/packages/web/app/project/[id]/reports/page.tsx +59 -0
- package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
- package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
- package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
- package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
- package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
- package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
- package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
- package/packages/web/components/CommandBar/index.ts +1 -0
- package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
- package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
- package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
- package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
- package/packages/web/components/EventRow/EventRow.tsx +4 -4
- package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
- package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
- package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
- package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
- package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
- package/packages/web/components/MasonryGrid/index.ts +1 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
- package/packages/web/components/MomentumWidget/index.ts +2 -0
- package/packages/web/components/NowCard/NowCard.tsx +81 -56
- package/packages/web/components/NowCard/NowCard.types.ts +1 -0
- package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
- package/packages/web/components/PageHeader/index.ts +1 -0
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
- package/packages/web/components/ProjectColorDot/index.ts +1 -0
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
- package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
- package/packages/web/components/Providers/Providers.tsx +4 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
- package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
- package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
- package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
- package/packages/web/components/RecoverCard/index.ts +2 -0
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
- package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
- package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
- package/packages/web/components/StatsMasonry/index.ts +1 -0
- package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
- package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
- package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
- package/packages/web/components/TerminalDock/index.ts +2 -0
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
- package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
- package/packages/web/components/WeeklyReports/index.ts +4 -0
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
- package/packages/web/components/charts/SessionsChart.tsx +6 -3
- package/packages/web/components/ui/dialog.tsx +143 -0
- package/packages/web/components/ui/drawer.tsx +135 -0
- package/packages/web/components/ui/select.tsx +187 -0
- package/packages/web/context/GlobalTerminalContext.tsx +538 -0
- package/packages/web/lib/commands.ts +81 -0
- package/packages/web/lib/generate-week-report.ts +285 -0
- package/packages/web/lib/parse-prjct-files.ts +56 -55
- package/packages/web/lib/project-colors.ts +58 -0
- package/packages/web/lib/projects.ts +58 -5
- package/packages/web/lib/services/projects.server.ts +11 -1
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +5 -1
- package/templates/commands/analyze.md +39 -3
- package/templates/commands/ask.md +58 -3
- package/templates/commands/bug.md +117 -26
- package/templates/commands/dash.md +95 -158
- package/templates/commands/done.md +130 -148
- package/templates/commands/feature.md +125 -103
- package/templates/commands/git.md +18 -3
- package/templates/commands/idea.md +121 -38
- package/templates/commands/init.md +124 -20
- package/templates/commands/migrate-all.md +63 -28
- package/templates/commands/migrate.md +140 -0
- package/templates/commands/next.md +115 -5
- package/templates/commands/now.md +146 -82
- package/templates/commands/pause.md +89 -74
- package/templates/commands/redo.md +6 -4
- package/templates/commands/resume.md +141 -59
- package/templates/commands/setup.md +18 -3
- package/templates/commands/ship.md +103 -231
- package/templates/commands/spec.md +98 -8
- package/templates/commands/suggest.md +22 -2
- package/templates/commands/sync.md +192 -203
- package/templates/commands/undo.md +6 -4
- package/templates/mcp-config.json +20 -1
- package/core/data/agents-manager.ts +0 -76
- package/core/data/analysis-manager.ts +0 -83
- package/core/data/base-manager.ts +0 -156
- package/core/data/ideas-manager.ts +0 -81
- package/core/data/outcomes-manager.ts +0 -96
- package/core/data/project-manager.ts +0 -75
- package/core/data/roadmap-manager.ts +0 -118
- package/core/data/shipped-manager.ts +0 -65
- package/core/data/state-manager.ts +0 -214
- package/core/state/index.ts +0 -25
- package/core/state/manager.ts +0 -376
- package/core/state/types.ts +0 -185
- package/core/utils/project-capabilities.ts +0 -156
- package/core/view-generator.ts +0 -536
- package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
- package/packages/web/app/project/[id]/stats/page.tsx +0 -253
- package/templates/agent-assignment.md +0 -72
- package/templates/analysis/project-analysis.md +0 -78
- package/templates/checklists/accessibility.md +0 -33
- package/templates/commands/build.md +0 -17
- package/templates/commands/decision.md +0 -226
- package/templates/commands/fix.md +0 -79
- package/templates/commands/help.md +0 -61
- package/templates/commands/progress.md +0 -14
- package/templates/commands/recap.md +0 -14
- package/templates/commands/roadmap.md +0 -52
- package/templates/commands/status.md +0 -17
- package/templates/commands/task.md +0 -63
- package/templates/commands/work.md +0 -44
- 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
|
@@ -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
|
+
}
|