prjct-cli 0.4.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 (71) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/CLAUDE.md +300 -0
  3. package/LICENSE +21 -0
  4. package/README.md +424 -0
  5. package/bin/prjct +214 -0
  6. package/core/agent-detector.js +249 -0
  7. package/core/agents/claude-agent.js +250 -0
  8. package/core/agents/codex-agent.js +256 -0
  9. package/core/agents/terminal-agent.js +465 -0
  10. package/core/analyzer.js +596 -0
  11. package/core/animations-simple.js +240 -0
  12. package/core/animations.js +277 -0
  13. package/core/author-detector.js +218 -0
  14. package/core/capability-installer.js +190 -0
  15. package/core/command-installer.js +775 -0
  16. package/core/commands.js +2050 -0
  17. package/core/config-manager.js +335 -0
  18. package/core/migrator.js +784 -0
  19. package/core/path-manager.js +324 -0
  20. package/core/project-capabilities.js +144 -0
  21. package/core/session-manager.js +439 -0
  22. package/core/version.js +107 -0
  23. package/core/workflow-engine.js +213 -0
  24. package/core/workflow-prompts.js +192 -0
  25. package/core/workflow-rules.js +147 -0
  26. package/package.json +80 -0
  27. package/scripts/install.sh +433 -0
  28. package/scripts/verify-installation.sh +158 -0
  29. package/templates/agents/AGENTS.md +164 -0
  30. package/templates/commands/analyze.md +125 -0
  31. package/templates/commands/cleanup.md +102 -0
  32. package/templates/commands/context.md +105 -0
  33. package/templates/commands/design.md +113 -0
  34. package/templates/commands/done.md +44 -0
  35. package/templates/commands/fix.md +87 -0
  36. package/templates/commands/git.md +79 -0
  37. package/templates/commands/help.md +72 -0
  38. package/templates/commands/idea.md +50 -0
  39. package/templates/commands/init.md +237 -0
  40. package/templates/commands/next.md +74 -0
  41. package/templates/commands/now.md +35 -0
  42. package/templates/commands/progress.md +92 -0
  43. package/templates/commands/recap.md +86 -0
  44. package/templates/commands/roadmap.md +107 -0
  45. package/templates/commands/ship.md +41 -0
  46. package/templates/commands/stuck.md +48 -0
  47. package/templates/commands/task.md +97 -0
  48. package/templates/commands/test.md +94 -0
  49. package/templates/commands/workflow.md +224 -0
  50. package/templates/examples/natural-language-examples.md +320 -0
  51. package/templates/mcp-config.json +8 -0
  52. package/templates/workflows/analyze.md +159 -0
  53. package/templates/workflows/cleanup.md +73 -0
  54. package/templates/workflows/context.md +72 -0
  55. package/templates/workflows/design.md +88 -0
  56. package/templates/workflows/done.md +20 -0
  57. package/templates/workflows/fix.md +201 -0
  58. package/templates/workflows/git.md +192 -0
  59. package/templates/workflows/help.md +13 -0
  60. package/templates/workflows/idea.md +22 -0
  61. package/templates/workflows/init.md +80 -0
  62. package/templates/workflows/natural-language-handler.md +183 -0
  63. package/templates/workflows/next.md +44 -0
  64. package/templates/workflows/now.md +19 -0
  65. package/templates/workflows/progress.md +113 -0
  66. package/templates/workflows/recap.md +66 -0
  67. package/templates/workflows/roadmap.md +95 -0
  68. package/templates/workflows/ship.md +18 -0
  69. package/templates/workflows/stuck.md +25 -0
  70. package/templates/workflows/task.md +109 -0
  71. package/templates/workflows/test.md +243 -0
@@ -0,0 +1,439 @@
1
+ const fs = require('fs').promises
2
+ const path = require('path')
3
+ const pathManager = require('./path-manager')
4
+ const { VERSION } = require('./version')
5
+
6
+ /**
7
+ * SessionManager - Manages temporal fragmentation of logs and progress data
8
+ *
9
+ * Handles:
10
+ * - Daily session creation and rotation
11
+ * - Writing logs to date-specific directories
12
+ * - Reading historical data across multiple sessions
13
+ * - Session consolidation and queries
14
+ * - Automatic migration from legacy single-file logs
15
+ *
16
+ * @version 0.2.1
17
+ */
18
+ class SessionManager {
19
+ constructor() {
20
+ this.currentSessionCache = new Map() // Cache current session paths
21
+ this.sessionMetadataCache = new Map() // Cache session metadata
22
+ }
23
+
24
+ /**
25
+ * Get or create current session directory for a project
26
+ *
27
+ * @param {string} projectId - The project identifier
28
+ * @returns {Promise<string>} - Path to today's session directory
29
+ */
30
+ async getCurrentSession(projectId) {
31
+ const cacheKey = `${projectId}-${this._getTodayKey()}`
32
+
33
+ if (this.currentSessionCache.has(cacheKey)) {
34
+ return this.currentSessionCache.get(cacheKey)
35
+ }
36
+
37
+ const sessionPath = await pathManager.ensureSessionPath(projectId)
38
+ this.currentSessionCache.set(cacheKey, sessionPath)
39
+
40
+ await this._ensureSessionMetadata(sessionPath)
41
+
42
+ return sessionPath
43
+ }
44
+
45
+ /**
46
+ * Write log entry to current session
47
+ *
48
+ * @param {string} projectId - The project identifier
49
+ * @param {Object} entry - Log entry object
50
+ * @param {string} filename - Target filename (default: context.jsonl)
51
+ * @returns {Promise<void>}
52
+ */
53
+ async writeToSession(projectId, entry, filename = 'context.jsonl') {
54
+ const sessionPath = await this.getCurrentSession(projectId)
55
+ const filePath = path.join(sessionPath, filename)
56
+
57
+ const logLine = JSON.stringify(entry) + '\n'
58
+
59
+ try {
60
+ const existing = await fs.readFile(filePath, 'utf-8')
61
+ await fs.writeFile(filePath, existing + logLine, 'utf-8')
62
+ } catch {
63
+ await fs.writeFile(filePath, logLine, 'utf-8')
64
+ }
65
+
66
+ await this._updateSessionMetadata(sessionPath, {
67
+ lastActivity: new Date().toISOString(),
68
+ entryCount: await this._getFileLineCount(filePath),
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Append content to a session file (for markdown files like shipped.md)
74
+ *
75
+ * @param {string} projectId - The project identifier
76
+ * @param {string} content - Content to append
77
+ * @param {string} filename - Target filename
78
+ * @returns {Promise<void>}
79
+ */
80
+ async appendToSession(projectId, content, filename) {
81
+ const sessionPath = await this.getCurrentSession(projectId)
82
+ const filePath = path.join(sessionPath, filename)
83
+
84
+ try {
85
+ const existing = await fs.readFile(filePath, 'utf-8')
86
+ await fs.writeFile(filePath, existing + content, 'utf-8')
87
+ } catch {
88
+ let initialContent = ''
89
+ if (filename === 'shipped.md') {
90
+ initialContent = '# SHIPPED 🚀\n\n'
91
+ }
92
+ await fs.writeFile(filePath, initialContent + content, 'utf-8')
93
+ }
94
+
95
+ await this._updateSessionMetadata(sessionPath, {
96
+ lastActivity: new Date().toISOString(),
97
+ })
98
+ }
99
+
100
+ /**
101
+ * Read logs from current session
102
+ *
103
+ * @param {string} projectId - The project identifier
104
+ * @param {string} filename - Source filename (default: context.jsonl)
105
+ * @returns {Promise<Array<Object>>} - Array of parsed log entries
106
+ */
107
+ async readCurrentSession(projectId, filename = 'context.jsonl') {
108
+ const sessionPath = await this.getCurrentSession(projectId)
109
+ const filePath = path.join(sessionPath, filename)
110
+
111
+ try {
112
+ const content = await fs.readFile(filePath, 'utf-8')
113
+ return this._parseJsonLines(content)
114
+ } catch {
115
+ return []
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Read logs from a specific date range
121
+ *
122
+ * @param {string} projectId - The project identifier
123
+ * @param {Date} fromDate - Start date
124
+ * @param {Date} toDate - End date (defaults to today)
125
+ * @param {string} filename - Source filename (default: context.jsonl)
126
+ * @returns {Promise<Array<Object>>} - Array of parsed log entries from all sessions in range
127
+ */
128
+ async readSessionRange(projectId, fromDate, toDate = new Date(), filename = 'context.jsonl') {
129
+ const sessions = await pathManager.getSessionsInRange(projectId, fromDate, toDate)
130
+ const allEntries = []
131
+
132
+ for (const session of sessions) {
133
+ const filePath = path.join(session.path, filename)
134
+ try {
135
+ const content = await fs.readFile(filePath, 'utf-8')
136
+ const entries = this._parseJsonLines(content)
137
+
138
+ entries.forEach(entry => {
139
+ entry._sessionDate = session.date
140
+ })
141
+
142
+ allEntries.push(...entries)
143
+ } catch {
144
+ continue
145
+ }
146
+ }
147
+
148
+ return allEntries
149
+ }
150
+
151
+ /**
152
+ * Read markdown content from sessions in date range
153
+ *
154
+ * @param {string} projectId - The project identifier
155
+ * @param {Date} fromDate - Start date
156
+ * @param {Date} toDate - End date
157
+ * @param {string} filename - Source filename (e.g., 'shipped.md')
158
+ * @returns {Promise<string>} - Concatenated content from all sessions
159
+ */
160
+ async readMarkdownRange(projectId, fromDate, toDate, filename) {
161
+ const sessions = await pathManager.getSessionsInRange(projectId, fromDate, toDate)
162
+ const allContent = []
163
+
164
+ for (const session of sessions) {
165
+ const filePath = path.join(session.path, filename)
166
+ try {
167
+ const content = await fs.readFile(filePath, 'utf-8')
168
+ if (content.trim()) {
169
+ allContent.push(`## Session: ${session.year}-${session.month}-${session.day}\n\n${content}`)
170
+ }
171
+ } catch {
172
+ continue
173
+ }
174
+ }
175
+
176
+ return allContent.join('\n---\n\n')
177
+ }
178
+
179
+ /**
180
+ * Get recent logs (last N days)
181
+ *
182
+ * @param {string} projectId - The project identifier
183
+ * @param {number} days - Number of days to look back
184
+ * @param {string} filename - Source filename
185
+ * @returns {Promise<Array<Object>>} - Recent log entries
186
+ */
187
+ async getRecentLogs(projectId, days = 7, filename = 'context.jsonl') {
188
+ const toDate = new Date()
189
+ const fromDate = new Date()
190
+ fromDate.setDate(fromDate.getDate() - days)
191
+
192
+ return await this.readSessionRange(projectId, fromDate, toDate, filename)
193
+ }
194
+
195
+ /**
196
+ * Get session statistics
197
+ *
198
+ * @param {string} projectId - The project identifier
199
+ * @param {Date} fromDate - Start date
200
+ * @param {Date} toDate - End date
201
+ * @returns {Promise<Object>} - Statistics object
202
+ */
203
+ async getSessionStats(projectId, fromDate, toDate) {
204
+ const sessions = await pathManager.getSessionsInRange(projectId, fromDate, toDate)
205
+
206
+ let totalEntries = 0
207
+ let totalShips = 0
208
+ let activeDays = 0
209
+
210
+ for (const session of sessions) {
211
+ const metadata = await this._getSessionMetadata(session.path)
212
+ if (metadata) {
213
+ totalEntries += metadata.entryCount || 0
214
+ totalShips += metadata.shipCount || 0
215
+ if (metadata.entryCount > 0) {
216
+ activeDays++
217
+ }
218
+ }
219
+ }
220
+
221
+ return {
222
+ totalSessions: sessions.length,
223
+ activeDays,
224
+ totalEntries,
225
+ totalShips,
226
+ averageEntriesPerDay: activeDays > 0 ? Math.round(totalEntries / activeDays) : 0,
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Migrate legacy single-file logs to session structure
232
+ *
233
+ * @param {string} projectId - The project identifier
234
+ * @param {string} legacyFilePath - Path to legacy log file
235
+ * @param {string} sessionFilename - Target filename in sessions
236
+ * @returns {Promise<Object>} - Migration result
237
+ */
238
+ async migrateLegacyLogs(projectId, legacyFilePath, sessionFilename) {
239
+ try {
240
+ const content = await fs.readFile(legacyFilePath, 'utf-8')
241
+
242
+ if (sessionFilename.endsWith('.jsonl')) {
243
+ return await this._migrateLegacyJsonl(projectId, content, sessionFilename)
244
+ } else {
245
+ return await this._migrateLegacyMarkdown(projectId, content, sessionFilename)
246
+ }
247
+ } catch (error) {
248
+ return {
249
+ success: false,
250
+ message: `Migration failed: ${error.message}`,
251
+ entriesMigrated: 0,
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Migrate legacy JSONL file
258
+ * @private
259
+ */
260
+ async _migrateLegacyJsonl(projectId, content, sessionFilename) {
261
+ const entries = this._parseJsonLines(content)
262
+ const sessionGroups = new Map()
263
+
264
+ for (const entry of entries) {
265
+ const date = new Date(entry.timestamp || entry.data?.timestamp || Date.now())
266
+ const dateKey = this._getDateKey(date)
267
+
268
+ if (!sessionGroups.has(dateKey)) {
269
+ sessionGroups.set(dateKey, [])
270
+ }
271
+ sessionGroups.get(dateKey).push(entry)
272
+ }
273
+
274
+ let migratedCount = 0
275
+ for (const [dateKey, groupEntries] of sessionGroups) {
276
+ const [year, month, day] = dateKey.split('-')
277
+ const date = new Date(year, month - 1, day)
278
+ const sessionPath = await pathManager.ensureSessionPath(projectId, date)
279
+ const filePath = path.join(sessionPath, sessionFilename)
280
+
281
+ const content = groupEntries.map(e => JSON.stringify(e)).join('\n') + '\n'
282
+ await fs.writeFile(filePath, content, 'utf-8')
283
+
284
+ migratedCount += groupEntries.length
285
+
286
+ await this._ensureSessionMetadata(sessionPath)
287
+ await this._updateSessionMetadata(sessionPath, {
288
+ entryCount: groupEntries.length,
289
+ migrated: true,
290
+ migratedAt: new Date().toISOString(),
291
+ })
292
+ }
293
+
294
+ return {
295
+ success: true,
296
+ message: `Migrated ${migratedCount} entries to ${sessionGroups.size} sessions`,
297
+ entriesMigrated: migratedCount,
298
+ sessionsCreated: sessionGroups.size,
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Migrate legacy markdown file
304
+ * @private
305
+ */
306
+ async _migrateLegacyMarkdown(projectId, content, sessionFilename) {
307
+ const sessionPath = await this.getCurrentSession(projectId)
308
+ const filePath = path.join(sessionPath, sessionFilename)
309
+
310
+ await fs.writeFile(filePath, content, 'utf-8')
311
+
312
+ await this._updateSessionMetadata(sessionPath, {
313
+ migrated: true,
314
+ migratedAt: new Date().toISOString(),
315
+ })
316
+
317
+ return {
318
+ success: true,
319
+ message: 'Migrated markdown content to current session',
320
+ entriesMigrated: 1,
321
+ sessionsCreated: 1,
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Get session metadata
327
+ * @private
328
+ */
329
+ async _getSessionMetadata(sessionPath) {
330
+ const metadataPath = path.join(sessionPath, 'session-meta.json')
331
+
332
+ if (this.sessionMetadataCache.has(sessionPath)) {
333
+ return this.sessionMetadataCache.get(sessionPath)
334
+ }
335
+
336
+ try {
337
+ const content = await fs.readFile(metadataPath, 'utf-8')
338
+ const metadata = JSON.parse(content)
339
+ this.sessionMetadataCache.set(sessionPath, metadata)
340
+ return metadata
341
+ } catch {
342
+ return null
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Ensure session metadata exists
348
+ * @private
349
+ */
350
+ async _ensureSessionMetadata(sessionPath) {
351
+ const metadataPath = path.join(sessionPath, 'session-meta.json')
352
+
353
+ try {
354
+ await fs.access(metadataPath)
355
+ } catch {
356
+ // Create initial metadata
357
+ const metadata = {
358
+ created: new Date().toISOString(),
359
+ lastActivity: new Date().toISOString(),
360
+ entryCount: 0,
361
+ shipCount: 0,
362
+ version: VERSION,
363
+ }
364
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8')
365
+ this.sessionMetadataCache.set(sessionPath, metadata)
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Update session metadata
371
+ * @private
372
+ */
373
+ async _updateSessionMetadata(sessionPath, updates) {
374
+ const metadata = await this._getSessionMetadata(sessionPath) || {}
375
+ Object.assign(metadata, updates)
376
+
377
+ const metadataPath = path.join(sessionPath, 'session-meta.json')
378
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8')
379
+
380
+ this.sessionMetadataCache.set(sessionPath, metadata)
381
+ }
382
+
383
+ /**
384
+ * Parse JSONL content
385
+ * @private
386
+ */
387
+ _parseJsonLines(content) {
388
+ const lines = content.split('\n').filter(line => line.trim())
389
+ const entries = []
390
+
391
+ for (const line of lines) {
392
+ try {
393
+ entries.push(JSON.parse(line))
394
+ } catch {
395
+ }
396
+ }
397
+
398
+ return entries
399
+ }
400
+
401
+ /**
402
+ * Get line count from file
403
+ * @private
404
+ */
405
+ async _getFileLineCount(filePath) {
406
+ try {
407
+ const content = await fs.readFile(filePath, 'utf-8')
408
+ return content.split('\n').filter(line => line.trim()).length
409
+ } catch {
410
+ return 0
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Get today's date key (YYYY-MM-DD)
416
+ * @private
417
+ */
418
+ _getTodayKey() {
419
+ return this._getDateKey(new Date())
420
+ }
421
+
422
+ /**
423
+ * Get date key for any date (YYYY-MM-DD)
424
+ * @private
425
+ */
426
+ _getDateKey(date) {
427
+ const year = date.getFullYear()
428
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
429
+ const day = date.getDate().toString().padStart(2, '0')
430
+ return `${year}-${month}-${day}`
431
+ }
432
+
433
+ clearCache() {
434
+ this.currentSessionCache.clear()
435
+ this.sessionMetadataCache.clear()
436
+ }
437
+ }
438
+
439
+ module.exports = new SessionManager()
@@ -0,0 +1,107 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ /**
5
+ * Version Manager - Single source of truth for application version
6
+ *
7
+ * Reads version from package.json dynamically to ensure consistency
8
+ * across the entire application.
9
+ *
10
+ * @module version
11
+ */
12
+
13
+ let cachedVersion = null
14
+ let cachedPackageJson = null
15
+
16
+ /**
17
+ * Get the current application version from package.json
18
+ *
19
+ * @returns {string} - Semantic version string (e.g., "0.2.1")
20
+ */
21
+ function getVersion() {
22
+ if (cachedVersion) {
23
+ return cachedVersion
24
+ }
25
+
26
+ try {
27
+ const packageJsonPath = path.join(__dirname, '..', 'package.json')
28
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
29
+ cachedVersion = packageJson.version
30
+ cachedPackageJson = packageJson
31
+ return cachedVersion
32
+ } catch (error) {
33
+ console.error('Failed to read version from package.json:', error.message)
34
+ return '0.0.0'
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get the full package.json object
40
+ *
41
+ * @returns {Object} - Package.json contents
42
+ */
43
+ function getPackageInfo() {
44
+ if (!cachedPackageJson) {
45
+ getVersion()
46
+ }
47
+ return cachedPackageJson
48
+ }
49
+
50
+ /**
51
+ * Compare two semantic version strings
52
+ *
53
+ * @param {string} v1 - First version (e.g., "0.2.1")
54
+ * @param {string} v2 - Second version (e.g., "0.2.0")
55
+ * @returns {number} - Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2
56
+ */
57
+ function compareVersions(v1, v2) {
58
+ const parts1 = v1.split('.').map(Number)
59
+ const parts2 = v2.split('.').map(Number)
60
+
61
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
62
+ const num1 = parts1[i] || 0
63
+ const num2 = parts2[i] || 0
64
+
65
+ if (num1 > num2) return 1
66
+ if (num1 < num2) return -1
67
+ }
68
+
69
+ return 0
70
+ }
71
+
72
+ /**
73
+ * Check if a config version is compatible with current version
74
+ *
75
+ * @param {string} configVersion - Version from config file
76
+ * @returns {boolean} - True if compatible
77
+ */
78
+ function isCompatible(configVersion) {
79
+ const current = getVersion()
80
+ const [currentMajor, currentMinor] = current.split('.').map(Number)
81
+ const [configMajor, configMinor] = configVersion.split('.').map(Number)
82
+
83
+ return currentMajor === configMajor && currentMinor === configMinor
84
+ }
85
+
86
+ /**
87
+ * Check if migration is needed based on version comparison
88
+ *
89
+ * @param {string} fromVersion - Current config version
90
+ * @param {string} toVersion - Target version (defaults to current)
91
+ * @returns {boolean} - True if migration needed
92
+ */
93
+ function needsMigration(fromVersion, toVersion = null) {
94
+ const target = toVersion || getVersion()
95
+ return compareVersions(fromVersion, target) < 0
96
+ }
97
+
98
+ const VERSION = getVersion()
99
+
100
+ module.exports = {
101
+ VERSION,
102
+ getVersion,
103
+ getPackageInfo,
104
+ compareVersions,
105
+ isCompatible,
106
+ needsMigration,
107
+ }