prjct-cli 0.13.3 → 0.15.0

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 (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. package/templates/commands/workflow.md +0 -12
@@ -8,6 +8,7 @@
8
8
  import path from 'path'
9
9
  import * as fileHelper from '../utils/file-helper'
10
10
  import pathManager from '../infrastructure/path-manager'
11
+ import { generateUUID } from '../schemas'
11
12
  import type { Outcome, OutcomeInput, OutcomeFilter } from './types'
12
13
 
13
14
  const OUTCOMES_DIR = 'outcomes'
@@ -38,7 +39,7 @@ export class OutcomeRecorder {
38
39
  async record(projectId: string, input: OutcomeInput): Promise<Outcome> {
39
40
  const outcome: Outcome = {
40
41
  ...input,
41
- id: `outcome_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
42
+ id: generateUUID(),
42
43
  }
43
44
 
44
45
  const outcomesPath = this.getOutcomesPath(projectId)
@@ -167,8 +167,9 @@ class PluginLoader {
167
167
  // Check if file exists
168
168
  await fs.access(pluginPath)
169
169
 
170
- // Require the plugin
171
- const plugin: Plugin = require(pluginPath)
170
+ // Import the plugin dynamically
171
+ const pluginModule = await import(pluginPath)
172
+ const plugin: Plugin = pluginModule.default || pluginModule
172
173
 
173
174
  // Validate plugin structure
174
175
  if (!plugin.name) {
@@ -259,11 +260,7 @@ class PluginLoader {
259
260
  // Remove from loaded plugins
260
261
  this.plugins.delete(name)
261
262
 
262
- // Clear require cache
263
- const pluginPath = this.pluginPaths.get(name)
264
- if (pluginPath) {
265
- delete require.cache[require.resolve(pluginPath)]
266
- }
263
+ // Clear plugin path reference
267
264
  this.pluginPaths.delete(name)
268
265
  }
269
266
 
@@ -122,9 +122,9 @@ class PluginRegistry {
122
122
  }
123
123
  }
124
124
 
125
- // Fallback: try to require (might have side effects)
126
- const plugin = require(pluginPath)
127
- delete require.cache[require.resolve(pluginPath)]
125
+ // Fallback: try to import (might have side effects)
126
+ const pluginModule = await import(pluginPath)
127
+ const plugin = pluginModule.default || pluginModule
128
128
 
129
129
  if (plugin.name) {
130
130
  return {
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * Schemas Module
3
3
  *
4
- * TypeScript types and defaults for all JSON data files.
5
- * These schemas define the source-of-truth for all project data.
4
+ * TypeScript types and defaults for all project data.
5
+ * MD-First Architecture: Markdown files are the source of truth.
6
6
  *
7
- * Data lives in: ~/.prjct-cli/projects/{projectId}/data/
8
- * Views are generated in: ~/.prjct-cli/projects/{projectId}/views/
7
+ * Structure: ~/.prjct-cli/projects/{projectId}/
8
+ * - core/ (now.md, next.md, context.md)
9
+ * - progress/ (shipped.md, metrics.md)
10
+ * - planning/ (ideas.md, roadmap.md, tasks/)
11
+ * - analysis/ (repo-summary.md)
12
+ * - memory/ (context.jsonl, patterns.json)
9
13
  */
10
14
 
11
15
  // State (current task + queue)
@@ -33,23 +37,25 @@ export * from './analysis'
33
37
  export * from './outcomes'
34
38
 
35
39
  // ============================================
36
- // ID GENERATORS
40
+ // ID GENERATORS - UUID ONLY
37
41
  // ============================================
38
42
 
39
- function generateId(prefix: string): string {
40
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
41
- let id = ''
42
- for (let i = 0; i < 8; i++) {
43
- id += chars[Math.floor(Math.random() * chars.length)]
44
- }
45
- return `${prefix}_${id}`
43
+ import crypto from 'crypto'
44
+
45
+ /**
46
+ * Generate a standard UUID.
47
+ * All IDs in the system use this format for PostgreSQL consistency.
48
+ */
49
+ export function generateUUID(): string {
50
+ return crypto.randomUUID()
46
51
  }
47
52
 
48
- export const generateTaskId = (): string => generateId('task')
49
- export const generateFeatureId = (): string => generateId('feat')
50
- export const generateIdeaId = (): string => generateId('idea')
51
- export const generateShipId = (): string => generateId('ship')
52
- export const generateSessionId = (): string => generateId('sess')
53
+ // All use the same UUID generator
54
+ export const generateTaskId = generateUUID
55
+ export const generateFeatureId = generateUUID
56
+ export const generateIdeaId = generateUUID
57
+ export const generateShipId = generateUUID
58
+ export const generateSessionId = generateUUID
53
59
 
54
60
  // ============================================
55
61
  // PATH HELPERS
@@ -63,11 +69,3 @@ export const GLOBAL_STORAGE = join(homedir(), '.prjct-cli', 'projects')
63
69
  export function getProjectPath(projectId: string): string {
64
70
  return join(GLOBAL_STORAGE, projectId)
65
71
  }
66
-
67
- export function getDataPath(projectId: string): string {
68
- return join(GLOBAL_STORAGE, projectId, 'data')
69
- }
70
-
71
- export function getViewsPath(projectId: string): string {
72
- return join(GLOBAL_STORAGE, projectId, 'views')
73
- }
@@ -25,6 +25,7 @@ export interface PreviousTask {
25
25
  status: 'paused'
26
26
  startedAt: string // ISO8601
27
27
  pausedAt: string // ISO8601
28
+ pauseReason?: string // Optional reason for pausing
28
29
  }
29
30
 
30
31
  // StateJson is the wrapper for state.json file
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Ideas Serializer
3
+ *
4
+ * Parses and serializes ideas.md for idea backlog.
5
+ *
6
+ * MD Format (ideas.md):
7
+ * ```
8
+ * # IDEAS 💡
9
+ *
10
+ * ## Brain Dump
11
+ *
12
+ * - Add dark mode _(2025-12-10)_ #ui #enhancement
13
+ * - Improve caching _(2025-12-09)_ #performance
14
+ *
15
+ * ## Converted
16
+ *
17
+ * - ✓ Add user auth → feat_123 _(2025-12-08)_
18
+ * ```
19
+ */
20
+
21
+ export type IdeaStatus = 'pending' | 'converted' | 'archived'
22
+ export type IdeaPriority = 'low' | 'medium' | 'high'
23
+
24
+ export interface Idea {
25
+ id: string
26
+ text: string
27
+ status: IdeaStatus
28
+ priority: IdeaPriority
29
+ tags: string[]
30
+ addedAt: string // ISO8601
31
+ convertedTo?: string // featureId if converted
32
+ }
33
+
34
+ /**
35
+ * Parse ideas.md content to Idea[]
36
+ */
37
+ export function parseIdeas(content: string): Idea[] {
38
+ if (!content || !content.trim()) {
39
+ return []
40
+ }
41
+
42
+ const ideas: Idea[] = []
43
+
44
+ // Split by sections
45
+ const sections = content.split(/\n## /).slice(1)
46
+
47
+ for (const section of sections) {
48
+ const lines = section.trim().split('\n')
49
+ const sectionName = lines[0]?.trim().toLowerCase() || ''
50
+
51
+ // Determine status based on section
52
+ let status: IdeaStatus = 'pending'
53
+ if (sectionName.includes('converted')) {
54
+ status = 'converted'
55
+ } else if (sectionName.includes('archived')) {
56
+ status = 'archived'
57
+ }
58
+
59
+ // Parse list items
60
+ for (let i = 1; i < lines.length; i++) {
61
+ const line = lines[i].trim()
62
+ if (!line.startsWith('-')) continue
63
+
64
+ const itemText = line.slice(1).trim()
65
+ if (!itemText) continue
66
+
67
+ // Extract date: _(date)_
68
+ const dateMatch = itemText.match(/_\((.+?)\)_/)
69
+ const addedAt = dateMatch ? dateMatch[1].trim() : new Date().toISOString()
70
+
71
+ // Extract tags: #tag1 #tag2
72
+ const tags = (itemText.match(/#(\w+)/g) || []).map(t => t.slice(1))
73
+
74
+ // Extract converted feature id: → feat_123
75
+ const convertedMatch = itemText.match(/→\s*(\w+)/)
76
+ const convertedTo = convertedMatch ? convertedMatch[1] : undefined
77
+
78
+ // Clean text: remove date, tags, and conversion marker
79
+ let text = itemText
80
+ .replace(/_\(.+?\)_/g, '')
81
+ .replace(/#\w+/g, '')
82
+ .replace(/→\s*\w+/g, '')
83
+ .replace(/^✓\s*/, '')
84
+ .trim()
85
+
86
+ // Detect priority from keywords
87
+ let priority: IdeaPriority = 'medium'
88
+ if (text.toLowerCase().includes('urgent') || text.toLowerCase().includes('critical')) {
89
+ priority = 'high'
90
+ } else if (text.toLowerCase().includes('nice to have') || text.toLowerCase().includes('maybe')) {
91
+ priority = 'low'
92
+ }
93
+
94
+ ideas.push({
95
+ id: `idea_${Date.parse(addedAt) || Date.now()}_${i}`,
96
+ text,
97
+ status,
98
+ priority,
99
+ tags,
100
+ addedAt,
101
+ convertedTo
102
+ })
103
+ }
104
+ }
105
+
106
+ return ideas
107
+ }
108
+
109
+ /**
110
+ * Serialize Idea[] to ideas.md format
111
+ */
112
+ export function serializeIdeas(ideas: Idea[]): string {
113
+ const lines: string[] = ['# IDEAS 💡', '']
114
+
115
+ const pending = ideas.filter(i => i.status === 'pending')
116
+ const converted = ideas.filter(i => i.status === 'converted')
117
+ const archived = ideas.filter(i => i.status === 'archived')
118
+
119
+ // Brain Dump section (pending ideas)
120
+ lines.push('## Brain Dump', '')
121
+
122
+ if (pending.length === 0) {
123
+ lines.push('_No pending ideas. Use `/p:idea` to capture one._', '')
124
+ } else {
125
+ // Sort by addedAt descending (newest first)
126
+ const sorted = [...pending].sort(
127
+ (a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime()
128
+ )
129
+
130
+ for (const idea of sorted) {
131
+ const tagsStr = idea.tags.length > 0 ? ' ' + idea.tags.map(t => `#${t}`).join(' ') : ''
132
+ const dateStr = formatDate(idea.addedAt)
133
+ lines.push(`- ${idea.text} _(${dateStr})_${tagsStr}`)
134
+ }
135
+ lines.push('')
136
+ }
137
+
138
+ // Converted section
139
+ if (converted.length > 0) {
140
+ lines.push('## Converted', '')
141
+ const sorted = [...converted].sort(
142
+ (a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime()
143
+ )
144
+
145
+ for (const idea of sorted) {
146
+ const convStr = idea.convertedTo ? ` → ${idea.convertedTo}` : ''
147
+ const dateStr = formatDate(idea.addedAt)
148
+ lines.push(`- ✓ ${idea.text}${convStr} _(${dateStr})_`)
149
+ }
150
+ lines.push('')
151
+ }
152
+
153
+ // Archived section
154
+ if (archived.length > 0) {
155
+ lines.push('## Archived', '')
156
+ const sorted = [...archived].sort(
157
+ (a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime()
158
+ )
159
+
160
+ for (const idea of sorted) {
161
+ const dateStr = formatDate(idea.addedAt)
162
+ lines.push(`- ${idea.text} _(${dateStr})_`)
163
+ }
164
+ lines.push('')
165
+ }
166
+
167
+ return lines.join('\n')
168
+ }
169
+
170
+ /**
171
+ * Format date for display
172
+ */
173
+ function formatDate(isoDate: string): string {
174
+ try {
175
+ const date = new Date(isoDate)
176
+ return date.toISOString().split('T')[0] // YYYY-MM-DD
177
+ } catch {
178
+ return isoDate
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Create empty ideas.md
184
+ */
185
+ export function createEmptyIdeasMd(): string {
186
+ return serializeIdeas([])
187
+ }
@@ -18,3 +18,19 @@ export {
18
18
  serializeQueue,
19
19
  createEmptyQueueMd
20
20
  } from './queue-serializer'
21
+
22
+ // Shipped (shipped.md)
23
+ export {
24
+ parseShipped,
25
+ serializeShipped,
26
+ createEmptyShippedMd
27
+ } from './shipped-serializer'
28
+ export type { ShippedFeature } from './shipped-serializer'
29
+
30
+ // Ideas (ideas.md)
31
+ export {
32
+ parseIdeas,
33
+ serializeIdeas,
34
+ createEmptyIdeasMd
35
+ } from './ideas-serializer'
36
+ export type { Idea, IdeaStatus, IdeaPriority } from './ideas-serializer'
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shipped Serializer
3
+ *
4
+ * Parses and serializes shipped.md for shipped features.
5
+ *
6
+ * MD Format (shipped.md):
7
+ * ```
8
+ * # SHIPPED 🚀
9
+ *
10
+ * ## Feature Name
11
+ *
12
+ * Shipped: 2025-12-10T10:00:00.000Z
13
+ * Version: 0.1.5
14
+ *
15
+ * ## Another Feature
16
+ *
17
+ * Shipped: 2025-12-09T14:30:00.000Z
18
+ * Version: 0.1.4
19
+ * ```
20
+ */
21
+
22
+ export interface ShippedFeature {
23
+ id: string
24
+ name: string
25
+ shippedAt: string // ISO8601
26
+ version: string
27
+ description?: string
28
+ }
29
+
30
+ /**
31
+ * Parse shipped.md content to ShippedFeature[]
32
+ */
33
+ export function parseShipped(content: string): ShippedFeature[] {
34
+ if (!content || !content.trim()) {
35
+ return []
36
+ }
37
+
38
+ const features: ShippedFeature[] = []
39
+ const sections = content.split(/\n## /).slice(1) // Skip header
40
+
41
+ for (const section of sections) {
42
+ const lines = section.trim().split('\n')
43
+ const name = lines[0]?.trim()
44
+
45
+ if (!name) continue
46
+
47
+ const shippedMatch = section.match(/Shipped:\s*(.+)/i)
48
+ const versionMatch = section.match(/Version:\s*(.+)/i)
49
+
50
+ const shippedAt = shippedMatch ? shippedMatch[1].trim() : new Date().toISOString()
51
+ const version = versionMatch ? versionMatch[1].trim() : '0.0.0'
52
+
53
+ // Extract description (lines between name and metadata)
54
+ const descLines: string[] = []
55
+ for (let i = 1; i < lines.length; i++) {
56
+ const line = lines[i].trim()
57
+ if (line.startsWith('Shipped:') || line.startsWith('Version:') || !line) break
58
+ descLines.push(line)
59
+ }
60
+
61
+ features.push({
62
+ id: `ship_${Date.parse(shippedAt) || Date.now()}`,
63
+ name,
64
+ shippedAt,
65
+ version,
66
+ description: descLines.join(' ') || undefined
67
+ })
68
+ }
69
+
70
+ return features
71
+ }
72
+
73
+ /**
74
+ * Serialize ShippedFeature[] to shipped.md format
75
+ */
76
+ export function serializeShipped(features: ShippedFeature[]): string {
77
+ const lines: string[] = ['# SHIPPED 🚀', '']
78
+
79
+ if (features.length === 0) {
80
+ lines.push('_No features shipped yet._', '')
81
+ lines.push('Use `/p:ship` to celebrate your first ship!')
82
+ return lines.join('\n')
83
+ }
84
+
85
+ // Sort by shippedAt descending (newest first)
86
+ const sorted = [...features].sort(
87
+ (a, b) => new Date(b.shippedAt).getTime() - new Date(a.shippedAt).getTime()
88
+ )
89
+
90
+ for (const feature of sorted) {
91
+ lines.push(`## ${feature.name}`, '')
92
+ if (feature.description) {
93
+ lines.push(feature.description, '')
94
+ }
95
+ lines.push(`Shipped: ${feature.shippedAt}`)
96
+ lines.push(`Version: ${feature.version}`)
97
+ lines.push('')
98
+ }
99
+
100
+ return lines.join('\n')
101
+ }
102
+
103
+ /**
104
+ * Create empty shipped.md
105
+ */
106
+ export function createEmptyShippedMd(): string {
107
+ return serializeShipped([])
108
+ }
@@ -3,18 +3,12 @@
3
3
  */
4
4
 
5
5
  import type { Session, TimelineEvent } from './types'
6
+ import { generateUUID } from '../schemas'
6
7
 
7
8
  /**
8
- * Generate unique session ID
9
+ * Generate unique session ID (re-export from schemas)
9
10
  */
10
- export function generateId(): string {
11
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
12
- let id = 'sess_'
13
- for (let i = 0; i < 8; i++) {
14
- id += chars.charAt(Math.floor(Math.random() * chars.length))
15
- }
16
- return id
17
- }
11
+ export const generateId = generateUUID
18
12
 
19
13
  /**
20
14
  * Calculate total duration in seconds