prjct-cli 0.12.2 → 0.13.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 (40) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CLAUDE.md +18 -6
  3. package/core/data/index.ts +19 -5
  4. package/core/data/md-base-manager.ts +203 -0
  5. package/core/data/md-queue-manager.ts +179 -0
  6. package/core/data/md-state-manager.ts +133 -0
  7. package/core/serializers/index.ts +20 -0
  8. package/core/serializers/queue-serializer.ts +210 -0
  9. package/core/serializers/state-serializer.ts +136 -0
  10. package/core/utils/file-helper.ts +12 -0
  11. package/package.json +1 -1
  12. package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
  13. package/packages/web/app/page.tsx +1 -6
  14. package/packages/web/app/project/[id]/page.tsx +34 -1
  15. package/packages/web/app/project/[id]/stats/page.tsx +11 -5
  16. package/packages/web/app/settings/page.tsx +2 -221
  17. package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
  18. package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
  19. package/packages/web/components/BlockersCard/index.ts +2 -0
  20. package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
  21. package/packages/web/lib/projects.ts +28 -27
  22. package/packages/web/lib/services/projects.server.ts +25 -21
  23. package/packages/web/lib/services/stats.server.ts +355 -57
  24. package/packages/web/next-env.d.ts +1 -1
  25. package/packages/web/package.json +0 -4
  26. package/templates/commands/decision.md +226 -0
  27. package/templates/commands/done.md +100 -68
  28. package/templates/commands/feature.md +102 -103
  29. package/templates/commands/idea.md +41 -38
  30. package/templates/commands/now.md +94 -33
  31. package/templates/commands/pause.md +90 -30
  32. package/templates/commands/ship.md +179 -74
  33. package/templates/commands/sync.md +324 -200
  34. package/packages/web/app/api/migrate/route.ts +0 -46
  35. package/packages/web/app/api/settings/route.ts +0 -97
  36. package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
  37. package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
  38. package/packages/web/components/MigrationGate/index.ts +0 -1
  39. package/packages/web/lib/json-loader.ts +0 -630
  40. package/packages/web/lib/services/migration.server.ts +0 -580
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Queue Serializer
3
+ *
4
+ * Parses and serializes next.md for task queue.
5
+ *
6
+ * MD Format (next.md):
7
+ * ```
8
+ * # Priority Queue
9
+ *
10
+ * > Tasks ready to start
11
+ *
12
+ * ## Active Tasks
13
+ * 1. [ ] Task description @agent (from: Feature Name)
14
+ * 2. [x] Completed task ✅
15
+ *
16
+ * ## Previously Active
17
+ * - [ ] Paused task
18
+ *
19
+ * ## Backlog
20
+ * - [ ] 🐛 [HIGH] Bug description
21
+ * - [ ] Feature task
22
+ * ```
23
+ */
24
+
25
+ import type { QueueJson, QueueTask, Priority, TaskType, TaskSection } from '../schemas/state'
26
+
27
+ /**
28
+ * Parse next.md content to QueueJson
29
+ */
30
+ export function parseQueue(content: string): QueueJson {
31
+ if (!content || !content.trim()) {
32
+ return { tasks: [], lastUpdated: '' }
33
+ }
34
+
35
+ const lines = content.split('\n')
36
+ const tasks: QueueTask[] = []
37
+ let currentSection: TaskSection = 'active'
38
+ let taskIndex = 0
39
+
40
+ for (const line of lines) {
41
+ // Detect section headers
42
+ if (line.match(/^##\s*Active/i)) {
43
+ currentSection = 'active'
44
+ continue
45
+ }
46
+ if (line.match(/^##\s*Previously/i)) {
47
+ currentSection = 'previously_active'
48
+ continue
49
+ }
50
+ if (line.match(/^##\s*Backlog/i)) {
51
+ currentSection = 'backlog'
52
+ continue
53
+ }
54
+
55
+ // Parse task lines: "1. [ ] Task" or "- [ ] Task" or "- [x] Task"
56
+ const taskMatch = line.match(/^(?:\d+\.|[-*])\s*\[([\sx])\]\s*(.+)$/i)
57
+ if (!taskMatch) continue
58
+
59
+ const isCompleted = taskMatch[1].toLowerCase() === 'x'
60
+ let taskText = taskMatch[2].trim()
61
+
62
+ // Extract agent: @fe, @be, @fe+be
63
+ const agentMatch = taskText.match(/@(\w+(?:\+\w+)?)/)
64
+ const agent = agentMatch ? agentMatch[1] : undefined
65
+ taskText = taskText.replace(/@\w+(?:\+\w+)?/, '').trim()
66
+
67
+ // Extract origin feature: (from: Feature Name)
68
+ const fromMatch = taskText.match(/\(from:\s*([^)]+)\)/)
69
+ const originFeature = fromMatch ? fromMatch[1].trim() : undefined
70
+ taskText = taskText.replace(/\(from:\s*[^)]+\)/, '').trim()
71
+
72
+ // Detect task type from emoji/prefix
73
+ let type: TaskType = 'feature'
74
+ let priority: Priority = 'medium'
75
+
76
+ if (taskText.includes('🐛') || taskText.toLowerCase().includes('bug')) {
77
+ type = 'bug'
78
+ taskText = taskText.replace('🐛', '').trim()
79
+ }
80
+ if (taskText.includes('🔧') || taskText.toLowerCase().includes('fix')) {
81
+ type = 'improvement'
82
+ }
83
+ if (taskText.includes('♻️') || taskText.toLowerCase().includes('refactor')) {
84
+ type = 'chore'
85
+ }
86
+
87
+ // Extract priority: [HIGH], [MEDIUM], [LOW], [CRITICAL]
88
+ const priorityMatch = taskText.match(/\[(HIGH|MEDIUM|LOW|CRITICAL)\]/i)
89
+ if (priorityMatch) {
90
+ priority = priorityMatch[1].toLowerCase() as Priority
91
+ taskText = taskText.replace(/\[(HIGH|MEDIUM|LOW|CRITICAL)\]/i, '').trim()
92
+ }
93
+
94
+ // Remove checkmarks and clean up
95
+ taskText = taskText.replace(/✅/g, '').trim()
96
+
97
+ // Skip empty descriptions
98
+ if (!taskText) continue
99
+
100
+ const task: QueueTask = {
101
+ id: `task_${Date.now()}_${taskIndex++}`,
102
+ description: taskText,
103
+ priority,
104
+ type,
105
+ completed: isCompleted,
106
+ createdAt: new Date().toISOString(),
107
+ section: currentSection
108
+ }
109
+
110
+ if (agent) task.agent = agent
111
+ if (originFeature) task.originFeature = originFeature
112
+ if (isCompleted) task.completedAt = new Date().toISOString()
113
+
114
+ tasks.push(task)
115
+ }
116
+
117
+ // Extract updated date
118
+ const updatedMatch = content.match(/_Updated:\s*(\d{4}-\d{2}-\d{2})/)
119
+ const lastUpdated = updatedMatch ? updatedMatch[1] : new Date().toISOString().split('T')[0]
120
+
121
+ return { tasks, lastUpdated }
122
+ }
123
+
124
+ /**
125
+ * Serialize QueueJson to next.md format
126
+ */
127
+ export function serializeQueue(data: QueueJson): string {
128
+ const lines: string[] = [
129
+ '# Priority Queue',
130
+ '',
131
+ '> Tasks ready to start (max 100)',
132
+ '> Auto-updated by prjct',
133
+ ''
134
+ ]
135
+
136
+ // Group tasks by section
137
+ const active = data.tasks.filter(t => t.section === 'active')
138
+ const previouslyActive = data.tasks.filter(t => t.section === 'previously_active')
139
+ const backlog = data.tasks.filter(t => t.section === 'backlog')
140
+
141
+ // Active Tasks
142
+ if (active.length > 0) {
143
+ lines.push('## Active Tasks', '')
144
+ active.forEach((task, i) => {
145
+ lines.push(formatTask(task, i + 1, true))
146
+ })
147
+ lines.push('')
148
+ }
149
+
150
+ // Previously Active
151
+ if (previouslyActive.length > 0) {
152
+ lines.push('## Previously Active', '')
153
+ previouslyActive.forEach(task => {
154
+ lines.push(formatTask(task, 0, false))
155
+ })
156
+ lines.push('')
157
+ }
158
+
159
+ // Backlog
160
+ lines.push('---', '', '## Backlog', '')
161
+ if (backlog.length > 0) {
162
+ backlog.forEach(task => {
163
+ lines.push(formatTask(task, 0, false))
164
+ })
165
+ } else {
166
+ lines.push('_No backlog items_')
167
+ }
168
+
169
+ lines.push('')
170
+ lines.push('---', '')
171
+ lines.push(`_Updated: ${data.lastUpdated || new Date().toISOString().split('T')[0]}_`)
172
+
173
+ return lines.join('\n')
174
+ }
175
+
176
+ /**
177
+ * Format a single task as markdown
178
+ */
179
+ function formatTask(task: QueueTask, num: number, numbered: boolean): string {
180
+ const checkbox = task.completed ? '[x]' : '[ ]'
181
+ const prefix = numbered && num > 0 ? `${num}.` : '-'
182
+
183
+ let text = task.description
184
+
185
+ // Add emoji for type
186
+ if (task.type === 'bug') text = `🐛 ${text}`
187
+
188
+ // Add priority tag for high/critical
189
+ if (task.priority === 'high' || task.priority === 'critical') {
190
+ text = `[${task.priority.toUpperCase()}] ${text}`
191
+ }
192
+
193
+ // Add agent
194
+ if (task.agent) text = `${text} @${task.agent}`
195
+
196
+ // Add origin feature
197
+ if (task.originFeature) text = `${text} (from: ${task.originFeature})`
198
+
199
+ // Add checkmark for completed
200
+ if (task.completed) text = `${text} ✅`
201
+
202
+ return `${prefix} ${checkbox} ${text}`
203
+ }
204
+
205
+ /**
206
+ * Quick helpers
207
+ */
208
+ export function createEmptyQueueMd(): string {
209
+ return serializeQueue({ tasks: [], lastUpdated: new Date().toISOString().split('T')[0] })
210
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * State Serializer
3
+ *
4
+ * Parses and serializes now.md for current task state.
5
+ *
6
+ * MD Format (now.md):
7
+ * ```
8
+ * # NOW
9
+ *
10
+ * **Task description here**
11
+ *
12
+ * Started: 2025-12-10T10:00:00.000Z
13
+ * Session: sess_abc123
14
+ * Feature: feat_xyz789
15
+ * Agent: fe
16
+ * ```
17
+ */
18
+
19
+ import type { StateJson, CurrentTask, PreviousTask } from '../schemas/state'
20
+
21
+ /**
22
+ * Parse now.md content to StateJson
23
+ */
24
+ export function parseState(content: string): StateJson {
25
+ if (!content || !content.trim()) {
26
+ return { currentTask: null, lastUpdated: '' }
27
+ }
28
+
29
+ const lines = content.split('\n')
30
+ let currentTask: CurrentTask | null = null
31
+ let previousTask: PreviousTask | null = null
32
+
33
+ // Find task description (bold line after # NOW)
34
+ let description = ''
35
+ for (const line of lines) {
36
+ const boldMatch = line.match(/^\*\*(.+)\*\*$/)
37
+ if (boldMatch) {
38
+ description = boldMatch[1].trim()
39
+ break
40
+ }
41
+ }
42
+
43
+ if (!description || description.toLowerCase().includes('no active task')) {
44
+ return { currentTask: null, lastUpdated: '' }
45
+ }
46
+
47
+ // Extract metadata
48
+ const startedMatch = content.match(/Started:\s*(.+)/i)
49
+ const sessionMatch = content.match(/Session:\s*(.+)/i)
50
+ const featureMatch = content.match(/Feature:\s*(.+)/i)
51
+ const idMatch = content.match(/ID:\s*(.+)/i)
52
+ const agentMatch = content.match(/Agent:\s*(.+)/i)
53
+ const pausedMatch = content.match(/Paused:\s*(.+)/i)
54
+
55
+ const id = idMatch ? idMatch[1].trim() : `task_${Date.now()}`
56
+ const startedAt = startedMatch ? startedMatch[1].trim() : new Date().toISOString()
57
+ const sessionId = sessionMatch ? sessionMatch[1].trim() : `sess_${Date.now()}`
58
+
59
+ if (pausedMatch) {
60
+ // This is a paused task
61
+ previousTask = {
62
+ id,
63
+ description,
64
+ status: 'paused',
65
+ startedAt,
66
+ pausedAt: pausedMatch[1].trim()
67
+ }
68
+ } else {
69
+ // Active task
70
+ currentTask = {
71
+ id,
72
+ description,
73
+ startedAt,
74
+ sessionId
75
+ }
76
+
77
+ if (featureMatch) {
78
+ currentTask.featureId = featureMatch[1].trim()
79
+ }
80
+ }
81
+
82
+ return {
83
+ currentTask,
84
+ previousTask,
85
+ lastUpdated: startedAt
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Serialize StateJson to now.md format
91
+ */
92
+ export function serializeState(data: StateJson): string {
93
+ const lines: string[] = ['# NOW', '']
94
+
95
+ if (!data.currentTask && !data.previousTask) {
96
+ lines.push('_No active task_', '')
97
+ lines.push('Use `/p:now` to start working on something.')
98
+ return lines.join('\n')
99
+ }
100
+
101
+ const task = data.currentTask || data.previousTask
102
+
103
+ if (task) {
104
+ lines.push(`**${task.description}**`, '')
105
+
106
+ if ('pausedAt' in task && task.pausedAt) {
107
+ lines.push(`Started: ${task.startedAt}`)
108
+ lines.push(`Paused: ${task.pausedAt}`)
109
+ } else if (data.currentTask) {
110
+ lines.push(`Started: ${data.currentTask.startedAt}`)
111
+ lines.push(`Session: ${data.currentTask.sessionId}`)
112
+ if (data.currentTask.featureId) {
113
+ lines.push(`Feature: ${data.currentTask.featureId}`)
114
+ }
115
+ }
116
+ }
117
+
118
+ return lines.join('\n')
119
+ }
120
+
121
+ /**
122
+ * Quick helpers for common operations
123
+ */
124
+ export function createCurrentTaskMd(task: CurrentTask): string {
125
+ return serializeState({
126
+ currentTask: task,
127
+ lastUpdated: task.startedAt
128
+ })
129
+ }
130
+
131
+ export function createEmptyStateMd(): string {
132
+ return serializeState({
133
+ currentTask: null,
134
+ lastUpdated: ''
135
+ })
136
+ }
@@ -66,6 +66,17 @@ export async function writeFile(filePath: string, content: string): Promise<void
66
66
  await fs.writeFile(filePath, content, 'utf-8')
67
67
  }
68
68
 
69
+ /**
70
+ * Atomic write - writes to temp file then renames (prevents partial writes)
71
+ */
72
+ export async function atomicWrite(filePath: string, content: string): Promise<void> {
73
+ const dir = path.dirname(filePath)
74
+ await fs.mkdir(dir, { recursive: true })
75
+ const tempPath = `${filePath}.${Date.now()}.tmp`
76
+ await fs.writeFile(tempPath, content, 'utf-8')
77
+ await fs.rename(tempPath, filePath)
78
+ }
79
+
69
80
  /**
70
81
  * Append to text file
71
82
  */
@@ -245,6 +256,7 @@ export default {
245
256
  writeJson,
246
257
  readFile,
247
258
  writeFile,
259
+ atomicWrite,
248
260
  fileExists,
249
261
  ensureDir,
250
262
  deleteFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.12.2",
3
+ "version": "0.13.1",
4
4
  "description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {
@@ -1,7 +1,11 @@
1
1
  import { NextRequest, NextResponse } from 'next/server'
2
- import { loadUnifiedJsonData, hasJsonState } from '@/lib/json-loader'
3
2
  import { getProjectStats, getRawProjectFiles } from '@/lib/parse-prjct-files'
4
3
 
4
+ /**
5
+ * GET /api/projects/[id]/stats
6
+ *
7
+ * MD-First Architecture: Returns stats parsed from MD files.
8
+ */
5
9
  export async function GET(
6
10
  request: NextRequest,
7
11
  { params }: { params: Promise<{ id: string }> }
@@ -16,33 +20,6 @@ export async function GET(
16
20
  }
17
21
 
18
22
  try {
19
- // Check if JSON files exist (new format)
20
- const hasJson = await hasJsonState(projectId)
21
-
22
- if (hasJson) {
23
- // Use new JSON loader (fast path)
24
- const jsonData = await loadUnifiedJsonData(projectId)
25
-
26
- // Convert to stats-compatible format using new architecture
27
- return NextResponse.json({
28
- success: true,
29
- version: 'v2',
30
- data: {
31
- currentTask: jsonData.state?.currentTask || null,
32
- queue: jsonData.queue?.tasks?.filter(t => !t.completed) || [],
33
- metrics: jsonData.metrics || null,
34
- agents: jsonData.agents,
35
- ideas: jsonData.ideas?.ideas || [],
36
- roadmap: jsonData.roadmap?.features || [],
37
- shipped: jsonData.shipped?.items || [],
38
- analysis: jsonData.analysis,
39
- outcomes: jsonData.outcomes,
40
- insights: jsonData.insights
41
- }
42
- })
43
- }
44
-
45
- // Fallback to legacy markdown parsing
46
23
  const [stats, raw] = await Promise.all([
47
24
  getProjectStats(projectId),
48
25
  getRawProjectFiles(projectId)
@@ -50,7 +27,7 @@ export async function GET(
50
27
 
51
28
  return NextResponse.json({
52
29
  success: true,
53
- version: 'v1-legacy',
30
+ version: 'md-first',
54
31
  data: stats,
55
32
  raw
56
33
  })
@@ -1,7 +1,6 @@
1
1
  import { getProjects } from '@/lib/services/projects.server'
2
2
  import { getGlobalStats } from '@/lib/services/stats.server'
3
3
  import { DashboardContent } from '@/components/DashboardContent'
4
- import { MigrationGate } from '@/components/MigrationGate'
5
4
 
6
5
  export default async function Dashboard() {
7
6
  const [projects, stats] = await Promise.all([
@@ -9,9 +8,5 @@ export default async function Dashboard() {
9
8
  getGlobalStats()
10
9
  ])
11
10
 
12
- return (
13
- <MigrationGate>
14
- <DashboardContent projects={projects} stats={stats} />
15
- </MigrationGate>
16
- )
11
+ return <DashboardContent projects={projects} stats={stats} />
17
12
  }
@@ -44,7 +44,8 @@ import {
44
44
  Undo2,
45
45
  Redo2,
46
46
  Command,
47
- X
47
+ X,
48
+ RefreshCw
48
49
  } from 'lucide-react'
49
50
  import { cn } from '@/lib/utils'
50
51
 
@@ -111,6 +112,15 @@ function CommandSidebarContent({
111
112
  onCommandClick?.()
112
113
  }}
113
114
  />
115
+ {/* Sync button - prominent, always visible */}
116
+ <CommandButton
117
+ cmd="p. sync"
118
+ icon={RefreshCw}
119
+ tip="Sync"
120
+ disabled={!isActiveConnected}
121
+ onClick={() => handleCommand('p. sync')}
122
+ variant="primary"
123
+ />
114
124
  <div className="border-b border-border w-8 my-2 mx-auto" />
115
125
 
116
126
  {COMMAND_GROUPS.map((group, groupIndex) => (
@@ -268,6 +278,29 @@ function ProjectPageContent({ projectId, project }: { projectId: string; project
268
278
  <span className="text-xs text-muted-foreground">Stats</span>
269
279
  </button>
270
280
 
281
+ {/* Sync button - prominent */}
282
+ <button
283
+ onClick={() => {
284
+ sendCommandToActive('p. sync')
285
+ setCommandSheetOpen(false)
286
+ }}
287
+ disabled={!isActiveConnected}
288
+ className={cn(
289
+ "flex flex-col items-center gap-1.5 p-3 rounded-lg transition-colors",
290
+ isActiveConnected
291
+ ? "hover:bg-primary/10"
292
+ : "opacity-50 cursor-not-allowed"
293
+ )}
294
+ >
295
+ <div className={cn(
296
+ "h-10 w-10 rounded-full flex items-center justify-center",
297
+ isActiveConnected ? "bg-primary text-primary-foreground" : "bg-muted"
298
+ )}>
299
+ <RefreshCw className="h-5 w-5" />
300
+ </div>
301
+ <span className="text-xs text-primary font-medium">Sync</span>
302
+ </button>
303
+
271
304
  {WORKFLOW_COMMANDS.map(({ cmd, icon: Icon, tip }) => (
272
305
  <button
273
306
  key={cmd}
@@ -21,6 +21,7 @@ import { ShipsCard } from '@/components/ShipsCard'
21
21
  import { IdeasCard } from '@/components/IdeasCard'
22
22
  import { AgentsCard } from '@/components/AgentsCard'
23
23
  import { RoadmapCard } from '@/components/RoadmapCard'
24
+ import { BlockersCard } from '@/components/BlockersCard'
24
25
  import { ActivityTimeline } from '@/components/ActivityTimeline'
25
26
 
26
27
  // Types for normalized component data
@@ -118,7 +119,7 @@ function normalizeShipped(stats: StatsResult): NormalizedShip[] {
118
119
  const items = stats.shipped?.items ?? []
119
120
  return items.map(s => ({
120
121
  name: s.name,
121
- date: s.shippedAt,
122
+ date: s.shippedAt || s.date || new Date().toISOString(),
122
123
  }))
123
124
  }
124
125
 
@@ -128,7 +129,7 @@ function normalizeIdeas(stats: StatsResult): NormalizedIdea[] {
128
129
  .filter(i => i.status === 'pending')
129
130
  .map(i => ({
130
131
  title: i.text,
131
- impact: i.priority.toUpperCase()
132
+ impact: i.priority?.toUpperCase() || 'MEDIUM'
132
133
  }))
133
134
  }
134
135
 
@@ -144,10 +145,10 @@ function normalizeAgents(stats: StatsResult): NormalizedAgent[] {
144
145
 
145
146
  function normalizeTimeline(stats: StatsResult): TimelineEvent[] {
146
147
  if (stats.metrics?.recentActivity?.length) {
147
- return stats.metrics.recentActivity.map((a: { timestamp: string; description: string; action?: string }) => ({
148
+ return stats.metrics.recentActivity.map(a => ({
148
149
  ts: a.timestamp,
149
- type: a.action || 'task_completed',
150
- task: a.description,
150
+ type: a.action || a.type || 'task_completed',
151
+ task: a.description || '',
151
152
  }))
152
153
  }
153
154
  return stats.legacyStats?.timeline ?? []
@@ -204,6 +205,9 @@ export default async function ProjectStatsPage({ params }: PageProps) {
204
205
  const totalShips = getTotalShips(stats)
205
206
  const tasksCompleted = getTasksCompleted(stats)
206
207
 
208
+ // Extract insights
209
+ const { estimateAccuracy, blockers } = stats.insights
210
+
207
211
  return (
208
212
  <div className="flex h-full flex-col p-4 md:p-8 overflow-auto">
209
213
  {/* Mobile: Add padding for hamburger menu */}
@@ -227,11 +231,13 @@ export default async function ProjectStatsPage({ params }: PageProps) {
227
231
  tasksPerDay={velocity}
228
232
  weeklyData={weeklyVelocityData}
229
233
  change={velocityChange}
234
+ estimateAccuracy={estimateAccuracy}
230
235
  />
231
236
  <RoadmapCard roadmap={roadmap} />
232
237
  <StreakCard streak={streak} />
233
238
  <QueueCard queue={queue} />
234
239
  <ShipsCard ships={shipped} totalShips={totalShips} />
240
+ <BlockersCard blockers={blockers} />
235
241
  <IdeasCard ideas={ideas} />
236
242
  <AgentsCard agents={agents} />
237
243
  </BentoGrid>