prjct-cli 0.20.0 → 0.21.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 (236) hide show
  1. package/CHANGELOG.md +24 -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/integrations/notion/client.ts +323 -0
  58. package/core/integrations/notion/index.ts +43 -0
  59. package/core/integrations/notion/setup.ts +230 -0
  60. package/core/integrations/notion/sync.ts +311 -0
  61. package/core/integrations/notion/templates.ts +234 -0
  62. package/core/outcomes/analyzer.ts +7 -41
  63. package/core/outcomes/index.ts +1 -1
  64. package/core/outcomes/recorder.ts +1 -1
  65. package/core/plugin/builtin/notion.ts +178 -0
  66. package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
  67. package/core/plugin/loader.ts +5 -5
  68. package/core/plugin/registry.ts +2 -2
  69. package/core/schemas/ideas.ts +85 -54
  70. package/core/schemas/index.ts +14 -33
  71. package/core/schemas/permissions.ts +177 -0
  72. package/core/schemas/project.ts +39 -12
  73. package/core/schemas/roadmap.ts +94 -59
  74. package/core/schemas/schemas.ts +39 -0
  75. package/core/schemas/shipped.ts +87 -60
  76. package/core/schemas/state.ts +110 -70
  77. package/core/server/index.ts +21 -0
  78. package/core/server/routes.ts +165 -0
  79. package/core/server/server.ts +136 -0
  80. package/core/server/sse.ts +135 -0
  81. package/core/services/agent-service.ts +170 -0
  82. package/core/services/breakdown-service.ts +126 -0
  83. package/core/services/index.ts +21 -0
  84. package/core/services/memory-service.ts +108 -0
  85. package/core/services/project-service.ts +146 -0
  86. package/core/services/skill-service.ts +253 -0
  87. package/core/session/compaction.ts +257 -0
  88. package/core/session/index.ts +20 -8
  89. package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
  90. package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
  91. package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
  92. package/core/session/utils.ts +1 -1
  93. package/core/storage/ideas-storage.ts +10 -26
  94. package/core/storage/index.ts +14 -162
  95. package/core/storage/queue-storage.ts +13 -11
  96. package/core/storage/shipped-storage.ts +4 -17
  97. package/core/storage/state-storage.ts +35 -43
  98. package/core/storage/storage-manager.ts +42 -52
  99. package/core/storage/storage.ts +160 -0
  100. package/core/sync/auth-config.ts +1 -8
  101. package/core/sync/index.ts +17 -10
  102. package/core/sync/oauth-handler.ts +1 -6
  103. package/core/sync/sync-client.ts +6 -34
  104. package/core/sync/sync-manager.ts +11 -40
  105. package/core/types/agentic.ts +577 -0
  106. package/core/types/agents.ts +145 -0
  107. package/core/types/bus.ts +82 -0
  108. package/core/types/commands.ts +366 -0
  109. package/core/types/config.ts +70 -0
  110. package/core/types/core.ts +96 -0
  111. package/core/types/domain.ts +71 -0
  112. package/core/types/events.ts +42 -0
  113. package/core/types/fs.ts +56 -0
  114. package/core/types/index.ts +396 -500
  115. package/core/types/infrastructure.ts +196 -0
  116. package/core/types/integrations.ts +57 -0
  117. package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
  118. package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
  119. package/core/types/plugin.ts +25 -0
  120. package/core/types/server.ts +54 -0
  121. package/core/types/services.ts +65 -0
  122. package/core/types/session.ts +135 -0
  123. package/core/types/storage.ts +148 -0
  124. package/core/types/sync.ts +121 -0
  125. package/core/types/task.ts +72 -0
  126. package/core/types/template.ts +24 -0
  127. package/core/types/utils.ts +90 -0
  128. package/core/utils/cache.ts +195 -0
  129. package/core/utils/collection-filters.ts +245 -0
  130. package/core/utils/date-helper.ts +1 -5
  131. package/core/utils/file-helper.ts +20 -10
  132. package/core/utils/jsonl-helper.ts +5 -8
  133. package/core/utils/markdown-builder.ts +277 -0
  134. package/core/utils/project-commands.ts +132 -0
  135. package/core/utils/runtime.ts +119 -0
  136. package/dist/bin/prjct.mjs +12568 -0
  137. package/package.json +13 -8
  138. package/scripts/build.js +106 -0
  139. package/scripts/postinstall.js +50 -8
  140. package/templates/agentic/subagent-generation.md +1 -1
  141. package/templates/commands/init.md +43 -0
  142. package/templates/commands/notion-setup.md +191 -0
  143. package/templates/commands/serve.md +118 -0
  144. package/templates/commands/ship.md +13 -2
  145. package/templates/commands/skill.md +110 -0
  146. package/templates/commands/sync.md +1 -1
  147. package/templates/commands/test.md +23 -4
  148. package/templates/mcp-config.json +28 -0
  149. package/templates/permissions/default.jsonc +60 -0
  150. package/templates/permissions/permissive.jsonc +49 -0
  151. package/templates/permissions/strict.jsonc +62 -0
  152. package/templates/skills/code-review.md +47 -0
  153. package/templates/skills/debug.md +61 -0
  154. package/templates/skills/refactor.md +47 -0
  155. package/templates/subagents/domain/devops.md +1 -1
  156. package/templates/subagents/domain/testing.md +6 -10
  157. package/templates/subagents/workflow/prjct-shipper.md +16 -7
  158. package/templates/tools/bash.txt +22 -0
  159. package/templates/tools/edit.txt +18 -0
  160. package/templates/tools/glob.txt +19 -0
  161. package/templates/tools/grep.txt +21 -0
  162. package/templates/tools/read.txt +14 -0
  163. package/templates/tools/task.txt +20 -0
  164. package/templates/tools/webfetch.txt +16 -0
  165. package/templates/tools/websearch.txt +18 -0
  166. package/templates/tools/write.txt +17 -0
  167. package/core/agentic/command-executor/command-executor.ts +0 -312
  168. package/core/agentic/command-executor/index.ts +0 -16
  169. package/core/agentic/command-executor/status-signal.ts +0 -38
  170. package/core/agentic/command-executor/types.ts +0 -79
  171. package/core/agentic/ground-truth/index.ts +0 -76
  172. package/core/agentic/ground-truth/types.ts +0 -33
  173. package/core/agentic/ground-truth/utils.ts +0 -48
  174. package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
  175. package/core/agentic/ground-truth/verifiers/done.ts +0 -75
  176. package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
  177. package/core/agentic/ground-truth/verifiers/index.ts +0 -37
  178. package/core/agentic/ground-truth/verifiers/init.ts +0 -52
  179. package/core/agentic/ground-truth/verifiers/now.ts +0 -57
  180. package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
  181. package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
  182. package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
  183. package/core/agentic/ground-truth/verifiers.ts +0 -6
  184. package/core/agentic/loop-detector/error-analysis.ts +0 -97
  185. package/core/agentic/loop-detector/hallucination.ts +0 -71
  186. package/core/agentic/loop-detector/index.ts +0 -41
  187. package/core/agentic/loop-detector/loop-detector.ts +0 -222
  188. package/core/agentic/loop-detector/types.ts +0 -66
  189. package/core/agentic/memory-system/history.ts +0 -53
  190. package/core/agentic/memory-system/index.ts +0 -192
  191. package/core/agentic/memory-system/patterns.ts +0 -156
  192. package/core/agentic/memory-system/semantic-memories.ts +0 -278
  193. package/core/agentic/memory-system/session.ts +0 -21
  194. package/core/agentic/plan-mode/approval.ts +0 -57
  195. package/core/agentic/plan-mode/constants.ts +0 -44
  196. package/core/agentic/plan-mode/index.ts +0 -28
  197. package/core/agentic/plan-mode/plan-mode.ts +0 -407
  198. package/core/agentic/plan-mode/types.ts +0 -193
  199. package/core/agents/types.ts +0 -126
  200. package/core/command-registry/categories.ts +0 -23
  201. package/core/command-registry/commands.ts +0 -15
  202. package/core/command-registry/core-commands.ts +0 -344
  203. package/core/command-registry/index.ts +0 -158
  204. package/core/command-registry/optional-commands.ts +0 -163
  205. package/core/command-registry/setup-commands.ts +0 -83
  206. package/core/command-registry/types.ts +0 -59
  207. package/core/command-registry.ts +0 -9
  208. package/core/commands/types.ts +0 -185
  209. package/core/commands.ts +0 -11
  210. package/core/constants/formats.ts +0 -187
  211. package/core/context-sync.ts +0 -18
  212. package/core/data/index.ts +0 -27
  213. package/core/data/md-base-manager.ts +0 -203
  214. package/core/data/md-ideas-manager.ts +0 -155
  215. package/core/data/md-queue-manager.ts +0 -180
  216. package/core/data/md-shipped-manager.ts +0 -90
  217. package/core/data/md-state-manager.ts +0 -137
  218. package/core/domain/task-stack/index.ts +0 -19
  219. package/core/domain/task-stack/parser.ts +0 -86
  220. package/core/domain/task-stack/storage.ts +0 -123
  221. package/core/domain/task-stack/task-stack.ts +0 -340
  222. package/core/domain/task-stack/types.ts +0 -51
  223. package/core/infrastructure/command-installer/command-installer.ts +0 -327
  224. package/core/infrastructure/command-installer/global-config.ts +0 -136
  225. package/core/infrastructure/command-installer/index.ts +0 -25
  226. package/core/infrastructure/command-installer/types.ts +0 -41
  227. package/core/infrastructure/session-manager/index.ts +0 -23
  228. package/core/infrastructure/session-manager/types.ts +0 -45
  229. package/core/infrastructure/session-manager.ts +0 -8
  230. package/core/serializers/ideas-serializer.ts +0 -187
  231. package/core/serializers/index.ts +0 -36
  232. package/core/serializers/queue-serializer.ts +0 -210
  233. package/core/serializers/shipped-serializer.ts +0 -108
  234. package/core/serializers/state-serializer.ts +0 -136
  235. package/core/session/types.ts +0 -29
  236. /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
@@ -4,11 +4,11 @@
4
4
  */
5
5
 
6
6
  import path from 'path'
7
- import pathManager from '../path-manager'
8
- import * as dateHelper from '../../utils/date-helper'
9
- import * as jsonlHelper from '../../utils/jsonl-helper'
10
- import * as fileHelper from '../../utils/file-helper'
11
- import type { SessionEntry, MigrationResult, SessionMetadata } from './types'
7
+ import pathManager from '../infrastructure/path-manager'
8
+ import * as dateHelper from '../utils/date-helper'
9
+ import * as jsonlHelper from '../utils/jsonl-helper'
10
+ import * as fileHelper from '../utils/file-helper'
11
+ import type { SessionEntry, SessionMigrationResult, SessionLogMetadata } from '../types'
12
12
 
13
13
  /**
14
14
  * Migrate legacy JSONL file
@@ -17,9 +17,9 @@ export async function migrateLegacyJsonl(
17
17
  projectId: string,
18
18
  content: string,
19
19
  sessionFilename: string,
20
- updateMetadata: (sessionPath: string, updates: Partial<SessionMetadata>) => Promise<void>,
20
+ updateMetadata: (sessionPath: string, updates: Partial<SessionLogMetadata>) => Promise<void>,
21
21
  ensureMetadata: (sessionPath: string) => Promise<void>
22
- ): Promise<MigrationResult> {
22
+ ): Promise<SessionMigrationResult> {
23
23
  const entries = jsonlHelper.parseJsonLines(content) as SessionEntry[]
24
24
  const sessionGroups = new Map<string, SessionEntry[]>()
25
25
 
@@ -68,8 +68,8 @@ export async function migrateLegacyMarkdown(
68
68
  sessionPath: string,
69
69
  content: string,
70
70
  sessionFilename: string,
71
- updateMetadata: (sessionPath: string, updates: Partial<SessionMetadata>) => Promise<void>
72
- ): Promise<MigrationResult> {
71
+ updateMetadata: (sessionPath: string, updates: Partial<SessionLogMetadata>) => Promise<void>
72
+ ): Promise<SessionMigrationResult> {
73
73
  const filePath = path.join(sessionPath, sessionFilename)
74
74
 
75
75
  await fileHelper.writeFile(filePath, content)
@@ -1,20 +1,21 @@
1
1
  /**
2
- * SessionManager Class
2
+ * SessionLogManager Class
3
3
  * Manages temporal fragmentation of logs and progress data.
4
+ * Writes to sessions/YYYY-MM/DD/ structure with auto-rotation.
4
5
  */
5
6
 
6
7
  import path from 'path'
7
- import pathManager from '../path-manager'
8
- import { VERSION } from '../../utils/version'
9
- import * as dateHelper from '../../utils/date-helper'
10
- import * as jsonlHelper from '../../utils/jsonl-helper'
11
- import * as fileHelper from '../../utils/file-helper'
12
- import { migrateLegacyJsonl, migrateLegacyMarkdown } from './migration'
13
- import type { SessionEntry, SessionMetadata, SessionStats, MigrationResult } from './types'
14
-
15
- export class SessionManager {
8
+ import pathManager from '../infrastructure/path-manager'
9
+ import { VERSION } from '../utils/version'
10
+ import * as dateHelper from '../utils/date-helper'
11
+ import * as jsonlHelper from '../utils/jsonl-helper'
12
+ import * as fileHelper from '../utils/file-helper'
13
+ import { migrateLegacyJsonl, migrateLegacyMarkdown } from './log-migration'
14
+ import type { SessionEntry, SessionLogMetadata, SessionStats, SessionMigrationResult } from '../types'
15
+
16
+ export class SessionLogManager {
16
17
  private currentSessionCache: Map<string, string>
17
- private sessionMetadataCache: Map<string, SessionMetadata>
18
+ private sessionMetadataCache: Map<string, SessionLogMetadata>
18
19
 
19
20
  constructor() {
20
21
  this.currentSessionCache = new Map()
@@ -34,7 +35,7 @@ export class SessionManager {
34
35
  const sessionPath = await pathManager.ensureSessionPath(projectId)
35
36
  this.currentSessionCache.set(cacheKey, sessionPath)
36
37
 
37
- await this._ensureSessionMetadata(sessionPath)
38
+ await this._ensureSessionLogMetadata(sessionPath)
38
39
 
39
40
  return sessionPath
40
41
  }
@@ -53,7 +54,7 @@ export class SessionManager {
53
54
  // Use automatic rotation to prevent large files (>10MB)
54
55
  await jsonlHelper.appendJsonLineWithRotation(filePath, entry, 10)
55
56
 
56
- await this._updateSessionMetadata(sessionPath, {
57
+ await this._updateSessionLogMetadata(sessionPath, {
57
58
  lastActivity: dateHelper.getTimestamp(),
58
59
  entryCount: await jsonlHelper.countJsonLines(filePath),
59
60
  })
@@ -73,7 +74,7 @@ export class SessionManager {
73
74
  await fileHelper.appendToFile(filePath, content)
74
75
  }
75
76
 
76
- await this._updateSessionMetadata(sessionPath, {
77
+ await this._updateSessionLogMetadata(sessionPath, {
77
78
  lastActivity: dateHelper.getTimestamp(),
78
79
  })
79
80
  }
@@ -177,7 +178,7 @@ export class SessionManager {
177
178
  let activeDays = 0
178
179
 
179
180
  for (const session of sessions) {
180
- const metadata = await this._getSessionMetadata(session.path)
181
+ const metadata = await this._getSessionLogMetadata(session.path)
181
182
  if (metadata) {
182
183
  totalEntries += metadata.entryCount || 0
183
184
  totalShips += metadata.shipCount || 0
@@ -203,7 +204,7 @@ export class SessionManager {
203
204
  projectId: string,
204
205
  legacyFilePath: string,
205
206
  sessionFilename: string
206
- ): Promise<MigrationResult> {
207
+ ): Promise<SessionMigrationResult> {
207
208
  try {
208
209
  const content = await fileHelper.readFile(legacyFilePath)
209
210
 
@@ -212,8 +213,8 @@ export class SessionManager {
212
213
  projectId,
213
214
  content,
214
215
  sessionFilename,
215
- (sp, u) => this._updateSessionMetadata(sp, u),
216
- (sp) => this._ensureSessionMetadata(sp)
216
+ (sp, u) => this._updateSessionLogMetadata(sp, u),
217
+ (sp) => this._ensureSessionLogMetadata(sp)
217
218
  )
218
219
  } else {
219
220
  const sessionPath = await this.getCurrentSession(projectId)
@@ -221,7 +222,7 @@ export class SessionManager {
221
222
  sessionPath,
222
223
  content,
223
224
  sessionFilename,
224
- (sp, u) => this._updateSessionMetadata(sp, u)
225
+ (sp, u) => this._updateSessionLogMetadata(sp, u)
225
226
  )
226
227
  }
227
228
  } catch (error) {
@@ -236,14 +237,14 @@ export class SessionManager {
236
237
  /**
237
238
  * Get session metadata
238
239
  */
239
- private async _getSessionMetadata(sessionPath: string): Promise<SessionMetadata | null> {
240
+ private async _getSessionLogMetadata(sessionPath: string): Promise<SessionLogMetadata | null> {
240
241
  const metadataPath = path.join(sessionPath, 'session-meta.json')
241
242
 
242
243
  if (this.sessionMetadataCache.has(sessionPath)) {
243
244
  return this.sessionMetadataCache.get(sessionPath)!
244
245
  }
245
246
 
246
- const metadata = await fileHelper.readJson<SessionMetadata>(metadataPath, null)
247
+ const metadata = await fileHelper.readJson<SessionLogMetadata>(metadataPath, null)
247
248
  if (metadata) {
248
249
  this.sessionMetadataCache.set(sessionPath, metadata)
249
250
  }
@@ -253,12 +254,12 @@ export class SessionManager {
253
254
  /**
254
255
  * Ensure session metadata exists
255
256
  */
256
- private async _ensureSessionMetadata(sessionPath: string): Promise<void> {
257
+ private async _ensureSessionLogMetadata(sessionPath: string): Promise<void> {
257
258
  const metadataPath = path.join(sessionPath, 'session-meta.json')
258
259
 
259
260
  const exists = await fileHelper.fileExists(metadataPath)
260
261
  if (!exists) {
261
- const metadata: SessionMetadata = {
262
+ const metadata: SessionLogMetadata = {
262
263
  created: dateHelper.getTimestamp(),
263
264
  lastActivity: dateHelper.getTimestamp(),
264
265
  entryCount: 0,
@@ -273,11 +274,11 @@ export class SessionManager {
273
274
  /**
274
275
  * Update session metadata
275
276
  */
276
- private async _updateSessionMetadata(
277
+ private async _updateSessionLogMetadata(
277
278
  sessionPath: string,
278
- updates: Partial<SessionMetadata>
279
+ updates: Partial<SessionLogMetadata>
279
280
  ): Promise<void> {
280
- const metadata = (await this._getSessionMetadata(sessionPath)) || {}
281
+ const metadata = (await this._getSessionLogMetadata(sessionPath)) || {}
281
282
  Object.assign(metadata, updates)
282
283
 
283
284
  const metadataPath = path.join(sessionPath, 'session-meta.json')
@@ -1,6 +1,7 @@
1
1
  /**
2
- * SessionManager Class
3
- * Core session management functionality
2
+ * TaskSessionManager Class
3
+ * Manages task lifecycle: create, pause, resume, complete
4
+ * Tracks metrics, timeline, and archives completed sessions.
4
5
  */
5
6
 
6
7
  import fs from 'fs/promises'
@@ -10,12 +11,12 @@ import { promisify } from 'util'
10
11
  import pathManager from '../infrastructure/path-manager'
11
12
  import configManager from '../infrastructure/config-manager'
12
13
  import { emit } from '../bus'
13
- import type { Session, SessionMetrics } from './types'
14
+ import type { Session, SessionMetrics } from '../types'
14
15
  import { generateId, calculateDuration, formatDuration } from './utils'
15
16
 
16
17
  const execAsync = promisify(exec)
17
18
 
18
- export class SessionManager {
19
+ export class TaskSessionManager {
19
20
  private projectPath: string
20
21
  private projectId: string | null
21
22
  private sessionDir: string | null
@@ -89,7 +90,9 @@ export class SessionManager {
89
90
  completedAt: null,
90
91
  duration: 0,
91
92
  metrics: {
93
+ filesCreated: 0,
92
94
  filesChanged: 0,
95
+ filesModified: 0,
93
96
  linesAdded: 0,
94
97
  linesRemoved: 0,
95
98
  commits: 0,
@@ -2,7 +2,7 @@
2
2
  * Session Utilities
3
3
  */
4
4
 
5
- import type { Session, TimelineEvent } from './types'
5
+ import type { Session, TimelineEvent } from '../types'
6
6
  import { generateUUID } from '../schemas'
7
7
 
8
8
  /**
@@ -7,24 +7,8 @@
7
7
 
8
8
  import { StorageManager } from './storage-manager'
9
9
  import { generateUUID } from '../schemas'
10
-
11
- export type IdeaStatus = 'pending' | 'converted' | 'archived'
12
- export type IdeaPriority = 'low' | 'medium' | 'high'
13
-
14
- export interface Idea {
15
- id: string
16
- text: string
17
- status: IdeaStatus
18
- priority: IdeaPriority
19
- tags: string[]
20
- addedAt: string
21
- convertedTo?: string // featureId if converted
22
- }
23
-
24
- export interface IdeasJson {
25
- ideas: Idea[]
26
- lastUpdated: string
27
- }
10
+ import { getTimestamp } from '../utils/date-helper'
11
+ import type { Idea, IdeasJson, IdeaStatus, IdeaPriority } from '../types'
28
12
 
29
13
  class IdeasStorage extends StorageManager<IdeasJson> {
30
14
  constructor() {
@@ -127,12 +111,12 @@ class IdeasStorage extends StorageManager<IdeasJson> {
127
111
  status: 'pending',
128
112
  priority: options.priority || 'medium',
129
113
  tags: options.tags || [],
130
- addedAt: new Date().toISOString()
114
+ addedAt: getTimestamp()
131
115
  }
132
116
 
133
117
  await this.update(projectId, (data) => ({
134
118
  ideas: [idea, ...data.ideas], // Prepend new ideas
135
- lastUpdated: new Date().toISOString()
119
+ lastUpdated: getTimestamp()
136
120
  }))
137
121
 
138
122
  // Publish event
@@ -167,7 +151,7 @@ class IdeasStorage extends StorageManager<IdeasJson> {
167
151
  ? { ...i, status: 'converted' as IdeaStatus, convertedTo: featureId }
168
152
  : i
169
153
  ),
170
- lastUpdated: new Date().toISOString()
154
+ lastUpdated: getTimestamp()
171
155
  }))
172
156
 
173
157
  await this.publishEvent(projectId, 'idea.converted', {
@@ -184,7 +168,7 @@ class IdeasStorage extends StorageManager<IdeasJson> {
184
168
  ideas: data.ideas.map(i =>
185
169
  i.id === ideaId ? { ...i, status: 'archived' as IdeaStatus } : i
186
170
  ),
187
- lastUpdated: new Date().toISOString()
171
+ lastUpdated: getTimestamp()
188
172
  }))
189
173
 
190
174
  await this.publishEvent(projectId, 'idea.archived', { ideaId })
@@ -202,7 +186,7 @@ class IdeasStorage extends StorageManager<IdeasJson> {
202
186
  ideas: data.ideas.map(i =>
203
187
  i.id === ideaId ? { ...i, priority } : i
204
188
  ),
205
- lastUpdated: new Date().toISOString()
189
+ lastUpdated: getTimestamp()
206
190
  }))
207
191
  }
208
192
 
@@ -220,7 +204,7 @@ class IdeasStorage extends StorageManager<IdeasJson> {
220
204
  ? { ...i, tags: [...new Set([...i.tags, ...tags])] }
221
205
  : i
222
206
  ),
223
- lastUpdated: new Date().toISOString()
207
+ lastUpdated: getTimestamp()
224
208
  }))
225
209
  }
226
210
 
@@ -230,7 +214,7 @@ class IdeasStorage extends StorageManager<IdeasJson> {
230
214
  async removeIdea(projectId: string, ideaId: string): Promise<void> {
231
215
  await this.update(projectId, (data) => ({
232
216
  ideas: data.ideas.filter(i => i.id !== ideaId),
233
- lastUpdated: new Date().toISOString()
217
+ lastUpdated: getTimestamp()
234
218
  }))
235
219
  }
236
220
 
@@ -266,7 +250,7 @@ class IdeasStorage extends StorageManager<IdeasJson> {
266
250
 
267
251
  await this.update(projectId, (d) => ({
268
252
  ideas: d.ideas.filter(i => !toRemove.has(i.id)),
269
- lastUpdated: new Date().toISOString()
253
+ lastUpdated: getTimestamp()
270
254
  }))
271
255
 
272
256
  return { removed }
@@ -38,167 +38,19 @@
38
38
  export { StorageManager } from './storage-manager'
39
39
  export { stateStorage } from './state-storage'
40
40
  export { queueStorage } from './queue-storage'
41
- export { ideasStorage, type Idea, type IdeasJson, type IdeaStatus, type IdeaPriority } from './ideas-storage'
42
- export { shippedStorage, type ShippedFeature, type ShippedJson } from './shipped-storage'
41
+ export { ideasStorage } from './ideas-storage'
42
+ export { shippedStorage } from './shipped-storage'
43
43
 
44
44
  // ========== GRANULAR STORAGE (Legacy) ==========
45
-
46
- import fs from 'fs/promises'
47
- import path from 'path'
48
- import os from 'os'
49
- import { eventBus, inferEventType } from '../events'
50
-
51
- export interface Storage {
52
- write<T>(path: string[], data: T): Promise<void>
53
- read<T>(path: string[]): Promise<T | null>
54
- list(prefix: string[]): Promise<string[][]>
55
- delete(path: string[]): Promise<void>
56
- exists(path: string[]): Promise<boolean>
57
- }
58
-
59
- class FileStorage implements Storage {
60
- private projectId: string
61
- private basePath: string
62
-
63
- constructor(projectId: string) {
64
- this.projectId = projectId
65
- this.basePath = path.join(os.homedir(), '.prjct-cli/projects', projectId, 'data')
66
- }
67
-
68
- /**
69
- * Convert path array to file path
70
- * ["task", "abc123"] → data/tasks/abc123.json
71
- * ["project"] → data/project.json
72
- */
73
- private pathToFile(pathArray: string[]): string {
74
- if (pathArray.length === 1) {
75
- return path.join(this.basePath, `${pathArray[0]}.json`)
76
- }
77
-
78
- // Pluralize first segment for directory
79
- const dir = pathArray[0] + 's'
80
- const rest = pathArray.slice(1)
81
- const filename = rest.join('/') + '.json'
82
-
83
- return path.join(this.basePath, dir, filename)
84
- }
85
-
86
- async write<T>(pathArray: string[], data: T): Promise<void> {
87
- const filePath = this.pathToFile(pathArray)
88
-
89
- // Ensure directory exists
90
- await fs.mkdir(path.dirname(filePath), { recursive: true })
91
-
92
- // Write data
93
- await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
94
-
95
- // Publish event for sync
96
- eventBus.publish({
97
- type: inferEventType(pathArray, 'write'),
98
- path: pathArray,
99
- data,
100
- timestamp: new Date().toISOString(),
101
- projectId: this.projectId
102
- })
103
-
104
- // Update index if it's a collection item
105
- if (pathArray.length === 2) {
106
- await this.updateIndex(pathArray[0], pathArray[1], 'add')
107
- }
108
- }
109
-
110
- async read<T>(pathArray: string[]): Promise<T | null> {
111
- const filePath = this.pathToFile(pathArray)
112
-
113
- try {
114
- const content = await fs.readFile(filePath, 'utf-8')
115
- return JSON.parse(content) as T
116
- } catch {
117
- return null
118
- }
119
- }
120
-
121
- async list(prefix: string[]): Promise<string[][]> {
122
- const dir = path.join(this.basePath, prefix[0] + 's')
123
-
124
- try {
125
- const files = await fs.readdir(dir)
126
- return files
127
- .filter(f => f.endsWith('.json') && f !== 'index.json')
128
- .map(f => [...prefix, f.replace('.json', '')])
129
- } catch {
130
- return []
131
- }
132
- }
133
-
134
- async delete(pathArray: string[]): Promise<void> {
135
- const filePath = this.pathToFile(pathArray)
136
-
137
- try {
138
- await fs.unlink(filePath)
139
-
140
- // Publish event for sync
141
- eventBus.publish({
142
- type: inferEventType(pathArray, 'delete'),
143
- path: pathArray,
144
- data: null,
145
- timestamp: new Date().toISOString(),
146
- projectId: this.projectId
147
- })
148
-
149
- // Update index if it's a collection item
150
- if (pathArray.length === 2) {
151
- await this.updateIndex(pathArray[0], pathArray[1], 'remove')
152
- }
153
- } catch {
154
- // File doesn't exist, ignore
155
- }
156
- }
157
-
158
- async exists(pathArray: string[]): Promise<boolean> {
159
- const filePath = this.pathToFile(pathArray)
160
-
161
- try {
162
- await fs.access(filePath)
163
- return true
164
- } catch {
165
- return false
166
- }
167
- }
168
-
169
- /**
170
- * Update collection index
171
- */
172
- private async updateIndex(collection: string, id: string, action: 'add' | 'remove'): Promise<void> {
173
- const indexPath = path.join(this.basePath, collection + 's', 'index.json')
174
-
175
- let index: { ids: string[]; updatedAt: string } = { ids: [], updatedAt: '' }
176
-
177
- try {
178
- const content = await fs.readFile(indexPath, 'utf-8')
179
- index = JSON.parse(content)
180
- } catch {
181
- // Index doesn't exist yet
182
- }
183
-
184
- if (action === 'add' && !index.ids.includes(id)) {
185
- index.ids.push(id)
186
- } else if (action === 'remove') {
187
- index.ids = index.ids.filter(i => i !== id)
188
- }
189
-
190
- index.updatedAt = new Date().toISOString()
191
-
192
- await fs.mkdir(path.dirname(indexPath), { recursive: true })
193
- await fs.writeFile(indexPath, JSON.stringify(index, null, 2), 'utf-8')
194
- }
195
- }
196
-
197
- /**
198
- * Get storage instance for a project
199
- */
200
- export function getStorage(projectId: string): Storage {
201
- return new FileStorage(projectId)
202
- }
203
-
204
- export default { getStorage }
45
+ export { getStorage, default } from './storage'
46
+
47
+ // Re-export types from canonical location
48
+ export type {
49
+ Storage,
50
+ Idea,
51
+ IdeasJson,
52
+ IdeaStatus,
53
+ IdeaPriority,
54
+ ShippedFeature,
55
+ ShippedJson,
56
+ } from '../types'
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { StorageManager } from './storage-manager'
9
9
  import { generateUUID } from '../schemas'
10
+ import { getTimestamp } from '../utils/date-helper'
10
11
  import type { QueueJson, QueueTask, Priority, TaskType, TaskSection } from '../schemas/state'
11
12
 
12
13
  class QueueStorage extends StorageManager<QueueJson> {
@@ -125,13 +126,13 @@ class QueueStorage extends StorageManager<QueueJson> {
125
126
  const newTask: QueueTask = {
126
127
  ...task,
127
128
  id: generateUUID(),
128
- createdAt: new Date().toISOString(),
129
+ createdAt: getTimestamp(),
129
130
  completed: false
130
131
  }
131
132
 
132
133
  await this.update(projectId, (queue) => ({
133
134
  tasks: [...queue.tasks, newTask],
134
- lastUpdated: new Date().toISOString()
135
+ lastUpdated: getTimestamp()
135
136
  }))
136
137
 
137
138
  // Publish incremental event
@@ -152,7 +153,7 @@ class QueueStorage extends StorageManager<QueueJson> {
152
153
  projectId: string,
153
154
  tasks: Omit<QueueTask, 'id' | 'createdAt' | 'completed' | 'completedAt'>[]
154
155
  ): Promise<QueueTask[]> {
155
- const now = new Date().toISOString()
156
+ const now = getTimestamp()
156
157
  const newTasks: QueueTask[] = tasks.map(task => ({
157
158
  ...task,
158
159
  id: generateUUID(),
@@ -180,7 +181,7 @@ class QueueStorage extends StorageManager<QueueJson> {
180
181
  async removeTask(projectId: string, taskId: string): Promise<void> {
181
182
  await this.update(projectId, (queue) => ({
182
183
  tasks: queue.tasks.filter(t => t.id !== taskId),
183
- lastUpdated: new Date().toISOString()
184
+ lastUpdated: getTimestamp()
184
185
  }))
185
186
 
186
187
  await this.publishEvent(projectId, 'queue.task_removed', { taskId })
@@ -198,20 +199,21 @@ class QueueStorage extends StorageManager<QueueJson> {
198
199
  completedTask = {
199
200
  ...t,
200
201
  completed: true,
201
- completedAt: new Date().toISOString()
202
+ completedAt: getTimestamp()
202
203
  }
203
204
  return completedTask
204
205
  }
205
206
  return t
206
207
  })
207
- return { tasks, lastUpdated: new Date().toISOString() }
208
+ return { tasks, lastUpdated: getTimestamp() }
208
209
  })
209
210
 
210
211
  if (completedTask) {
212
+ const task = completedTask as QueueTask
211
213
  await this.publishEvent(projectId, 'queue.task_completed', {
212
214
  taskId,
213
- description: completedTask.description,
214
- completedAt: completedTask.completedAt
215
+ description: task.description,
216
+ completedAt: task.completedAt
215
217
  })
216
218
  }
217
219
 
@@ -230,7 +232,7 @@ class QueueStorage extends StorageManager<QueueJson> {
230
232
  tasks: queue.tasks.map(t =>
231
233
  t.id === taskId ? { ...t, section } : t
232
234
  ),
233
- lastUpdated: new Date().toISOString()
235
+ lastUpdated: getTimestamp()
234
236
  }))
235
237
  }
236
238
 
@@ -246,7 +248,7 @@ class QueueStorage extends StorageManager<QueueJson> {
246
248
  tasks: queue.tasks.map(t =>
247
249
  t.id === taskId ? { ...t, priority } : t
248
250
  ),
249
- lastUpdated: new Date().toISOString()
251
+ lastUpdated: getTimestamp()
250
252
  }))
251
253
  }
252
254
 
@@ -259,7 +261,7 @@ class QueueStorage extends StorageManager<QueueJson> {
259
261
 
260
262
  await this.update(projectId, (q) => ({
261
263
  tasks: q.tasks.filter(t => !t.completed),
262
- lastUpdated: new Date().toISOString()
264
+ lastUpdated: getTimestamp()
263
265
  }))
264
266
 
265
267
  return completedCount
@@ -7,21 +7,8 @@
7
7
 
8
8
  import { StorageManager } from './storage-manager'
9
9
  import { generateUUID } from '../schemas'
10
-
11
- export interface ShippedFeature {
12
- id: string
13
- name: string
14
- shippedAt: string
15
- version: string
16
- description?: string
17
- tasks?: string[] // Task IDs that were part of this ship
18
- duration?: string // How long it took
19
- }
20
-
21
- export interface ShippedJson {
22
- shipped: ShippedFeature[]
23
- lastUpdated: string
24
- }
10
+ import { getTimestamp } from '../utils/date-helper'
11
+ import type { ShippedFeature, ShippedJson } from '../types'
25
12
 
26
13
  class ShippedStorage extends StorageManager<ShippedJson> {
27
14
  constructor() {
@@ -139,12 +126,12 @@ class ShippedStorage extends StorageManager<ShippedJson> {
139
126
  const shipped: ShippedFeature = {
140
127
  ...feature,
141
128
  id: generateUUID(),
142
- shippedAt: new Date().toISOString()
129
+ shippedAt: getTimestamp()
143
130
  }
144
131
 
145
132
  await this.update(projectId, (data) => ({
146
133
  shipped: [shipped, ...data.shipped], // Prepend
147
- lastUpdated: new Date().toISOString()
134
+ lastUpdated: getTimestamp()
148
135
  }))
149
136
 
150
137
  // Publish event