prjct-cli 0.19.0 → 0.20.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 (230) hide show
  1. package/CHANGELOG.md +66 -6
  2. package/CLAUDE.md +56 -15
  3. package/README.md +5 -6
  4. package/bin/prjct +59 -42
  5. package/bin/prjct.ts +60 -0
  6. package/core/__tests__/agentic/memory-system.test.ts +18 -3
  7. package/core/__tests__/agentic/plan-mode.test.ts +55 -26
  8. package/core/__tests__/agentic/prompt-builder.test.ts +6 -6
  9. package/core/__tests__/utils/project-commands.test.ts +72 -0
  10. package/core/agentic/agent-router.ts +3 -12
  11. package/core/agentic/command-executor.ts +372 -3
  12. package/core/agentic/context-builder.ts +7 -27
  13. package/core/agentic/ground-truth.ts +604 -5
  14. package/core/agentic/index.ts +180 -0
  15. package/core/agentic/loop-detector.ts +418 -4
  16. package/core/agentic/memory-system.ts +857 -3
  17. package/core/agentic/plan-mode.ts +491 -4
  18. package/core/agentic/prompt-builder.ts +44 -65
  19. package/core/agentic/services.ts +13 -5
  20. package/core/agentic/skill-loader.ts +112 -0
  21. package/core/agentic/smart-context.ts +37 -122
  22. package/core/agentic/template-loader.ts +79 -122
  23. package/core/agentic/tool-registry.ts +5 -11
  24. package/core/agents/index.ts +1 -1
  25. package/core/agents/performance.ts +4 -2
  26. package/core/bus/bus.ts +262 -0
  27. package/core/bus/index.ts +3 -313
  28. package/core/commands/analysis.ts +5 -5
  29. package/core/commands/analytics.ts +11 -11
  30. package/core/commands/base.ts +33 -209
  31. package/core/commands/cleanup.ts +148 -0
  32. package/core/commands/command-data.ts +346 -0
  33. package/core/commands/commands.ts +216 -0
  34. package/core/commands/design.ts +83 -0
  35. package/core/commands/index.ts +13 -207
  36. package/core/commands/maintenance.ts +52 -473
  37. package/core/commands/planning.ts +3 -3
  38. package/core/commands/register.ts +104 -0
  39. package/core/commands/registry.ts +441 -0
  40. package/core/commands/setup.ts +25 -9
  41. package/core/commands/shipping.ts +48 -11
  42. package/core/commands/snapshots.ts +299 -0
  43. package/core/commands/workflow.ts +2 -2
  44. package/core/constants/index.ts +254 -4
  45. package/core/domain/agent-loader.ts +5 -6
  46. package/core/domain/task-stack.ts +555 -4
  47. package/core/errors.ts +127 -1
  48. package/core/events/events.ts +87 -0
  49. package/core/events/index.ts +4 -138
  50. package/core/index.ts +15 -23
  51. package/core/infrastructure/agent-detector.ts +126 -201
  52. package/core/infrastructure/author-detector.ts +99 -171
  53. package/core/infrastructure/command-installer.ts +476 -4
  54. package/core/infrastructure/config-manager.ts +41 -37
  55. package/core/infrastructure/path-manager.ts +59 -9
  56. package/core/infrastructure/permission-manager.ts +286 -0
  57. package/core/outcomes/analyzer.ts +7 -41
  58. package/core/outcomes/index.ts +1 -1
  59. package/core/outcomes/recorder.ts +1 -1
  60. package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
  61. package/core/plugin/loader.ts +5 -5
  62. package/core/plugin/registry.ts +2 -2
  63. package/core/schemas/ideas.ts +85 -54
  64. package/core/schemas/index.ts +14 -33
  65. package/core/schemas/permissions.ts +177 -0
  66. package/core/schemas/project.ts +39 -12
  67. package/core/schemas/roadmap.ts +94 -59
  68. package/core/schemas/schemas.ts +39 -0
  69. package/core/schemas/shipped.ts +87 -60
  70. package/core/schemas/state.ts +110 -70
  71. package/core/server/index.ts +21 -0
  72. package/core/server/routes.ts +165 -0
  73. package/core/server/server.ts +136 -0
  74. package/core/server/sse.ts +135 -0
  75. package/core/services/agent-service.ts +170 -0
  76. package/core/services/breakdown-service.ts +126 -0
  77. package/core/services/index.ts +21 -0
  78. package/core/services/memory-service.ts +108 -0
  79. package/core/services/project-service.ts +146 -0
  80. package/core/services/skill-service.ts +253 -0
  81. package/core/session/compaction.ts +257 -0
  82. package/core/session/index.ts +20 -8
  83. package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
  84. package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
  85. package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
  86. package/core/session/utils.ts +1 -1
  87. package/core/storage/ideas-storage.ts +10 -26
  88. package/core/storage/index.ts +14 -162
  89. package/core/storage/queue-storage.ts +13 -11
  90. package/core/storage/shipped-storage.ts +4 -17
  91. package/core/storage/state-storage.ts +35 -43
  92. package/core/storage/storage-manager.ts +42 -52
  93. package/core/storage/storage.ts +160 -0
  94. package/core/sync/auth-config.ts +1 -8
  95. package/core/sync/index.ts +17 -10
  96. package/core/sync/oauth-handler.ts +1 -6
  97. package/core/sync/sync-client.ts +6 -34
  98. package/core/sync/sync-manager.ts +11 -40
  99. package/core/types/agentic.ts +577 -0
  100. package/core/types/agents.ts +145 -0
  101. package/core/types/bus.ts +82 -0
  102. package/core/types/commands.ts +366 -0
  103. package/core/types/config.ts +66 -0
  104. package/core/types/core.ts +96 -0
  105. package/core/types/domain.ts +71 -0
  106. package/core/types/events.ts +42 -0
  107. package/core/types/fs.ts +56 -0
  108. package/core/types/index.ts +387 -500
  109. package/core/types/infrastructure.ts +196 -0
  110. package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
  111. package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
  112. package/core/types/plugin.ts +25 -0
  113. package/core/types/server.ts +54 -0
  114. package/core/types/services.ts +65 -0
  115. package/core/types/session.ts +135 -0
  116. package/core/types/storage.ts +148 -0
  117. package/core/types/sync.ts +121 -0
  118. package/core/types/task.ts +72 -0
  119. package/core/types/template.ts +24 -0
  120. package/core/types/utils.ts +90 -0
  121. package/core/utils/cache.ts +195 -0
  122. package/core/utils/collection-filters.ts +245 -0
  123. package/core/utils/date-helper.ts +1 -5
  124. package/core/utils/file-helper.ts +20 -10
  125. package/core/utils/jsonl-helper.ts +5 -8
  126. package/core/utils/markdown-builder.ts +277 -0
  127. package/core/utils/project-commands.ts +132 -0
  128. package/core/utils/runtime.ts +119 -0
  129. package/dist/bin/prjct.mjs +12568 -0
  130. package/package.json +13 -8
  131. package/scripts/build.js +106 -0
  132. package/scripts/postinstall.js +50 -8
  133. package/templates/agentic/agents/uxui.md +210 -0
  134. package/templates/agentic/subagent-generation.md +1 -1
  135. package/templates/commands/bug.md +219 -41
  136. package/templates/commands/feature.md +368 -80
  137. package/templates/commands/serve.md +118 -0
  138. package/templates/commands/ship.md +152 -14
  139. package/templates/commands/skill.md +110 -0
  140. package/templates/commands/sync.md +63 -4
  141. package/templates/commands/test.md +40 -188
  142. package/templates/mcp-config.json +0 -36
  143. package/templates/permissions/default.jsonc +60 -0
  144. package/templates/permissions/permissive.jsonc +49 -0
  145. package/templates/permissions/strict.jsonc +62 -0
  146. package/templates/skills/code-review.md +47 -0
  147. package/templates/skills/debug.md +61 -0
  148. package/templates/skills/refactor.md +47 -0
  149. package/templates/subagents/domain/devops.md +1 -1
  150. package/templates/subagents/domain/testing.md +6 -10
  151. package/templates/subagents/workflow/prjct-shipper.md +16 -7
  152. package/templates/tools/bash.txt +22 -0
  153. package/templates/tools/edit.txt +18 -0
  154. package/templates/tools/glob.txt +19 -0
  155. package/templates/tools/grep.txt +21 -0
  156. package/templates/tools/read.txt +14 -0
  157. package/templates/tools/task.txt +20 -0
  158. package/templates/tools/webfetch.txt +16 -0
  159. package/templates/tools/websearch.txt +18 -0
  160. package/templates/tools/write.txt +17 -0
  161. package/core/agentic/command-executor/command-executor.ts +0 -312
  162. package/core/agentic/command-executor/index.ts +0 -16
  163. package/core/agentic/command-executor/status-signal.ts +0 -38
  164. package/core/agentic/command-executor/types.ts +0 -79
  165. package/core/agentic/ground-truth/index.ts +0 -76
  166. package/core/agentic/ground-truth/types.ts +0 -33
  167. package/core/agentic/ground-truth/utils.ts +0 -48
  168. package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
  169. package/core/agentic/ground-truth/verifiers/done.ts +0 -75
  170. package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
  171. package/core/agentic/ground-truth/verifiers/index.ts +0 -37
  172. package/core/agentic/ground-truth/verifiers/init.ts +0 -52
  173. package/core/agentic/ground-truth/verifiers/now.ts +0 -57
  174. package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
  175. package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
  176. package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
  177. package/core/agentic/ground-truth/verifiers.ts +0 -6
  178. package/core/agentic/loop-detector/error-analysis.ts +0 -97
  179. package/core/agentic/loop-detector/hallucination.ts +0 -71
  180. package/core/agentic/loop-detector/index.ts +0 -41
  181. package/core/agentic/loop-detector/loop-detector.ts +0 -222
  182. package/core/agentic/loop-detector/types.ts +0 -66
  183. package/core/agentic/memory-system/history.ts +0 -53
  184. package/core/agentic/memory-system/index.ts +0 -192
  185. package/core/agentic/memory-system/patterns.ts +0 -156
  186. package/core/agentic/memory-system/semantic-memories.ts +0 -278
  187. package/core/agentic/memory-system/session.ts +0 -21
  188. package/core/agentic/plan-mode/approval.ts +0 -57
  189. package/core/agentic/plan-mode/constants.ts +0 -44
  190. package/core/agentic/plan-mode/index.ts +0 -28
  191. package/core/agentic/plan-mode/plan-mode.ts +0 -407
  192. package/core/agentic/plan-mode/types.ts +0 -193
  193. package/core/agents/types.ts +0 -126
  194. package/core/command-registry/categories.ts +0 -23
  195. package/core/command-registry/commands.ts +0 -15
  196. package/core/command-registry/core-commands.ts +0 -344
  197. package/core/command-registry/index.ts +0 -158
  198. package/core/command-registry/optional-commands.ts +0 -163
  199. package/core/command-registry/setup-commands.ts +0 -83
  200. package/core/command-registry/types.ts +0 -59
  201. package/core/command-registry.ts +0 -9
  202. package/core/commands/types.ts +0 -185
  203. package/core/commands.ts +0 -11
  204. package/core/constants/formats.ts +0 -187
  205. package/core/context-sync.ts +0 -18
  206. package/core/data/index.ts +0 -27
  207. package/core/data/md-base-manager.ts +0 -203
  208. package/core/data/md-ideas-manager.ts +0 -155
  209. package/core/data/md-queue-manager.ts +0 -180
  210. package/core/data/md-shipped-manager.ts +0 -90
  211. package/core/data/md-state-manager.ts +0 -137
  212. package/core/domain/task-stack/index.ts +0 -19
  213. package/core/domain/task-stack/parser.ts +0 -86
  214. package/core/domain/task-stack/storage.ts +0 -123
  215. package/core/domain/task-stack/task-stack.ts +0 -340
  216. package/core/domain/task-stack/types.ts +0 -51
  217. package/core/infrastructure/command-installer/command-installer.ts +0 -327
  218. package/core/infrastructure/command-installer/global-config.ts +0 -136
  219. package/core/infrastructure/command-installer/index.ts +0 -25
  220. package/core/infrastructure/command-installer/types.ts +0 -41
  221. package/core/infrastructure/session-manager/index.ts +0 -23
  222. package/core/infrastructure/session-manager/types.ts +0 -45
  223. package/core/infrastructure/session-manager.ts +0 -8
  224. package/core/serializers/ideas-serializer.ts +0 -187
  225. package/core/serializers/index.ts +0 -36
  226. package/core/serializers/queue-serializer.ts +0 -210
  227. package/core/serializers/shipped-serializer.ts +0 -108
  228. package/core/serializers/state-serializer.ts +0 -136
  229. package/core/session/types.ts +0 -29
  230. /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Collection Filter Utilities
3
+ *
4
+ * Reusable filtering and sorting functions for storage collections.
5
+ * Eliminates duplicated filter logic across storage classes.
6
+ */
7
+
8
+ import type { Priority, TaskSection } from '../schemas/state'
9
+
10
+ /**
11
+ * Priority order mapping for sorting (highest priority first)
12
+ */
13
+ export const PRIORITY_ORDER: Record<Priority, number> = {
14
+ critical: 0,
15
+ high: 1,
16
+ medium: 2,
17
+ low: 3,
18
+ }
19
+
20
+ /**
21
+ * Section order mapping for sorting
22
+ */
23
+ export const SECTION_ORDER: Record<TaskSection, number> = {
24
+ active: 0,
25
+ previously_active: 1,
26
+ backlog: 2,
27
+ }
28
+
29
+ /**
30
+ * Filter items by a specific field value
31
+ */
32
+ export function filterByField<T, K extends keyof T>(
33
+ items: T[],
34
+ field: K,
35
+ value: T[K]
36
+ ): T[] {
37
+ return items.filter((item) => item[field] === value)
38
+ }
39
+
40
+ /**
41
+ * Filter items by multiple field values (OR logic)
42
+ */
43
+ export function filterByFieldIn<T, K extends keyof T>(
44
+ items: T[],
45
+ field: K,
46
+ values: T[K][]
47
+ ): T[] {
48
+ return items.filter((item) => values.includes(item[field]))
49
+ }
50
+
51
+ /**
52
+ * Filter items excluding a specific field value
53
+ */
54
+ export function filterByFieldNot<T, K extends keyof T>(
55
+ items: T[],
56
+ field: K,
57
+ value: T[K]
58
+ ): T[] {
59
+ return items.filter((item) => item[field] !== value)
60
+ }
61
+
62
+ /**
63
+ * Combined filter: field equals value AND another field is falsy
64
+ * Useful for filtering active items that aren't completed
65
+ */
66
+ export function filterActiveByField<T, K extends keyof T>(
67
+ items: T[],
68
+ field: K,
69
+ value: T[K],
70
+ activeField: keyof T
71
+ ): T[] {
72
+ return items.filter((item) => item[field] === value && !item[activeField])
73
+ }
74
+
75
+ /**
76
+ * Filter items where a field is truthy
77
+ */
78
+ export function filterByTruthy<T, K extends keyof T>(items: T[], field: K): T[] {
79
+ return items.filter((item) => Boolean(item[field]))
80
+ }
81
+
82
+ /**
83
+ * Filter items where a field is falsy
84
+ */
85
+ export function filterByFalsy<T, K extends keyof T>(items: T[], field: K): T[] {
86
+ return items.filter((item) => !item[field])
87
+ }
88
+
89
+ /**
90
+ * Sort items by priority (highest first)
91
+ */
92
+ export function sortByPriority<T extends { priority: Priority }>(items: T[]): T[] {
93
+ return [...items].sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority])
94
+ }
95
+
96
+ /**
97
+ * Sort items by section, then priority
98
+ */
99
+ export function sortBySectionAndPriority<T extends { section: TaskSection; priority: Priority }>(
100
+ items: T[]
101
+ ): T[] {
102
+ return [...items].sort((a, b) => {
103
+ const sectionDiff = SECTION_ORDER[a.section] - SECTION_ORDER[b.section]
104
+ if (sectionDiff !== 0) return sectionDiff
105
+ return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Sort items by date field
111
+ */
112
+ export function sortByDate<T>(
113
+ items: T[],
114
+ dateField: keyof T,
115
+ direction: 'asc' | 'desc' = 'desc'
116
+ ): T[] {
117
+ return [...items].sort((a, b) => {
118
+ const dateA = new Date(a[dateField] as string).getTime()
119
+ const dateB = new Date(b[dateField] as string).getTime()
120
+ return direction === 'desc' ? dateB - dateA : dateA - dateB
121
+ })
122
+ }
123
+
124
+ /**
125
+ * Filter items by date range
126
+ */
127
+ export function filterByDateRange<T>(
128
+ items: T[],
129
+ dateField: keyof T,
130
+ startDate: Date,
131
+ endDate: Date
132
+ ): T[] {
133
+ return items.filter((item) => {
134
+ const date = new Date(item[dateField] as string)
135
+ return date >= startDate && date <= endDate
136
+ })
137
+ }
138
+
139
+ /**
140
+ * Filter items from the last N days
141
+ */
142
+ export function filterByLastDays<T>(items: T[], dateField: keyof T, days: number): T[] {
143
+ const startDate = new Date()
144
+ startDate.setDate(startDate.getDate() - days)
145
+ return filterByDateRange(items, dateField, startDate, new Date())
146
+ }
147
+
148
+ /**
149
+ * Group items by a field value
150
+ */
151
+ export function groupByField<T, K extends keyof T>(items: T[], field: K): Map<T[K], T[]> {
152
+ const groups = new Map<T[K], T[]>()
153
+
154
+ for (const item of items) {
155
+ const key = item[field]
156
+ if (!groups.has(key)) {
157
+ groups.set(key, [])
158
+ }
159
+ groups.get(key)!.push(item)
160
+ }
161
+
162
+ return groups
163
+ }
164
+
165
+ /**
166
+ * Count items by field value
167
+ */
168
+ export function countByField<T, K extends keyof T>(items: T[], field: K): Map<T[K], number> {
169
+ const counts = new Map<T[K], number>()
170
+
171
+ for (const item of items) {
172
+ const key = item[field]
173
+ counts.set(key, (counts.get(key) || 0) + 1)
174
+ }
175
+
176
+ return counts
177
+ }
178
+
179
+ /**
180
+ * Get the first N items
181
+ */
182
+ export function take<T>(items: T[], count: number): T[] {
183
+ return items.slice(0, count)
184
+ }
185
+
186
+ /**
187
+ * Get the last N items
188
+ */
189
+ export function takeLast<T>(items: T[], count: number): T[] {
190
+ return items.slice(-count)
191
+ }
192
+
193
+ /**
194
+ * Find item by field value
195
+ */
196
+ export function findByField<T, K extends keyof T>(
197
+ items: T[],
198
+ field: K,
199
+ value: T[K]
200
+ ): T | undefined {
201
+ return items.find((item) => item[field] === value)
202
+ }
203
+
204
+ /**
205
+ * Check if any item matches field value
206
+ */
207
+ export function anyByField<T, K extends keyof T>(items: T[], field: K, value: T[K]): boolean {
208
+ return items.some((item) => item[field] === value)
209
+ }
210
+
211
+ /**
212
+ * Remove duplicates by field
213
+ */
214
+ export function uniqueByField<T, K extends keyof T>(items: T[], field: K): T[] {
215
+ const seen = new Set<T[K]>()
216
+ return items.filter((item) => {
217
+ if (seen.has(item[field])) return false
218
+ seen.add(item[field])
219
+ return true
220
+ })
221
+ }
222
+
223
+ // Default export for CommonJS compatibility
224
+ export default {
225
+ PRIORITY_ORDER,
226
+ SECTION_ORDER,
227
+ filterByField,
228
+ filterByFieldIn,
229
+ filterByFieldNot,
230
+ filterActiveByField,
231
+ filterByTruthy,
232
+ filterByFalsy,
233
+ sortByPriority,
234
+ sortBySectionAndPriority,
235
+ sortByDate,
236
+ filterByDateRange,
237
+ filterByLastDays,
238
+ groupByField,
239
+ countByField,
240
+ take,
241
+ takeLast,
242
+ findByField,
243
+ anyByField,
244
+ uniqueByField,
245
+ }
@@ -7,11 +7,7 @@
7
7
  * - commands.ts (38+ inline date operations)
8
8
  */
9
9
 
10
- export interface DateComponents {
11
- year: string
12
- month: string
13
- day: string
14
- }
10
+ import type { DateComponents } from '../types'
15
11
 
16
12
  /**
17
13
  * Format a date to YYYY-MM-DD format
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs/promises'
2
2
  import path from 'path'
3
+ import { type NodeError, isNotFoundError } from '../types/fs'
3
4
 
4
5
  /**
5
6
  * File Helper - Centralized file operations with error handling
@@ -16,10 +17,6 @@ interface ListFilesOptions {
16
17
  extension?: string
17
18
  }
18
19
 
19
- interface NodeError extends Error {
20
- code?: string
21
- }
22
-
23
20
  /**
24
21
  * Read JSON file and parse
25
22
  */
@@ -28,7 +25,7 @@ export async function readJson<T = unknown>(filePath: string, defaultValue: T |
28
25
  const content = await fs.readFile(filePath, 'utf-8')
29
26
  return JSON.parse(content) as T
30
27
  } catch (error) {
31
- if ((error as NodeError).code === 'ENOENT') {
28
+ if (isNotFoundError(error)) {
32
29
  return defaultValue
33
30
  }
34
31
  throw error
@@ -50,7 +47,7 @@ export async function readFile(filePath: string, defaultValue = ''): Promise<str
50
47
  try {
51
48
  return await fs.readFile(filePath, 'utf-8')
52
49
  } catch (error) {
53
- if ((error as NodeError).code === 'ENOENT') {
50
+ if (isNotFoundError(error)) {
54
51
  return defaultValue
55
52
  }
56
53
  throw error
@@ -84,6 +81,15 @@ export async function appendToFile(filePath: string, content: string): Promise<v
84
81
  await fs.appendFile(filePath, content, 'utf-8')
85
82
  }
86
83
 
84
+ /**
85
+ * Append a single line to file (with newline and directory creation)
86
+ */
87
+ export async function appendLine(filePath: string, line: string): Promise<void> {
88
+ const dir = path.dirname(filePath)
89
+ await fs.mkdir(dir, { recursive: true })
90
+ await fs.appendFile(filePath, line + '\n', 'utf-8')
91
+ }
92
+
87
93
  /**
88
94
  * Prepend to text file (adds content at beginning)
89
95
  */
@@ -92,7 +98,7 @@ export async function prependToFile(filePath: string, content: string): Promise<
92
98
  const existing = await fs.readFile(filePath, 'utf-8')
93
99
  await fs.writeFile(filePath, content + existing, 'utf-8')
94
100
  } catch (error) {
95
- if ((error as NodeError).code === 'ENOENT') {
101
+ if (isNotFoundError(error)) {
96
102
  await fs.writeFile(filePath, content, 'utf-8')
97
103
  } else {
98
104
  throw error
@@ -139,7 +145,7 @@ export async function deleteFile(filePath: string): Promise<boolean> {
139
145
  await fs.unlink(filePath)
140
146
  return true
141
147
  } catch (error) {
142
- if ((error as NodeError).code === 'ENOENT') {
148
+ if (isNotFoundError(error)) {
143
149
  return false // File didn't exist
144
150
  }
145
151
  throw error
@@ -154,7 +160,7 @@ export async function deleteDir(dirPath: string): Promise<boolean> {
154
160
  await fs.rm(dirPath, { recursive: true, force: true })
155
161
  return true
156
162
  } catch (error) {
157
- if ((error as NodeError).code === 'ENOENT') {
163
+ if (isNotFoundError(error)) {
158
164
  return false
159
165
  }
160
166
  throw error
@@ -183,7 +189,7 @@ export async function listFiles(dirPath: string, options: ListFilesOptions = {})
183
189
 
184
190
  return files.map((entry) => entry.name)
185
191
  } catch (error) {
186
- if ((error as NodeError).code === 'ENOENT') {
192
+ if (isNotFoundError(error)) {
187
193
  return []
188
194
  }
189
195
  throw error
@@ -257,7 +263,11 @@ export default {
257
263
  readFile,
258
264
  writeFile,
259
265
  atomicWrite,
266
+ appendToFile,
267
+ appendLine,
268
+ prependToFile,
260
269
  fileExists,
270
+ dirExists,
261
271
  ensureDir,
262
272
  deleteFile,
263
273
  deleteDir,
@@ -2,6 +2,7 @@ import fs from 'fs/promises'
2
2
  import fsSync from 'fs'
3
3
  import readline from 'readline'
4
4
  import path from 'path'
5
+ import { isNotFoundError } from '../types/fs'
5
6
 
6
7
  /**
7
8
  * JSONL Helper - Centralized JSONL parsing and writing
@@ -17,10 +18,6 @@ import path from 'path'
17
18
  * {"ts":"2025-10-04T15:00:00Z","type":"task_start","task":"JWT"}
18
19
  */
19
20
 
20
- interface NodeError extends Error {
21
- code?: string
22
- }
23
-
24
21
  interface FileSizeWarning {
25
22
  sizeMB: number
26
23
  isLarge: boolean
@@ -60,7 +57,7 @@ export async function readJsonLines<T = Record<string, unknown>>(filePath: strin
60
57
  const content = await fs.readFile(filePath, 'utf-8')
61
58
  return parseJsonLines<T>(content)
62
59
  } catch (error) {
63
- if ((error as NodeError).code === 'ENOENT') {
60
+ if (isNotFoundError(error)) {
64
61
  return [] // File doesn't exist, return empty array
65
62
  }
66
63
  throw error
@@ -113,7 +110,7 @@ export async function countJsonLines(filePath: string): Promise<number> {
113
110
  const lines = content.split('\n').filter((line) => line.trim())
114
111
  return lines.length
115
112
  } catch (error) {
116
- if ((error as NodeError).code === 'ENOENT') {
113
+ if (isNotFoundError(error)) {
117
114
  return 0
118
115
  }
119
116
  throw error
@@ -200,7 +197,7 @@ export async function readJsonLinesStreaming<T = Record<string, unknown>>(
200
197
 
201
198
  return lines
202
199
  } catch (error) {
203
- if ((error as NodeError).code === 'ENOENT') {
200
+ if (isNotFoundError(error)) {
204
201
  return []
205
202
  }
206
203
  throw error
@@ -215,7 +212,7 @@ export async function getFileSizeMB(filePath: string): Promise<number> {
215
212
  const stats = await fs.stat(filePath)
216
213
  return stats.size / (1024 * 1024)
217
214
  } catch (error) {
218
- if ((error as NodeError).code === 'ENOENT') {
215
+ if (isNotFoundError(error)) {
219
216
  return 0
220
217
  }
221
218
  throw error
@@ -0,0 +1,277 @@
1
+ /**
2
+ * MarkdownBuilder - Fluent API for constructing markdown documents
3
+ *
4
+ * Eliminates duplicated markdown generation across:
5
+ * - state-storage.ts (~35 lines)
6
+ * - queue-storage.ts (~45 lines)
7
+ * - ideas-storage.ts (~40 lines)
8
+ * - shipped-storage.ts (~25 lines)
9
+ *
10
+ * Usage:
11
+ * ```typescript
12
+ * const content = md()
13
+ * .h1('Title')
14
+ * .p('Description')
15
+ * .when(hasItems, m => m.list(items))
16
+ * .build()
17
+ * ```
18
+ */
19
+
20
+ export class MarkdownBuilder {
21
+ private lines: string[] = []
22
+
23
+ /**
24
+ * Add H1 heading
25
+ */
26
+ h1(text: string): this {
27
+ this.lines.push(`# ${text}`, '')
28
+ return this
29
+ }
30
+
31
+ /**
32
+ * Add H2 heading
33
+ */
34
+ h2(text: string): this {
35
+ this.lines.push(`## ${text}`, '')
36
+ return this
37
+ }
38
+
39
+ /**
40
+ * Add H3 heading
41
+ */
42
+ h3(text: string): this {
43
+ this.lines.push(`### ${text}`)
44
+ return this
45
+ }
46
+
47
+ /**
48
+ * Add H4 heading
49
+ */
50
+ h4(text: string): this {
51
+ this.lines.push(`#### ${text}`)
52
+ return this
53
+ }
54
+
55
+ /**
56
+ * Add paragraph
57
+ */
58
+ p(text: string): this {
59
+ this.lines.push(text, '')
60
+ return this
61
+ }
62
+
63
+ /**
64
+ * Add bold text on its own line
65
+ */
66
+ bold(text: string): this {
67
+ this.lines.push(`**${text}**`)
68
+ return this
69
+ }
70
+
71
+ /**
72
+ * Add italic text on its own line
73
+ */
74
+ italic(text: string): this {
75
+ this.lines.push(`*${text}*`)
76
+ return this
77
+ }
78
+
79
+ /**
80
+ * Add inline code
81
+ */
82
+ code(text: string): this {
83
+ this.lines.push(`\`${text}\``)
84
+ return this
85
+ }
86
+
87
+ /**
88
+ * Add code block
89
+ */
90
+ codeBlock(code: string, lang = ''): this {
91
+ this.lines.push(`\`\`\`${lang}`, code, '```', '')
92
+ return this
93
+ }
94
+
95
+ /**
96
+ * Add list item with optional checkbox
97
+ */
98
+ li(text: string, options?: { checked?: boolean; indent?: number }): this {
99
+ const indent = ' '.repeat(options?.indent ?? 0)
100
+ if (options?.checked !== undefined) {
101
+ this.lines.push(`${indent}- [${options.checked ? 'x' : ' '}] ${text}`)
102
+ } else {
103
+ this.lines.push(`${indent}- ${text}`)
104
+ }
105
+ return this
106
+ }
107
+
108
+ /**
109
+ * Add numbered list item
110
+ */
111
+ oli(text: string, number: number): this {
112
+ this.lines.push(`${number}. ${text}`)
113
+ return this
114
+ }
115
+
116
+ /**
117
+ * Add multiple list items
118
+ */
119
+ list(items: string[], options?: { checked?: boolean }): this {
120
+ items.forEach((item) => this.li(item, options))
121
+ return this
122
+ }
123
+
124
+ /**
125
+ * Add multiple numbered list items
126
+ */
127
+ orderedList(items: string[]): this {
128
+ items.forEach((item, i) => this.oli(item, i + 1))
129
+ return this
130
+ }
131
+
132
+ /**
133
+ * Add horizontal rule
134
+ */
135
+ hr(): this {
136
+ this.lines.push('', '---', '')
137
+ return this
138
+ }
139
+
140
+ /**
141
+ * Add blank line
142
+ */
143
+ blank(): this {
144
+ this.lines.push('')
145
+ return this
146
+ }
147
+
148
+ /**
149
+ * Add raw line(s)
150
+ */
151
+ raw(text: string): this {
152
+ this.lines.push(text)
153
+ return this
154
+ }
155
+
156
+ /**
157
+ * Add multiple raw lines
158
+ */
159
+ rawLines(lines: string[]): this {
160
+ this.lines.push(...lines)
161
+ return this
162
+ }
163
+
164
+ /**
165
+ * Add blockquote
166
+ */
167
+ quote(text: string): this {
168
+ this.lines.push(`> ${text}`)
169
+ return this
170
+ }
171
+
172
+ /**
173
+ * Add link
174
+ */
175
+ link(text: string, url: string): this {
176
+ this.lines.push(`[${text}](${url})`)
177
+ return this
178
+ }
179
+
180
+ /**
181
+ * Add key-value pair (bold key)
182
+ */
183
+ kv(key: string, value: string): this {
184
+ this.lines.push(`**${key}:** ${value}`)
185
+ return this
186
+ }
187
+
188
+ /**
189
+ * Add table from data
190
+ */
191
+ table(headers: string[], rows: string[][]): this {
192
+ // Header row
193
+ this.lines.push(`| ${headers.join(' | ')} |`)
194
+ // Separator
195
+ this.lines.push(`| ${headers.map(() => '---').join(' | ')} |`)
196
+ // Data rows
197
+ rows.forEach((row) => {
198
+ this.lines.push(`| ${row.join(' | ')} |`)
199
+ })
200
+ this.blank()
201
+ return this
202
+ }
203
+
204
+ /**
205
+ * Conditional block - only adds content if condition is true
206
+ */
207
+ when(condition: boolean, builder: (md: MarkdownBuilder) => void): this {
208
+ if (condition) {
209
+ builder(this)
210
+ }
211
+ return this
212
+ }
213
+
214
+ /**
215
+ * Optional block - only adds content if value exists
216
+ */
217
+ maybe<T>(value: T | null | undefined, builder: (md: MarkdownBuilder, val: T) => void): this {
218
+ if (value != null) {
219
+ builder(this, value)
220
+ }
221
+ return this
222
+ }
223
+
224
+ /**
225
+ * Iterate and build for each item
226
+ */
227
+ each<T>(items: T[], builder: (md: MarkdownBuilder, item: T, index: number) => void): this {
228
+ items.forEach((item, i) => builder(this, item, i))
229
+ return this
230
+ }
231
+
232
+ /**
233
+ * Add section with heading and content
234
+ */
235
+ section(heading: string, level: 1 | 2 | 3 | 4, builder: (md: MarkdownBuilder) => void): this {
236
+ switch (level) {
237
+ case 1:
238
+ this.h1(heading)
239
+ break
240
+ case 2:
241
+ this.h2(heading)
242
+ break
243
+ case 3:
244
+ this.h3(heading)
245
+ break
246
+ case 4:
247
+ this.h4(heading)
248
+ break
249
+ }
250
+ builder(this)
251
+ return this
252
+ }
253
+
254
+ /**
255
+ * Build final markdown string
256
+ */
257
+ build(): string {
258
+ return this.lines.join('\n')
259
+ }
260
+
261
+ /**
262
+ * Get current line count
263
+ */
264
+ get length(): number {
265
+ return this.lines.length
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Factory function for fluent API
271
+ */
272
+ export function md(): MarkdownBuilder {
273
+ return new MarkdownBuilder()
274
+ }
275
+
276
+ // Default export
277
+ export default { MarkdownBuilder, md }