prjct-cli 0.11.4 → 0.11.5

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 (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/serve.js +22 -6
  3. package/core/__tests__/utils/date-helper.test.js +416 -0
  4. package/core/agentic/agent-router.js +30 -18
  5. package/core/agentic/command-executor.js +20 -24
  6. package/core/agentic/context-builder.js +7 -8
  7. package/core/agentic/memory-system.js +14 -19
  8. package/core/agentic/prompt-builder.js +41 -27
  9. package/core/agentic/template-loader.js +8 -2
  10. package/core/infrastructure/agent-detector.js +7 -4
  11. package/core/infrastructure/migrator.js +10 -13
  12. package/core/infrastructure/session-manager.js +10 -10
  13. package/package.json +1 -1
  14. package/packages/web/app/project/[id]/stats/page.tsx +102 -343
  15. package/packages/web/components/stats/ActivityTimeline.tsx +201 -0
  16. package/packages/web/components/stats/AgentsCard.tsx +56 -0
  17. package/packages/web/components/stats/BentoCard.tsx +88 -0
  18. package/packages/web/components/stats/BentoGrid.tsx +22 -0
  19. package/packages/web/components/stats/EmptyState.tsx +67 -0
  20. package/packages/web/components/stats/HeroSection.tsx +172 -0
  21. package/packages/web/components/stats/IdeasCard.tsx +59 -0
  22. package/packages/web/components/stats/NowCard.tsx +71 -0
  23. package/packages/web/components/stats/ProgressRing.tsx +74 -0
  24. package/packages/web/components/stats/QueueCard.tsx +58 -0
  25. package/packages/web/components/stats/RoadmapCard.tsx +97 -0
  26. package/packages/web/components/stats/ShipsCard.tsx +70 -0
  27. package/packages/web/components/stats/SparklineChart.tsx +44 -0
  28. package/packages/web/components/stats/StreakCard.tsx +59 -0
  29. package/packages/web/components/stats/VelocityCard.tsx +60 -0
  30. package/packages/web/components/stats/index.ts +17 -0
  31. package/packages/web/components/ui/tooltip.tsx +2 -2
  32. package/packages/web/next-env.d.ts +1 -1
  33. package/packages/web/package.json +2 -1
@@ -0,0 +1,71 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { EmptyState } from './EmptyState'
5
+ import { Target, Clock } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ interface CurrentTask {
9
+ task: string
10
+ duration?: string
11
+ startedAt?: string
12
+ }
13
+
14
+ interface NowCardProps {
15
+ currentTask: CurrentTask | null
16
+ className?: string
17
+ }
18
+
19
+ export function NowCard({ currentTask, className }: NowCardProps) {
20
+ return (
21
+ <BentoCard
22
+ size="2x2"
23
+ title="Now"
24
+ icon={Target}
25
+ accentColor={currentTask ? 'warning' : 'default'}
26
+ className={className}
27
+ >
28
+ {currentTask ? (
29
+ <div className="flex flex-col h-full">
30
+ {/* Status indicator */}
31
+ <div className="flex items-center gap-2 mb-3">
32
+ <div className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" />
33
+ <span className="text-[10px] font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-wider">
34
+ Working
35
+ </span>
36
+ </div>
37
+
38
+ {/* Task name */}
39
+ <p className="text-xl font-semibold leading-tight flex-1">
40
+ {currentTask.task}
41
+ </p>
42
+
43
+ {/* Duration */}
44
+ {currentTask.duration && (
45
+ <div className="flex items-center gap-2 mt-4 text-muted-foreground">
46
+ <Clock className="h-3.5 w-3.5" />
47
+ <span className="text-sm">{currentTask.duration} elapsed</span>
48
+ </div>
49
+ )}
50
+
51
+ {/* Progress visualization - simple bar */}
52
+ <div className="mt-4">
53
+ <div className="h-1.5 bg-muted rounded-full overflow-hidden">
54
+ <div
55
+ className="h-full bg-amber-500 rounded-full animate-pulse"
56
+ style={{ width: '60%' }}
57
+ />
58
+ </div>
59
+ </div>
60
+ </div>
61
+ ) : (
62
+ <EmptyState
63
+ icon={Target}
64
+ title="No active task"
65
+ description="Start working on something"
66
+ command="/p:now"
67
+ />
68
+ )}
69
+ </BentoCard>
70
+ )
71
+ }
@@ -0,0 +1,74 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ interface ProgressRingProps {
6
+ value: number // 0-100
7
+ size?: 'sm' | 'md' | 'lg' | 'xl'
8
+ showValue?: boolean
9
+ strokeWidth?: number
10
+ className?: string
11
+ accentColor?: 'default' | 'success' | 'warning' | 'destructive'
12
+ }
13
+
14
+ const sizes = {
15
+ sm: { container: 'h-8 w-8', text: 'text-[10px]', viewBox: 36, radius: 14 },
16
+ md: { container: 'h-12 w-12', text: 'text-xs', viewBox: 36, radius: 14 },
17
+ lg: { container: 'h-16 w-16', text: 'text-sm', viewBox: 36, radius: 14 },
18
+ xl: { container: 'h-20 w-20', text: 'text-base', viewBox: 36, radius: 14 },
19
+ }
20
+
21
+ const colorStyles = {
22
+ default: 'text-foreground',
23
+ success: 'text-emerald-500',
24
+ warning: 'text-amber-500',
25
+ destructive: 'text-destructive',
26
+ }
27
+
28
+ export function ProgressRing({
29
+ value,
30
+ size = 'md',
31
+ showValue = true,
32
+ strokeWidth = 3,
33
+ className,
34
+ accentColor = 'default',
35
+ }: ProgressRingProps) {
36
+ const { container, text, viewBox, radius } = sizes[size]
37
+ const circumference = 2 * Math.PI * radius
38
+ const strokeDashoffset = circumference - (value / 100) * circumference
39
+
40
+ return (
41
+ <div className={cn('relative', container, className)}>
42
+ <svg className="h-full w-full -rotate-90" viewBox={`0 0 ${viewBox} ${viewBox}`}>
43
+ {/* Background ring */}
44
+ <circle
45
+ cx={viewBox / 2}
46
+ cy={viewBox / 2}
47
+ r={radius}
48
+ fill="none"
49
+ stroke="currentColor"
50
+ strokeWidth={strokeWidth}
51
+ className="text-foreground/10"
52
+ />
53
+ {/* Progress ring */}
54
+ <circle
55
+ cx={viewBox / 2}
56
+ cy={viewBox / 2}
57
+ r={radius}
58
+ fill="none"
59
+ stroke="currentColor"
60
+ strokeWidth={strokeWidth}
61
+ strokeDasharray={circumference}
62
+ strokeDashoffset={strokeDashoffset}
63
+ strokeLinecap="round"
64
+ className={cn('transition-all duration-700 ease-out', colorStyles[accentColor])}
65
+ />
66
+ </svg>
67
+ {showValue && (
68
+ <span className={cn('absolute inset-0 flex items-center justify-center font-bold tabular-nums', text)}>
69
+ {value}
70
+ </span>
71
+ )}
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,58 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { EmptyState } from './EmptyState'
5
+ import { ListTodo } from 'lucide-react'
6
+
7
+ interface QueueItem {
8
+ task: string
9
+ priority?: number
10
+ }
11
+
12
+ interface QueueCardProps {
13
+ queue: QueueItem[]
14
+ className?: string
15
+ }
16
+
17
+ export function QueueCard({ queue, className }: QueueCardProps) {
18
+ const displayItems = queue.slice(0, 5)
19
+ const remaining = queue.length - 5
20
+
21
+ return (
22
+ <BentoCard
23
+ size="1x2"
24
+ title="Queue"
25
+ icon={ListTodo}
26
+ count={queue.length}
27
+ className={className}
28
+ >
29
+ {queue.length === 0 ? (
30
+ <EmptyState
31
+ icon={ListTodo}
32
+ title="Queue empty"
33
+ description="Plan your next tasks"
34
+ command="/p:next"
35
+ compact
36
+ />
37
+ ) : (
38
+ <div className="space-y-2">
39
+ {displayItems.map((item, i) => (
40
+ <div key={i} className="flex items-start gap-2 group">
41
+ <span className="text-[10px] font-medium text-muted-foreground w-4 shrink-0 pt-0.5 tabular-nums">
42
+ {i + 1}
43
+ </span>
44
+ <p className="text-sm leading-tight truncate flex-1 group-hover:text-foreground transition-colors">
45
+ {item.task}
46
+ </p>
47
+ </div>
48
+ ))}
49
+ {remaining > 0 && (
50
+ <p className="text-[10px] text-muted-foreground mt-2 pl-6">
51
+ +{remaining} more
52
+ </p>
53
+ )}
54
+ </div>
55
+ )}
56
+ </BentoCard>
57
+ )
58
+ }
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { EmptyState } from './EmptyState'
5
+ import { Map } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ interface RoadmapPhase {
9
+ name: string
10
+ progress: number
11
+ features?: { name: string; status: string }[]
12
+ }
13
+
14
+ interface RoadmapData {
15
+ phases: RoadmapPhase[]
16
+ progress: number
17
+ }
18
+
19
+ interface RoadmapCardProps {
20
+ roadmap: RoadmapData | null
21
+ className?: string
22
+ }
23
+
24
+ export function RoadmapCard({ roadmap, className }: RoadmapCardProps) {
25
+ const hasPhases = roadmap?.phases && roadmap.phases.length > 0
26
+
27
+ return (
28
+ <BentoCard
29
+ size="2x2"
30
+ title="Roadmap"
31
+ icon={Map}
32
+ count={hasPhases ? `${roadmap.progress}%` : undefined}
33
+ className={className}
34
+ >
35
+ {!hasPhases ? (
36
+ <EmptyState
37
+ icon={Map}
38
+ title="No roadmap yet"
39
+ description="Plan your features"
40
+ command="/p:feature"
41
+ />
42
+ ) : (
43
+ <div className="flex flex-col h-full">
44
+ {/* Overall progress */}
45
+ <div className="mb-4">
46
+ <div className="flex items-center justify-between text-xs mb-1.5">
47
+ <span className="text-muted-foreground">Overall progress</span>
48
+ <span className="font-bold tabular-nums">{roadmap.progress}%</span>
49
+ </div>
50
+ <div className="h-2 bg-muted rounded-full overflow-hidden">
51
+ <div
52
+ className={cn(
53
+ 'h-full rounded-full transition-all duration-500',
54
+ roadmap.progress === 100 ? 'bg-emerald-500' : 'bg-foreground'
55
+ )}
56
+ style={{ width: `${roadmap.progress}%` }}
57
+ />
58
+ </div>
59
+ </div>
60
+
61
+ {/* Phases */}
62
+ <div className="space-y-3 flex-1">
63
+ {roadmap.phases
64
+ .filter(p => (p.features || []).length > 0)
65
+ .slice(0, 4)
66
+ .map((phase) => (
67
+ <div key={phase.name}>
68
+ <div className="flex items-center justify-between text-xs mb-1">
69
+ <span className="font-medium truncate">{phase.name}</span>
70
+ <span className="text-muted-foreground tabular-nums shrink-0 ml-2">
71
+ {phase.progress}%
72
+ </span>
73
+ </div>
74
+ <div className="h-1.5 bg-muted rounded-full overflow-hidden">
75
+ <div
76
+ className={cn(
77
+ 'h-full rounded-full transition-all duration-300',
78
+ phase.progress === 100 ? 'bg-emerald-500' : 'bg-foreground/70'
79
+ )}
80
+ style={{ width: `${phase.progress}%` }}
81
+ />
82
+ </div>
83
+ </div>
84
+ ))}
85
+ </div>
86
+
87
+ {/* Feature count */}
88
+ {roadmap.phases.length > 0 && (
89
+ <p className="text-[10px] text-muted-foreground mt-3">
90
+ {roadmap.phases.reduce((acc, p) => acc + (p.features?.length || 0), 0)} features across {roadmap.phases.length} phases
91
+ </p>
92
+ )}
93
+ </div>
94
+ )}
95
+ </BentoCard>
96
+ )
97
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { EmptyState } from './EmptyState'
5
+ import { Rocket } from 'lucide-react'
6
+ import { Badge } from '@/components/ui/badge'
7
+
8
+ interface Ship {
9
+ name: string
10
+ date: string
11
+ version?: string
12
+ duration?: string
13
+ }
14
+
15
+ interface ShipsCardProps {
16
+ ships: Ship[]
17
+ totalShips?: number
18
+ className?: string
19
+ }
20
+
21
+ function formatShipDate(dateString: string): string {
22
+ const date = new Date(dateString)
23
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
24
+ }
25
+
26
+ export function ShipsCard({ ships, totalShips = 0, className }: ShipsCardProps) {
27
+ const displayShips = ships.slice(0, 4)
28
+
29
+ return (
30
+ <BentoCard
31
+ size="1x2"
32
+ title="Ships"
33
+ icon={Rocket}
34
+ count={totalShips}
35
+ accentColor={ships.length > 0 ? 'success' : 'default'}
36
+ className={className}
37
+ >
38
+ {ships.length === 0 ? (
39
+ <EmptyState
40
+ icon={Rocket}
41
+ title="Nothing shipped yet"
42
+ description="Ship your first feature"
43
+ command="/p:ship"
44
+ compact
45
+ />
46
+ ) : (
47
+ <div className="space-y-3">
48
+ {displayShips.map((ship, i) => (
49
+ <div key={i} className="group">
50
+ <div className="flex items-center gap-2">
51
+ {ship.version && (
52
+ <Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono shrink-0">
53
+ {ship.version}
54
+ </Badge>
55
+ )}
56
+ <p className="text-sm font-medium truncate group-hover:text-foreground transition-colors">
57
+ {ship.name}
58
+ </p>
59
+ </div>
60
+ <p className="text-[10px] text-muted-foreground mt-0.5">
61
+ {formatShipDate(ship.date)}
62
+ {ship.duration && ` · ${ship.duration}`}
63
+ </p>
64
+ </div>
65
+ ))}
66
+ </div>
67
+ )}
68
+ </BentoCard>
69
+ )
70
+ }
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { Area, AreaChart, ResponsiveContainer } from 'recharts'
4
+
5
+ interface SparklineChartProps {
6
+ data: number[]
7
+ color?: string
8
+ height?: number
9
+ showArea?: boolean
10
+ }
11
+
12
+ export function SparklineChart({
13
+ data,
14
+ color = 'currentColor',
15
+ height = 32,
16
+ showArea = true,
17
+ }: SparklineChartProps) {
18
+ const chartData = data.map((value, index) => ({ index, value }))
19
+
20
+ if (data.length === 0) {
21
+ return <div style={{ height }} className="w-full" />
22
+ }
23
+
24
+ return (
25
+ <ResponsiveContainer width="100%" height={height}>
26
+ <AreaChart data={chartData} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
27
+ <defs>
28
+ <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
29
+ <stop offset="0%" stopColor={color} stopOpacity={0.3} />
30
+ <stop offset="100%" stopColor={color} stopOpacity={0} />
31
+ </linearGradient>
32
+ </defs>
33
+ <Area
34
+ type="monotone"
35
+ dataKey="value"
36
+ stroke={color}
37
+ strokeWidth={1.5}
38
+ fill={showArea ? 'url(#sparklineGradient)' : 'none'}
39
+ isAnimationActive={false}
40
+ />
41
+ </AreaChart>
42
+ </ResponsiveContainer>
43
+ )
44
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { Flame } from 'lucide-react'
5
+ import { cn } from '@/lib/utils'
6
+
7
+ interface StreakCardProps {
8
+ streak: number
9
+ className?: string
10
+ }
11
+
12
+ export function StreakCard({ streak, className }: StreakCardProps) {
13
+ const isHot = streak >= 3
14
+ const isOnFire = streak >= 7
15
+
16
+ return (
17
+ <BentoCard
18
+ size="1x1"
19
+ title="Streak"
20
+ icon={Flame}
21
+ accentColor={isOnFire ? 'warning' : 'default'}
22
+ className={className}
23
+ >
24
+ <div className="flex flex-col h-full justify-between">
25
+ <div className="flex items-center gap-3">
26
+ <Flame
27
+ className={cn(
28
+ 'h-8 w-8 transition-colors',
29
+ isOnFire ? 'text-orange-500' : isHot ? 'text-amber-500' : 'text-muted-foreground'
30
+ )}
31
+ />
32
+ <div>
33
+ <p className="text-3xl font-bold tabular-nums">{streak}</p>
34
+ <p className="text-xs text-muted-foreground">day{streak !== 1 ? 's' : ''}</p>
35
+ </div>
36
+ </div>
37
+
38
+ {/* Visual streak indicator - dots */}
39
+ <div className="flex gap-1 mt-3">
40
+ {Array.from({ length: 7 }).map((_, i) => (
41
+ <div
42
+ key={i}
43
+ className={cn(
44
+ 'h-1.5 flex-1 rounded-full transition-colors',
45
+ i < streak
46
+ ? isOnFire
47
+ ? 'bg-orange-500'
48
+ : isHot
49
+ ? 'bg-amber-500'
50
+ : 'bg-foreground'
51
+ : 'bg-muted'
52
+ )}
53
+ />
54
+ ))}
55
+ </div>
56
+ </div>
57
+ </BentoCard>
58
+ )
59
+ }
@@ -0,0 +1,60 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { SparklineChart } from './SparklineChart'
5
+ import { Zap, TrendingUp, TrendingDown } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ interface VelocityCardProps {
9
+ tasksPerDay: number
10
+ weeklyData?: number[]
11
+ change?: number
12
+ className?: string
13
+ }
14
+
15
+ export function VelocityCard({
16
+ tasksPerDay,
17
+ weeklyData = [],
18
+ change = 0,
19
+ className,
20
+ }: VelocityCardProps) {
21
+ return (
22
+ <BentoCard
23
+ size="1x1"
24
+ title="Velocity"
25
+ icon={Zap}
26
+ className={className}
27
+ >
28
+ <div className="flex flex-col h-full justify-between">
29
+ <div>
30
+ <p className="text-3xl font-bold tabular-nums">{tasksPerDay}</p>
31
+ <p className="text-xs text-muted-foreground">tasks/day</p>
32
+ </div>
33
+
34
+ {weeklyData.length > 0 && (
35
+ <div className="mt-2">
36
+ <SparklineChart data={weeklyData} height={28} />
37
+ </div>
38
+ )}
39
+
40
+ {change !== 0 && (
41
+ <div className="flex items-center gap-1 mt-2">
42
+ {change >= 0 ? (
43
+ <TrendingUp className="h-3 w-3 text-emerald-500" />
44
+ ) : (
45
+ <TrendingDown className="h-3 w-3 text-muted-foreground" />
46
+ )}
47
+ <span
48
+ className={cn(
49
+ 'text-xs font-medium',
50
+ change >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'
51
+ )}
52
+ >
53
+ {change >= 0 ? '+' : ''}{change}%
54
+ </span>
55
+ </div>
56
+ )}
57
+ </div>
58
+ </BentoCard>
59
+ )
60
+ }
@@ -0,0 +1,17 @@
1
+ export { BentoCard, BentoCardSkeleton } from './BentoCard'
2
+ export type { BentoSize, BentoCardProps } from './BentoCard'
3
+
4
+ export { BentoGrid } from './BentoGrid'
5
+ export { SparklineChart } from './SparklineChart'
6
+ export { ProgressRing } from './ProgressRing'
7
+ export { EmptyState } from './EmptyState'
8
+ export { HeroSection } from './HeroSection'
9
+ export { NowCard } from './NowCard'
10
+ export { VelocityCard } from './VelocityCard'
11
+ export { StreakCard } from './StreakCard'
12
+ export { QueueCard } from './QueueCard'
13
+ export { ShipsCard } from './ShipsCard'
14
+ export { IdeasCard } from './IdeasCard'
15
+ export { AgentsCard } from './AgentsCard'
16
+ export { RoadmapCard } from './RoadmapCard'
17
+ export { ActivityTimeline } from './ActivityTimeline'
@@ -46,13 +46,13 @@ function TooltipContent({
46
46
  data-slot="tooltip-content"
47
47
  sideOffset={sideOffset}
48
48
  className={cn(
49
- "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
49
+ "bg-popover text-popover-foreground border shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
50
50
  className
51
51
  )}
52
52
  {...props}
53
53
  >
54
54
  {children}
55
- <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
55
+ <TooltipPrimitive.Arrow className="bg-popover fill-popover z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
56
56
  </TooltipPrimitive.Content>
57
57
  </TooltipPrimitive.Portal>
58
58
  )
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -6,7 +6,8 @@
6
6
  "dev": "tsx server.ts",
7
7
  "dev:next": "next dev",
8
8
  "build": "next build",
9
- "start": "node server.js",
9
+ "start": "NODE_ENV=production node server.js",
10
+ "start:prod": "NODE_ENV=production tsx server.ts",
10
11
  "lint": "eslint"
11
12
  },
12
13
  "dependencies": {