prjct-cli 0.10.14 → 0.11.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 (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/dev.js +217 -0
  3. package/bin/prjct +10 -0
  4. package/bin/serve.js +78 -0
  5. package/core/bus/index.js +322 -0
  6. package/core/command-registry.js +65 -0
  7. package/core/domain/snapshot-manager.js +375 -0
  8. package/core/plugin/hooks.js +313 -0
  9. package/core/plugin/index.js +52 -0
  10. package/core/plugin/loader.js +331 -0
  11. package/core/plugin/registry.js +325 -0
  12. package/core/plugins/webhook.js +143 -0
  13. package/core/session/index.js +449 -0
  14. package/core/session/metrics.js +293 -0
  15. package/package.json +28 -4
  16. package/packages/shared/dist/index.d.ts +615 -0
  17. package/packages/shared/dist/index.js +204 -0
  18. package/packages/shared/package.json +29 -0
  19. package/packages/shared/src/index.ts +9 -0
  20. package/packages/shared/src/schemas.ts +124 -0
  21. package/packages/shared/src/types.ts +187 -0
  22. package/packages/shared/src/utils.ts +148 -0
  23. package/packages/shared/tsconfig.json +18 -0
  24. package/packages/web/README.md +36 -0
  25. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  26. package/packages/web/app/api/claude/status/route.ts +34 -0
  27. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  29. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  31. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  32. package/packages/web/app/api/projects/route.ts +16 -0
  33. package/packages/web/app/api/sessions/history/route.ts +122 -0
  34. package/packages/web/app/api/stats/route.ts +38 -0
  35. package/packages/web/app/error.tsx +34 -0
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +155 -0
  38. package/packages/web/app/layout.tsx +43 -0
  39. package/packages/web/app/loading.tsx +7 -0
  40. package/packages/web/app/not-found.tsx +25 -0
  41. package/packages/web/app/page.tsx +227 -0
  42. package/packages/web/app/project/[id]/error.tsx +41 -0
  43. package/packages/web/app/project/[id]/loading.tsx +9 -0
  44. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  45. package/packages/web/app/project/[id]/page.tsx +253 -0
  46. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  47. package/packages/web/app/sessions/page.tsx +165 -0
  48. package/packages/web/app/settings/page.tsx +150 -0
  49. package/packages/web/components/AppSidebar.tsx +113 -0
  50. package/packages/web/components/CommandButton.tsx +39 -0
  51. package/packages/web/components/ConnectionStatus.tsx +29 -0
  52. package/packages/web/components/Logo.tsx +65 -0
  53. package/packages/web/components/MarkdownContent.tsx +123 -0
  54. package/packages/web/components/ProjectAvatar.tsx +54 -0
  55. package/packages/web/components/TechStackBadges.tsx +20 -0
  56. package/packages/web/components/TerminalTab.tsx +84 -0
  57. package/packages/web/components/TerminalTabs.tsx +210 -0
  58. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  59. package/packages/web/components/providers.tsx +45 -0
  60. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  61. package/packages/web/components/ui/badge.tsx +46 -0
  62. package/packages/web/components/ui/button.tsx +60 -0
  63. package/packages/web/components/ui/card.tsx +92 -0
  64. package/packages/web/components/ui/chart.tsx +385 -0
  65. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  66. package/packages/web/components/ui/scroll-area.tsx +58 -0
  67. package/packages/web/components/ui/sheet.tsx +139 -0
  68. package/packages/web/components/ui/tabs.tsx +66 -0
  69. package/packages/web/components/ui/tooltip.tsx +61 -0
  70. package/packages/web/components.json +22 -0
  71. package/packages/web/context/TerminalContext.tsx +45 -0
  72. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  73. package/packages/web/eslint.config.mjs +18 -0
  74. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  75. package/packages/web/hooks/useProjectStats.ts +38 -0
  76. package/packages/web/hooks/useProjects.ts +73 -0
  77. package/packages/web/hooks/useStats.ts +28 -0
  78. package/packages/web/lib/format.ts +23 -0
  79. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  80. package/packages/web/lib/projects.ts +452 -0
  81. package/packages/web/lib/pty.ts +101 -0
  82. package/packages/web/lib/query-config.ts +44 -0
  83. package/packages/web/lib/utils.ts +6 -0
  84. package/packages/web/next-env.d.ts +6 -0
  85. package/packages/web/next.config.ts +7 -0
  86. package/packages/web/package.json +53 -0
  87. package/packages/web/postcss.config.mjs +7 -0
  88. package/packages/web/public/file.svg +1 -0
  89. package/packages/web/public/globe.svg +1 -0
  90. package/packages/web/public/next.svg +1 -0
  91. package/packages/web/public/vercel.svg +1 -0
  92. package/packages/web/public/window.svg +1 -0
  93. package/packages/web/server.ts +262 -0
  94. package/packages/web/tsconfig.json +34 -0
  95. package/templates/commands/done.md +176 -54
  96. package/templates/commands/history.md +176 -0
  97. package/templates/commands/init.md +28 -1
  98. package/templates/commands/now.md +191 -9
  99. package/templates/commands/pause.md +176 -12
  100. package/templates/commands/redo.md +142 -0
  101. package/templates/commands/resume.md +166 -62
  102. package/templates/commands/serve.md +121 -0
  103. package/templates/commands/ship.md +45 -1
  104. package/templates/commands/sync.md +34 -1
  105. package/templates/commands/undo.md +152 -0
@@ -0,0 +1,1122 @@
1
+ /**
2
+ * Comprehensive parsers for prjct-cli data files
3
+ * Extracts ALL rich data from the prjct storage system
4
+ */
5
+
6
+ import { readFile, readdir, stat } from 'fs/promises'
7
+ import { join } from 'path'
8
+ import { homedir } from 'os'
9
+
10
+ // ============================================
11
+ // TYPES - Full data model from prjct
12
+ // ============================================
13
+
14
+ export interface Author {
15
+ name: string
16
+ email: string
17
+ username?: string
18
+ }
19
+
20
+ export interface CurrentTask {
21
+ task: string
22
+ started?: string
23
+ duration?: string
24
+ feature?: string
25
+ phase?: string
26
+ agent?: string
27
+ estimate?: string
28
+ priority?: string
29
+ }
30
+
31
+ export interface TaskEvent {
32
+ ts: string
33
+ type: 'task_start' | 'task_complete' | 'task_shipped'
34
+ task: string
35
+ feature?: string
36
+ phase?: string
37
+ agent?: string
38
+ estimate?: string
39
+ duration?: string
40
+ started?: string
41
+ completed?: string
42
+ deliverables?: string[]
43
+ files_modified?: number
44
+ impact?: string
45
+ progress?: string
46
+ author?: Author
47
+ }
48
+
49
+ export interface ShipEvent {
50
+ ts: string
51
+ type: 'ship' | 'feature_ship' | 'feature_shipped'
52
+ name: string
53
+ feature?: string
54
+ version?: string
55
+ commit?: string
56
+ tasks_done?: number
57
+ duration?: string
58
+ agent?: string
59
+ impact?: string
60
+ complexity?: string
61
+ decision?: string
62
+ details?: string
63
+ author?: Author
64
+ }
65
+
66
+ export interface BugEvent {
67
+ ts: string
68
+ type: 'bug_report' | 'bug_fix'
69
+ name?: string
70
+ description: string
71
+ severity: string
72
+ files_modified?: number
73
+ author?: Author
74
+ }
75
+
76
+ export interface SyncEvent {
77
+ ts: string
78
+ type: 'sync' | 'repository_analyzed' | 'agents_generated' | 'agents_removed'
79
+ agents?: string[]
80
+ stack?: string
81
+ patterns?: number
82
+ fileCount?: number
83
+ gitCommits?: number
84
+ reason?: string
85
+ author?: Author
86
+ }
87
+
88
+ export interface CleanupEvent {
89
+ ts: string
90
+ type: 'cleanup'
91
+ action?: string
92
+ files_changed?: number
93
+ lines_removed?: number
94
+ lines_added?: number
95
+ net_change?: number
96
+ fixes?: Record<string, number>
97
+ details?: string
98
+ author?: Author
99
+ }
100
+
101
+ export interface RoadmapEvent {
102
+ ts: string
103
+ type: 'roadmap'
104
+ action: string
105
+ phases?: number
106
+ features?: number
107
+ estimated_weeks?: string
108
+ author?: Author
109
+ }
110
+
111
+ export interface IdeaEvent {
112
+ ts: string
113
+ type: 'idea_captured' | 'feature_add'
114
+ idea?: string
115
+ name?: string
116
+ priority?: string
117
+ impact?: string
118
+ effort?: string
119
+ actionable?: boolean
120
+ tasks_created?: number
121
+ author?: Author
122
+ }
123
+
124
+ export interface StuckEvent {
125
+ ts: string
126
+ type: 'stuck'
127
+ issue: string
128
+ context?: string
129
+ author?: Author
130
+ }
131
+
132
+ export type TimelineEvent =
133
+ | TaskEvent
134
+ | ShipEvent
135
+ | BugEvent
136
+ | SyncEvent
137
+ | CleanupEvent
138
+ | RoadmapEvent
139
+ | IdeaEvent
140
+ | StuckEvent
141
+ | { ts: string; type: string; [key: string]: unknown }
142
+
143
+ export interface Metrics {
144
+ tasksStarted: number
145
+ tasksCompleted: number
146
+ inProgress: number
147
+ totalTime: string
148
+ daysActive: number
149
+ velocity: {
150
+ tasksPerDay: number
151
+ featuresPerWeek?: number
152
+ }
153
+ testCoverage?: {
154
+ total: number
155
+ passing: number
156
+ categories?: Record<string, { count: number; passing: number }>
157
+ }
158
+ codeQuality?: {
159
+ linesAdded: number
160
+ linesRemoved: number
161
+ filesChanged: number
162
+ }
163
+ }
164
+
165
+ export interface ShippedFeature {
166
+ date: string
167
+ name: string
168
+ version?: string
169
+ commit?: string
170
+ type?: string
171
+ agent?: string
172
+ time?: string
173
+ impact?: string
174
+ filesChanged?: number
175
+ linesAdded?: number
176
+ linesRemoved?: number
177
+ rootCause?: string
178
+ solution?: string
179
+ details?: string[]
180
+ }
181
+
182
+ export interface QueueItem {
183
+ task: string
184
+ priority: number
185
+ feature?: string
186
+ agent?: string
187
+ estimate?: string
188
+ status: 'pending' | 'in_progress'
189
+ }
190
+
191
+ export interface Idea {
192
+ title: string
193
+ status: string
194
+ date?: string
195
+ description?: string
196
+ painPoints?: string[]
197
+ solutions?: string[]
198
+ impact?: string
199
+ effort?: string
200
+ }
201
+
202
+ export interface RoadmapPhase {
203
+ name: string
204
+ status: 'completed' | 'in_progress' | 'queued'
205
+ progress: number
206
+ features?: RoadmapFeature[]
207
+ }
208
+
209
+ export interface RoadmapFeature {
210
+ name: string
211
+ status: 'completed' | 'in_progress' | 'queued'
212
+ tasks: number
213
+ tasksCompleted: number
214
+ time?: string
215
+ shippedDate?: string
216
+ version?: string
217
+ }
218
+
219
+ export interface Agent {
220
+ name: string
221
+ role?: string
222
+ responsibilities?: string[]
223
+ whenToUse?: string[]
224
+ }
225
+
226
+ export interface SessionDay {
227
+ date: string
228
+ events: TimelineEvent[]
229
+ tasksStarted: number
230
+ tasksCompleted: number
231
+ featuresShipped: number
232
+ timeTracked?: string
233
+ }
234
+
235
+ export interface AnalysisData {
236
+ fileCount: number
237
+ commitCount: number
238
+ stack: string
239
+ techStack?: string[]
240
+ structure?: string
241
+ architecture?: string
242
+ }
243
+
244
+ export interface CodePatterns {
245
+ moduleSystem?: Record<string, string>
246
+ namingConventions?: Record<string, string>
247
+ asyncPatterns?: Record<string, string>
248
+ classStructure?: string
249
+ }
250
+
251
+ // Full project stats interface
252
+ export interface ProjectStats {
253
+ currentTask: CurrentTask | null
254
+ metrics: Metrics
255
+ shipped: ShippedFeature[]
256
+ queue: QueueItem[]
257
+ ideas: {
258
+ pending: Idea[]
259
+ archived: number
260
+ implemented: number
261
+ }
262
+ roadmap: {
263
+ phases: RoadmapPhase[]
264
+ completedFeatures: number
265
+ totalFeatures: number
266
+ progress: number
267
+ }
268
+ agents: Agent[]
269
+ timeline: TimelineEvent[]
270
+ sessions: SessionDay[]
271
+ analysis: AnalysisData
272
+ patterns?: CodePatterns
273
+ summary: {
274
+ totalEvents: number
275
+ firstActivity?: string
276
+ lastActivity?: string
277
+ activeDays: number
278
+ totalTasksEver: number
279
+ totalShipsEver: number
280
+ totalBugsFixed: number
281
+ totalCleanups: number
282
+ }
283
+ }
284
+
285
+ // ============================================
286
+ // HELPERS
287
+ // ============================================
288
+
289
+ async function safeReadFile(path: string): Promise<string> {
290
+ try {
291
+ return await readFile(path, 'utf-8')
292
+ } catch {
293
+ return ''
294
+ }
295
+ }
296
+
297
+ async function safeReadDir(path: string): Promise<string[]> {
298
+ try {
299
+ return await readdir(path)
300
+ } catch {
301
+ return []
302
+ }
303
+ }
304
+
305
+ async function fileExists(path: string): Promise<boolean> {
306
+ try {
307
+ await stat(path)
308
+ return true
309
+ } catch {
310
+ return false
311
+ }
312
+ }
313
+
314
+ export function getStoragePath(projectId: string): string {
315
+ return join(homedir(), '.prjct-cli', 'projects', projectId)
316
+ }
317
+
318
+ // ============================================
319
+ // PARSERS
320
+ // ============================================
321
+
322
+ // Parse now.md - Current task with full context
323
+ export function parseNow(content: string): CurrentTask | null {
324
+ if (!content || content.includes('No active task')) {
325
+ return null
326
+ }
327
+
328
+ const lines = content.split('\n').filter(l => l.trim())
329
+
330
+ // Get task line (first non-header, non-empty line)
331
+ const taskLine = lines.find(l => !l.startsWith('#') && !l.startsWith('_') && l.trim())
332
+ if (!taskLine) return null
333
+
334
+ const task: CurrentTask = {
335
+ task: taskLine.replace(/^[-*]\s*/, '').replace(/\*\*/g, '').trim()
336
+ }
337
+
338
+ // Extract metadata if present
339
+ const startedMatch = content.match(/Started:\s*(.+)/i)
340
+ const featureMatch = content.match(/Feature:\s*(.+)/i)
341
+ const phaseMatch = content.match(/Phase:\s*(.+)/i)
342
+ const agentMatch = content.match(/Agent:\s*(.+)/i)
343
+ const estimateMatch = content.match(/Estimate:\s*(.+)/i)
344
+ const priorityMatch = content.match(/Priority:\s*(.+)/i)
345
+
346
+ if (startedMatch) task.started = startedMatch[1].trim()
347
+ if (featureMatch) task.feature = featureMatch[1].trim()
348
+ if (phaseMatch) task.phase = phaseMatch[1].trim()
349
+ if (agentMatch) task.agent = agentMatch[1].trim()
350
+ if (estimateMatch) task.estimate = estimateMatch[1].trim()
351
+ if (priorityMatch) task.priority = priorityMatch[1].trim()
352
+
353
+ return task
354
+ }
355
+
356
+ // Parse next.md - Priority queue with context
357
+ export function parseQueue(content: string): QueueItem[] {
358
+ const items: QueueItem[] = []
359
+ if (!content) return items
360
+
361
+ const lines = content.split('\n')
362
+ let priority = 1
363
+ let currentFeature: string | undefined
364
+
365
+ for (const line of lines) {
366
+ // Track feature headers
367
+ const featureMatch = line.match(/^##\s+(.+)$/)
368
+ if (featureMatch) {
369
+ currentFeature = featureMatch[1].trim()
370
+ continue
371
+ }
372
+
373
+ // Match items: 1. [ ] Task or - [ ] Task
374
+ const itemMatch = line.match(/^(?:\d+\.|[-*])\s*\[([\sx])\]\s*(.+)$/i)
375
+ if (itemMatch) {
376
+ const isCompleted = itemMatch[1].toLowerCase() === 'x'
377
+ if (isCompleted) continue // Skip completed
378
+
379
+ const taskText = itemMatch[2].trim()
380
+
381
+ // Extract agent if present: Task @agent
382
+ const agentMatch = taskText.match(/@(\w+)$/)
383
+ const estimateMatch = taskText.match(/\((\d+[hmd])\)/)
384
+
385
+ items.push({
386
+ task: taskText.replace(/@\w+$/, '').replace(/\(\d+[hmd]\)/, '').trim(),
387
+ priority: priority++,
388
+ feature: currentFeature,
389
+ agent: agentMatch?.[1],
390
+ estimate: estimateMatch?.[1],
391
+ status: 'pending'
392
+ })
393
+ }
394
+ }
395
+
396
+ return items
397
+ }
398
+
399
+ // Parse metrics.md - Full metrics extraction
400
+ export function parseMetrics(content: string): Metrics {
401
+ const defaults: Metrics = {
402
+ tasksStarted: 0,
403
+ tasksCompleted: 0,
404
+ inProgress: 0,
405
+ totalTime: '0h',
406
+ daysActive: 0,
407
+ velocity: { tasksPerDay: 0 }
408
+ }
409
+
410
+ if (!content) return defaults
411
+
412
+ // Basic metrics
413
+ const started = content.match(/Tasks Started\*?\*?:\s*(\d+)/i)
414
+ const completed = content.match(/(?:Tasks Completed|Total Tasks Shipped)\*?\*?:\s*(\d+)/i)
415
+ const inProgress = content.match(/In Progress\*?\*?:\s*(\d+)/i)
416
+ const totalTime = content.match(/(?:Total Time|Total Time Tracked)\*?\*?:\s*([^\n]+)/i)
417
+ const daysActive = content.match(/Days Active\*?\*?:\s*(\d+)/i)
418
+ const tasksPerDay = content.match(/(?:Tasks\/Day|Tasks per Day)\*?\*?:\s*([\d.]+)/i)
419
+ const featuresPerWeek = content.match(/Features\/Week\*?\*?:\s*([\d.]+)/i)
420
+
421
+ const metrics: Metrics = {
422
+ tasksStarted: started ? parseInt(started[1]) : 0,
423
+ tasksCompleted: completed ? parseInt(completed[1]) : 0,
424
+ inProgress: inProgress ? parseInt(inProgress[1]) : 0,
425
+ totalTime: totalTime ? totalTime[1].trim() : '0h',
426
+ daysActive: daysActive ? parseInt(daysActive[1]) : 0,
427
+ velocity: {
428
+ tasksPerDay: tasksPerDay ? parseFloat(tasksPerDay[1]) : 0,
429
+ featuresPerWeek: featuresPerWeek ? parseFloat(featuresPerWeek[1]) : undefined
430
+ }
431
+ }
432
+
433
+ // Test coverage section
434
+ const testSection = content.match(/##\s*Test Coverage([\s\S]*?)(?=##|$)/i)
435
+ if (testSection) {
436
+ const totalTests = testSection[1].match(/Total:\s*(\d+)/i)
437
+ const passingTests = testSection[1].match(/(\d+)\s*passing/i)
438
+ if (totalTests || passingTests) {
439
+ metrics.testCoverage = {
440
+ total: totalTests ? parseInt(totalTests[1]) : 0,
441
+ passing: passingTests ? parseInt(passingTests[1]) : 0
442
+ }
443
+ }
444
+ }
445
+
446
+ // Code quality
447
+ const linesAdded = content.match(/Lines Added\*?\*?:\s*(\d+)/i)
448
+ const linesRemoved = content.match(/Lines Removed\*?\*?:\s*(\d+)/i)
449
+ const filesChanged = content.match(/Files Changed\*?\*?:\s*(\d+)/i)
450
+ if (linesAdded || linesRemoved || filesChanged) {
451
+ metrics.codeQuality = {
452
+ linesAdded: linesAdded ? parseInt(linesAdded[1]) : 0,
453
+ linesRemoved: linesRemoved ? parseInt(linesRemoved[1]) : 0,
454
+ filesChanged: filesChanged ? parseInt(filesChanged[1]) : 0
455
+ }
456
+ }
457
+
458
+ return metrics
459
+ }
460
+
461
+ // Parse shipped.md - Full feature details
462
+ export function parseShipped(content: string): ShippedFeature[] {
463
+ const features: ShippedFeature[] = []
464
+ if (!content) return features
465
+
466
+ // Split by version headers: ## v1.2.3 - Name or ## 2025-01-01
467
+ const sections = content.split(/^##\s+/m).filter(s => s.trim())
468
+
469
+ for (const section of sections) {
470
+ const lines = section.split('\n')
471
+ const headerLine = lines[0]
472
+
473
+ // Parse header: v1.2.3 - Feature Name or date
474
+ const versionMatch = headerLine.match(/^(v[\d.]+)\s*[-–]\s*(.+)$/i)
475
+ const dateMatch = headerLine.match(/^(\d{4}-\d{2}-\d{2})/)
476
+
477
+ if (!versionMatch && !dateMatch) continue
478
+
479
+ const feature: ShippedFeature = {
480
+ date: dateMatch ? dateMatch[1] : new Date().toISOString().split('T')[0],
481
+ name: versionMatch ? versionMatch[2].trim() : '',
482
+ version: versionMatch ? versionMatch[1] : undefined
483
+ }
484
+
485
+ // Parse feature metadata
486
+ for (const line of lines.slice(1)) {
487
+ const typeMatch = line.match(/Type\*?\*?:\s*(\w+)/i)
488
+ const agentMatch = line.match(/Agent\*?\*?:\s*(\w+)/i)
489
+ const timeMatch = line.match(/Time\*?\*?:\s*([^\n]+)/i)
490
+ const commitMatch = line.match(/Commit\*?\*?:\s*(\w+)/i)
491
+ const impactMatch = line.match(/Impact\*?\*?:\s*(\w+)/i)
492
+ const filesMatch = line.match(/Files changed\*?\*?:\s*(\d+)/i)
493
+ const addedMatch = line.match(/\+(\d+)/i)
494
+ const removedMatch = line.match(/-(\d+)/i)
495
+
496
+ if (typeMatch) feature.type = typeMatch[1]
497
+ if (agentMatch) feature.agent = agentMatch[1]
498
+ if (timeMatch) feature.time = timeMatch[1].trim()
499
+ if (commitMatch) feature.commit = commitMatch[1]
500
+ if (impactMatch) feature.impact = impactMatch[1]
501
+ if (filesMatch) feature.filesChanged = parseInt(filesMatch[1])
502
+ if (addedMatch) feature.linesAdded = parseInt(addedMatch[1])
503
+ if (removedMatch) feature.linesRemoved = parseInt(removedMatch[1])
504
+
505
+ // Root cause and solution for fixes
506
+ if (line.includes('Root Cause')) {
507
+ feature.rootCause = line.replace(/.*Root Cause\*?\*?:\s*/i, '').trim()
508
+ }
509
+ if (line.includes('Solution')) {
510
+ feature.solution = line.replace(/.*Solution\*?\*?:\s*/i, '').trim()
511
+ }
512
+ }
513
+
514
+ // If we found name from features list
515
+ if (!feature.name) {
516
+ const featureLineMatch = section.match(/\*\*([^*]+)\*\*/m)
517
+ if (featureLineMatch) {
518
+ feature.name = featureLineMatch[1].trim()
519
+ }
520
+ }
521
+
522
+ if (feature.name) {
523
+ features.push(feature)
524
+ }
525
+ }
526
+
527
+ return features.slice(0, 20) // Last 20
528
+ }
529
+
530
+ // Parse ideas.md - Full idea structure
531
+ export function parseIdeas(content: string): { pending: Idea[]; archived: number; implemented: number } {
532
+ const pending: Idea[] = []
533
+ let archived = 0
534
+ let implemented = 0
535
+
536
+ if (!content) return { pending, archived, implemented }
537
+
538
+ const lines = content.split('\n')
539
+ let inArchived = false
540
+ let inImplemented = false
541
+ let currentIdea: Partial<Idea> | null = null
542
+ let collectingPainPoints = false
543
+ let collectingSolutions = false
544
+
545
+ for (const line of lines) {
546
+ // Check section headers
547
+ if (line.match(/^##.*Archived/i)) {
548
+ inArchived = true
549
+ inImplemented = false
550
+ if (currentIdea?.title) pending.push(currentIdea as Idea)
551
+ currentIdea = null
552
+ continue
553
+ }
554
+ if (line.match(/^##.*Implemented/i)) {
555
+ inImplemented = true
556
+ inArchived = false
557
+ if (currentIdea?.title) pending.push(currentIdea as Idea)
558
+ currentIdea = null
559
+ continue
560
+ }
561
+
562
+ // Date header: ## 2025-01-01
563
+ const dateMatch = line.match(/^##\s*(\d{4}-\d{2}-\d{2})/)
564
+ if (dateMatch) {
565
+ if (currentIdea?.title && !inArchived && !inImplemented) {
566
+ pending.push(currentIdea as Idea)
567
+ }
568
+ currentIdea = { date: dateMatch[1], status: 'PENDING' }
569
+ inArchived = false
570
+ inImplemented = false
571
+ continue
572
+ }
573
+
574
+ // Idea title: ### Title
575
+ const titleMatch = line.match(/^###\s+(.+)$/)
576
+ if (titleMatch) {
577
+ if (inArchived) {
578
+ archived++
579
+ continue
580
+ }
581
+ if (inImplemented) {
582
+ implemented++
583
+ continue
584
+ }
585
+
586
+ if (currentIdea?.title) {
587
+ pending.push(currentIdea as Idea)
588
+ }
589
+ currentIdea = {
590
+ title: titleMatch[1].trim(),
591
+ status: 'PENDING',
592
+ date: currentIdea?.date
593
+ }
594
+ collectingPainPoints = false
595
+ collectingSolutions = false
596
+ continue
597
+ }
598
+
599
+ if (!currentIdea || inArchived || inImplemented) continue
600
+
601
+ // Status
602
+ const statusMatch = line.match(/Status\*?\*?:\s*(\w+)/i)
603
+ if (statusMatch) currentIdea.status = statusMatch[1]
604
+
605
+ // Description
606
+ const descMatch = line.match(/Description\*?\*?:\s*(.+)/i)
607
+ if (descMatch) currentIdea.description = descMatch[1].trim()
608
+
609
+ // Impact and effort
610
+ const impactMatch = line.match(/Impact\*?\*?:\s*(\w+)/i)
611
+ if (impactMatch) currentIdea.impact = impactMatch[1]
612
+
613
+ const effortMatch = line.match(/Effort\*?\*?:\s*([^\n|]+)/i)
614
+ if (effortMatch) currentIdea.effort = effortMatch[1].trim()
615
+
616
+ // Pain points section
617
+ if (line.match(/Pain Points/i)) {
618
+ collectingPainPoints = true
619
+ collectingSolutions = false
620
+ currentIdea.painPoints = []
621
+ continue
622
+ }
623
+ if (line.match(/Solutions/i)) {
624
+ collectingSolutions = true
625
+ collectingPainPoints = false
626
+ currentIdea.solutions = []
627
+ continue
628
+ }
629
+
630
+ // Collect list items
631
+ const listMatch = line.match(/^[-*\d.]\s+(.+)$/)
632
+ if (listMatch) {
633
+ if (collectingPainPoints && currentIdea.painPoints) {
634
+ currentIdea.painPoints.push(listMatch[1].trim())
635
+ } else if (collectingSolutions && currentIdea.solutions) {
636
+ currentIdea.solutions.push(listMatch[1].trim())
637
+ }
638
+ }
639
+ }
640
+
641
+ // Add last idea
642
+ if (currentIdea?.title && !inArchived && !inImplemented) {
643
+ pending.push(currentIdea as Idea)
644
+ }
645
+
646
+ return { pending, archived, implemented }
647
+ }
648
+
649
+ // Parse roadmap.md - Full roadmap structure
650
+ export function parseRoadmap(content: string): { phases: RoadmapPhase[]; completedFeatures: number; totalFeatures: number; progress: number } {
651
+ const phases: RoadmapPhase[] = []
652
+ let completedFeatures = 0
653
+ let totalFeatures = 0
654
+
655
+ if (!content) return { phases, completedFeatures, totalFeatures, progress: 0 }
656
+
657
+ // Split by phase headers
658
+ const phaseSections = content.split(/^##\s+(?:Phase\s+)?/mi).filter(s => s.trim())
659
+
660
+ for (const section of phaseSections) {
661
+ const lines = section.split('\n')
662
+ const header = lines[0]
663
+
664
+ // Match: P1: Name or Phase 1: Name or just number
665
+ const phaseMatch = header.match(/^(?:P)?(\d+)[:\s-]*(.*)$/i)
666
+ if (!phaseMatch) continue
667
+
668
+ const phaseNum = phaseMatch[1]
669
+ const features: RoadmapFeature[] = []
670
+ let phaseStatus: 'completed' | 'in_progress' | 'queued' = 'queued'
671
+
672
+ // Check for status in section
673
+ if (section.match(/Status\*?\*?:\s*(?:COMPLETED|Done)/i)) {
674
+ phaseStatus = 'completed'
675
+ } else if (section.match(/Status\*?\*?:\s*(?:IN.?PROGRESS|Active)/i)) {
676
+ phaseStatus = 'in_progress'
677
+ }
678
+
679
+ // Parse features
680
+ for (const line of lines) {
681
+ // Feature line: - [x] Feature Name (tasks, time) - Shipped date
682
+ const featureMatch = line.match(/^[-*]\s*\[([\sx])\]\s*(.+)/i)
683
+ if (featureMatch) {
684
+ totalFeatures++
685
+ const isCompleted = featureMatch[1].toLowerCase() === 'x'
686
+ if (isCompleted) completedFeatures++
687
+
688
+ const featureText = featureMatch[2]
689
+ const nameMatch = featureText.match(/^([^(]+)/)
690
+ const tasksMatch = featureText.match(/\((\d+)\s*(?:tasks?|\/\d+)/)
691
+ const shippedMatch = featureText.match(/Shipped\s+(\d{4}-\d{2}-\d{2})/i)
692
+ const versionMatch = featureText.match(/\((v[\d.]+)\)/)
693
+
694
+ if (nameMatch) {
695
+ features.push({
696
+ name: nameMatch[1].replace(/\*\*/g, '').trim(),
697
+ status: isCompleted ? 'completed' : 'in_progress',
698
+ tasks: tasksMatch ? parseInt(tasksMatch[1]) : 0,
699
+ tasksCompleted: isCompleted ? (tasksMatch ? parseInt(tasksMatch[1]) : 0) : 0,
700
+ shippedDate: shippedMatch?.[1],
701
+ version: versionMatch?.[1]
702
+ })
703
+
704
+ // Update phase status based on features
705
+ if (!isCompleted) phaseStatus = phaseStatus === 'completed' ? 'in_progress' : phaseStatus
706
+ }
707
+ }
708
+ }
709
+
710
+ // Calculate phase progress
711
+ const completedInPhase = features.filter(f => f.status === 'completed').length
712
+ const phaseProgress = features.length > 0
713
+ ? Math.round((completedInPhase / features.length) * 100)
714
+ : (phaseStatus === 'completed' ? 100 : 0)
715
+
716
+ phases.push({
717
+ name: `P${phaseNum}`,
718
+ status: phaseStatus,
719
+ progress: phaseProgress,
720
+ features
721
+ })
722
+ }
723
+
724
+ // Also check for progress table format
725
+ const tableRows = content.match(/\|\s*P(\d+)[^|]*\|[^|]*\|\s*(\d+)%/gi)
726
+ if (tableRows && phases.length === 0) {
727
+ for (const row of tableRows) {
728
+ const match = row.match(/P(\d+)[^|]*\|[^|]*\|\s*(\d+)%/)
729
+ if (match) {
730
+ const progress = parseInt(match[2])
731
+ phases.push({
732
+ name: `P${match[1]}`,
733
+ status: progress === 100 ? 'completed' : progress > 0 ? 'in_progress' : 'queued',
734
+ progress
735
+ })
736
+ }
737
+ }
738
+ }
739
+
740
+ const overallProgress = totalFeatures > 0
741
+ ? Math.round((completedFeatures / totalFeatures) * 100)
742
+ : 0
743
+
744
+ return { phases, completedFeatures, totalFeatures, progress: overallProgress }
745
+ }
746
+
747
+ // Parse context.jsonl - Full timeline with all event types
748
+ export function parseTimeline(content: string): TimelineEvent[] {
749
+ const events: TimelineEvent[] = []
750
+ if (!content) return events
751
+
752
+ const lines = content.split('\n').filter(l => l.trim())
753
+
754
+ for (const line of lines) {
755
+ try {
756
+ const raw = JSON.parse(line)
757
+ const ts = raw.ts || raw.timestamp || ''
758
+ const type = raw.type || raw.action || 'unknown'
759
+
760
+ // Normalize the event
761
+ const event: TimelineEvent = {
762
+ ts,
763
+ type,
764
+ ...raw
765
+ }
766
+
767
+ events.push(event)
768
+ } catch {
769
+ // Skip invalid JSON
770
+ }
771
+ }
772
+
773
+ // Sort by timestamp descending (most recent first)
774
+ return events.sort((a, b) => {
775
+ const dateA = new Date(a.ts).getTime()
776
+ const dateB = new Date(b.ts).getTime()
777
+ return dateB - dateA
778
+ })
779
+ }
780
+
781
+ // Parse session files from progress/sessions and memory/sessions
782
+ export async function parseSessions(storagePath: string): Promise<SessionDay[]> {
783
+ const sessions: Map<string, SessionDay> = new Map()
784
+
785
+ const sessionDirs = [
786
+ join(storagePath, 'progress', 'sessions'),
787
+ join(storagePath, 'memory', 'sessions')
788
+ ]
789
+
790
+ for (const dir of sessionDirs) {
791
+ // Check for monthly folders
792
+ const months = await safeReadDir(dir)
793
+
794
+ for (const month of months) {
795
+ const monthPath = join(dir, month)
796
+ const files = await safeReadDir(monthPath)
797
+
798
+ for (const file of files) {
799
+ if (!file.endsWith('.jsonl')) continue
800
+
801
+ const date = file.replace('.jsonl', '')
802
+ const content = await safeReadFile(join(monthPath, file))
803
+ const events = parseTimeline(content)
804
+
805
+ if (!sessions.has(date)) {
806
+ sessions.set(date, {
807
+ date,
808
+ events: [],
809
+ tasksStarted: 0,
810
+ tasksCompleted: 0,
811
+ featuresShipped: 0
812
+ })
813
+ }
814
+
815
+ const day = sessions.get(date)!
816
+ day.events.push(...events)
817
+ day.tasksStarted += events.filter(e => e.type === 'task_start').length
818
+ day.tasksCompleted += events.filter(e => e.type === 'task_complete').length
819
+ day.featuresShipped += events.filter(e =>
820
+ e.type === 'ship' || e.type === 'feature_ship' || e.type === 'feature_shipped'
821
+ ).length
822
+ }
823
+ }
824
+
825
+ // Also check root session files
826
+ const rootFiles = await safeReadDir(dir)
827
+ for (const file of rootFiles) {
828
+ if (!file.endsWith('.jsonl')) continue
829
+
830
+ const date = file.replace('.jsonl', '')
831
+ if (date.match(/^\d{4}-\d{2}-\d{2}$/)) {
832
+ const content = await safeReadFile(join(dir, file))
833
+ const events = parseTimeline(content)
834
+
835
+ if (!sessions.has(date)) {
836
+ sessions.set(date, {
837
+ date,
838
+ events: [],
839
+ tasksStarted: 0,
840
+ tasksCompleted: 0,
841
+ featuresShipped: 0
842
+ })
843
+ }
844
+
845
+ const day = sessions.get(date)!
846
+ day.events.push(...events)
847
+ day.tasksStarted += events.filter(e => e.type === 'task_start').length
848
+ day.tasksCompleted += events.filter(e => e.type === 'task_complete').length
849
+ day.featuresShipped += events.filter(e =>
850
+ e.type === 'ship' || e.type === 'feature_ship' || e.type === 'feature_shipped'
851
+ ).length
852
+ }
853
+ }
854
+ }
855
+
856
+ // Sort by date descending
857
+ return Array.from(sessions.values()).sort((a, b) =>
858
+ new Date(b.date).getTime() - new Date(a.date).getTime()
859
+ )
860
+ }
861
+
862
+ // Parse agents - Full agent details
863
+ export async function parseAgents(storagePath: string): Promise<Agent[]> {
864
+ const agents: Agent[] = []
865
+ const agentsDir = join(storagePath, 'agents')
866
+
867
+ const files = await safeReadDir(agentsDir)
868
+
869
+ for (const file of files) {
870
+ if (!file.endsWith('.md')) continue
871
+
872
+ const name = file.replace('.md', '')
873
+ const content = await safeReadFile(join(agentsDir, file))
874
+
875
+ const agent: Agent = { name }
876
+
877
+ // Parse role
878
+ const roleMatch = content.match(/(?:Role|Description)\*?\*?:\s*([^\n]+)/i)
879
+ if (roleMatch) agent.role = roleMatch[1].trim()
880
+
881
+ // Parse responsibilities
882
+ const respSection = content.match(/##\s*Responsibilities([\s\S]*?)(?=##|$)/i)
883
+ if (respSection) {
884
+ const items = respSection[1].match(/^[-*]\s+(.+)$/gm)
885
+ if (items) {
886
+ agent.responsibilities = items.map(i => i.replace(/^[-*]\s+/, '').trim())
887
+ }
888
+ }
889
+
890
+ // Parse when to use
891
+ const whenSection = content.match(/##\s*When to Use([\s\S]*?)(?=##|$)/i)
892
+ if (whenSection) {
893
+ const items = whenSection[1].match(/^[-*]\s+(.+)$/gm)
894
+ if (items) {
895
+ agent.whenToUse = items.map(i => i.replace(/^[-*]\s+/, '').trim())
896
+ }
897
+ }
898
+
899
+ agents.push(agent)
900
+ }
901
+
902
+ return agents
903
+ }
904
+
905
+ // Parse repo-summary.md - Full analysis
906
+ export function parseAnalysis(content: string): AnalysisData {
907
+ const defaults: AnalysisData = { fileCount: 0, commitCount: 0, stack: 'Unknown' }
908
+ if (!content) return defaults
909
+
910
+ const fileMatch = content.match(/(\d+)\s*files/i)
911
+ const commitMatch = content.match(/(?:Total Commits|Commits)\*?\*?:\s*(\d+)/i) || content.match(/(\d+)\s*commits/i)
912
+ const stackMatch = content.match(/(?:Runtime|Tech Stack)[^:]*:\s*([^\n]+)/i)
913
+
914
+ // Extract tech stack array
915
+ const techStackSection = content.match(/##\s*(?:Tech Stack|Dependencies)([\s\S]*?)(?=##|$)/i)
916
+ const techStack: string[] = []
917
+ if (techStackSection) {
918
+ const items = techStackSection[1].match(/[-*]\s+([^:\n]+)/g)
919
+ if (items) {
920
+ techStack.push(...items.map(i => i.replace(/^[-*]\s+/, '').trim()))
921
+ }
922
+ }
923
+
924
+ // Extract structure
925
+ const structureSection = content.match(/##\s*(?:Project )?Structure([\s\S]*?)(?=##|$)/i)
926
+
927
+ // Extract architecture
928
+ const archSection = content.match(/##\s*Architecture([\s\S]*?)(?=##|$)/i)
929
+
930
+ return {
931
+ fileCount: fileMatch ? parseInt(fileMatch[1]) : 0,
932
+ commitCount: commitMatch ? parseInt(commitMatch[1]) : 0,
933
+ stack: stackMatch ? stackMatch[1].trim().replace(/^[-*]\s*/, '') : 'Unknown',
934
+ techStack: techStack.length > 0 ? techStack : undefined,
935
+ structure: structureSection?.[1]?.trim(),
936
+ architecture: archSection?.[1]?.trim()
937
+ }
938
+ }
939
+
940
+ // Parse patterns.md - Code patterns
941
+ export function parsePatterns(content: string): CodePatterns | undefined {
942
+ if (!content) return undefined
943
+
944
+ const patterns: CodePatterns = {}
945
+
946
+ // Module system
947
+ const moduleSection = content.match(/##\s*Module System([\s\S]*?)(?=##|$)/i)
948
+ if (moduleSection) {
949
+ const items = moduleSection[1].match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/g)
950
+ if (items) {
951
+ patterns.moduleSystem = {}
952
+ for (const item of items) {
953
+ const match = item.match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/)
954
+ if (match && !match[1].includes('Pattern')) {
955
+ patterns.moduleSystem[match[1].trim()] = match[2].trim()
956
+ }
957
+ }
958
+ }
959
+ }
960
+
961
+ // Naming conventions
962
+ const namingSection = content.match(/##\s*Naming Conventions([\s\S]*?)(?=##|$)/i)
963
+ if (namingSection) {
964
+ const items = namingSection[1].match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/g)
965
+ if (items) {
966
+ patterns.namingConventions = {}
967
+ for (const item of items) {
968
+ const match = item.match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/)
969
+ if (match && !match[1].includes('Element')) {
970
+ patterns.namingConventions[match[1].trim()] = match[2].trim()
971
+ }
972
+ }
973
+ }
974
+ }
975
+
976
+ return Object.keys(patterns).length > 0 ? patterns : undefined
977
+ }
978
+
979
+ // ============================================
980
+ // MAIN EXPORT
981
+ // ============================================
982
+
983
+ export async function getProjectStats(projectId: string): Promise<ProjectStats> {
984
+ const storagePath = getStoragePath(projectId)
985
+
986
+ // Read all files in parallel
987
+ const [
988
+ nowContent,
989
+ nextContent,
990
+ metricsContent,
991
+ shippedContent,
992
+ ideasContent,
993
+ roadmapContent,
994
+ timelineContent,
995
+ summaryContent,
996
+ patternsContent,
997
+ agents,
998
+ sessions
999
+ ] = await Promise.all([
1000
+ safeReadFile(join(storagePath, 'core', 'now.md')),
1001
+ safeReadFile(join(storagePath, 'core', 'next.md')),
1002
+ safeReadFile(join(storagePath, 'progress', 'metrics.md')),
1003
+ safeReadFile(join(storagePath, 'progress', 'shipped.md')),
1004
+ safeReadFile(join(storagePath, 'planning', 'ideas.md')),
1005
+ safeReadFile(join(storagePath, 'planning', 'roadmap.md')),
1006
+ safeReadFile(join(storagePath, 'memory', 'context.jsonl')),
1007
+ safeReadFile(join(storagePath, 'analysis', 'repo-summary.md')),
1008
+ safeReadFile(join(storagePath, 'analysis', 'patterns.md')),
1009
+ parseAgents(storagePath),
1010
+ parseSessions(storagePath)
1011
+ ])
1012
+
1013
+ const timeline = parseTimeline(timelineContent)
1014
+ const ideas = parseIdeas(ideasContent)
1015
+ const roadmap = parseRoadmap(roadmapContent)
1016
+
1017
+ // Calculate summary stats from timeline
1018
+ const taskStarts = timeline.filter(e => e.type === 'task_start')
1019
+ const taskCompletes = timeline.filter(e => e.type === 'task_complete')
1020
+ const ships = timeline.filter(e => ['ship', 'feature_ship', 'feature_shipped'].includes(e.type))
1021
+ const bugFixes = timeline.filter(e => e.type === 'bug_fix')
1022
+ const cleanups = timeline.filter(e => e.type === 'cleanup')
1023
+
1024
+ const firstEvent = timeline[timeline.length - 1]
1025
+ const lastEvent = timeline[0]
1026
+
1027
+ // Calculate active days
1028
+ const uniqueDays = new Set(timeline.map(e => e.ts?.split('T')[0]).filter(Boolean))
1029
+
1030
+ return {
1031
+ currentTask: parseNow(nowContent),
1032
+ metrics: parseMetrics(metricsContent),
1033
+ shipped: parseShipped(shippedContent),
1034
+ queue: parseQueue(nextContent),
1035
+ ideas,
1036
+ roadmap,
1037
+ agents,
1038
+ timeline: timeline.slice(0, 100), // Last 100 events
1039
+ sessions: sessions.slice(0, 30), // Last 30 days
1040
+ analysis: parseAnalysis(summaryContent),
1041
+ patterns: parsePatterns(patternsContent),
1042
+ summary: {
1043
+ totalEvents: timeline.length,
1044
+ firstActivity: firstEvent?.ts,
1045
+ lastActivity: lastEvent?.ts,
1046
+ activeDays: uniqueDays.size,
1047
+ totalTasksEver: taskStarts.length,
1048
+ totalShipsEver: ships.length,
1049
+ totalBugsFixed: bugFixes.length,
1050
+ totalCleanups: cleanups.length
1051
+ }
1052
+ }
1053
+ }
1054
+
1055
+ // ============================================
1056
+ // RAW FILES - Return markdown files as-is for rendering
1057
+ // ============================================
1058
+
1059
+ export interface RawProjectFiles {
1060
+ shipped: string // progress/shipped.md
1061
+ roadmap: string // planning/roadmap.md
1062
+ ideas: string // planning/ideas.md
1063
+ queue: string // core/next.md
1064
+ now: string // core/now.md
1065
+ context: string // core/context.md
1066
+ timeline: string // memory/context.jsonl (raw for custom rendering)
1067
+ agents: { name: string; content: string }[] // agents/*.md
1068
+ }
1069
+
1070
+ export async function getRawProjectFiles(projectId: string): Promise<RawProjectFiles> {
1071
+ const storagePath = join(homedir(), '.prjct-cli', 'projects', projectId)
1072
+
1073
+ // Read all files in parallel
1074
+ const [
1075
+ shipped,
1076
+ roadmap,
1077
+ ideas,
1078
+ queue,
1079
+ now,
1080
+ context,
1081
+ timeline,
1082
+ agentFiles
1083
+ ] = await Promise.all([
1084
+ safeReadFile(join(storagePath, 'progress', 'shipped.md')),
1085
+ safeReadFile(join(storagePath, 'planning', 'roadmap.md')),
1086
+ safeReadFile(join(storagePath, 'planning', 'ideas.md')),
1087
+ safeReadFile(join(storagePath, 'core', 'next.md')),
1088
+ safeReadFile(join(storagePath, 'core', 'now.md')),
1089
+ safeReadFile(join(storagePath, 'core', 'context.md')),
1090
+ safeReadFile(join(storagePath, 'memory', 'context.jsonl')),
1091
+ readAgentsRaw(join(storagePath, 'agents'))
1092
+ ])
1093
+
1094
+ return {
1095
+ shipped,
1096
+ roadmap,
1097
+ ideas,
1098
+ queue,
1099
+ now,
1100
+ context,
1101
+ timeline,
1102
+ agents: agentFiles
1103
+ }
1104
+ }
1105
+
1106
+ async function readAgentsRaw(agentsDir: string): Promise<{ name: string; content: string }[]> {
1107
+ try {
1108
+ const files = await readdir(agentsDir)
1109
+ const mdFiles = files.filter(f => f.endsWith('.md'))
1110
+
1111
+ const agents = await Promise.all(
1112
+ mdFiles.map(async (file) => ({
1113
+ name: file.replace('.md', ''),
1114
+ content: await safeReadFile(join(agentsDir, file))
1115
+ }))
1116
+ )
1117
+
1118
+ return agents.filter(a => a.content.trim())
1119
+ } catch {
1120
+ return []
1121
+ }
1122
+ }