prjct-cli 0.6.0 → 0.7.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 (83) hide show
  1. package/CHANGELOG.md +67 -6
  2. package/CLAUDE.md +442 -36
  3. package/README.md +47 -54
  4. package/bin/prjct +170 -240
  5. package/core/agentic/command-executor.js +113 -0
  6. package/core/agentic/context-builder.js +85 -0
  7. package/core/agentic/prompt-builder.js +86 -0
  8. package/core/agentic/template-loader.js +104 -0
  9. package/core/agentic/tool-registry.js +117 -0
  10. package/core/command-registry.js +106 -62
  11. package/core/commands.js +2030 -2211
  12. package/core/domain/agent-generator.js +118 -0
  13. package/core/domain/analyzer.js +211 -0
  14. package/core/domain/architect-session.js +300 -0
  15. package/core/{agents → infrastructure/agents}/claude-agent.js +16 -13
  16. package/core/{author-detector.js → infrastructure/author-detector.js} +3 -1
  17. package/core/{capability-installer.js → infrastructure/capability-installer.js} +3 -6
  18. package/core/{command-installer.js → infrastructure/command-installer.js} +4 -2
  19. package/core/{config-manager.js → infrastructure/config-manager.js} +4 -4
  20. package/core/{editors-config.js → infrastructure/editors-config.js} +2 -10
  21. package/core/{migrator.js → infrastructure/migrator.js} +34 -19
  22. package/core/{path-manager.js → infrastructure/path-manager.js} +20 -44
  23. package/core/{session-manager.js → infrastructure/session-manager.js} +45 -105
  24. package/core/{update-checker.js → infrastructure/update-checker.js} +67 -67
  25. package/core/{animations-simple.js → utils/animations.js} +3 -23
  26. package/core/utils/date-helper.js +238 -0
  27. package/core/utils/file-helper.js +327 -0
  28. package/core/utils/jsonl-helper.js +206 -0
  29. package/core/{project-capabilities.js → utils/project-capabilities.js} +21 -22
  30. package/core/utils/session-helper.js +277 -0
  31. package/core/{version.js → utils/version.js} +1 -1
  32. package/package.json +4 -12
  33. package/templates/agents/AGENTS.md +101 -27
  34. package/templates/analysis/analyze.md +84 -0
  35. package/templates/commands/analyze.md +9 -2
  36. package/templates/commands/bug.md +79 -0
  37. package/templates/commands/build.md +5 -2
  38. package/templates/commands/cleanup.md +5 -2
  39. package/templates/commands/design.md +5 -2
  40. package/templates/commands/done.md +4 -2
  41. package/templates/commands/feature.md +113 -0
  42. package/templates/commands/fix.md +41 -10
  43. package/templates/commands/git.md +7 -2
  44. package/templates/commands/help.md +2 -2
  45. package/templates/commands/idea.md +14 -5
  46. package/templates/commands/init.md +62 -7
  47. package/templates/commands/next.md +4 -2
  48. package/templates/commands/now.md +4 -2
  49. package/templates/commands/progress.md +27 -5
  50. package/templates/commands/recap.md +39 -10
  51. package/templates/commands/roadmap.md +19 -5
  52. package/templates/commands/ship.md +118 -16
  53. package/templates/commands/status.md +4 -2
  54. package/templates/commands/sync.md +19 -15
  55. package/templates/commands/task.md +4 -2
  56. package/templates/commands/test.md +5 -2
  57. package/templates/commands/workflow.md +4 -2
  58. package/core/agent-generator.js +0 -525
  59. package/core/analyzer.js +0 -600
  60. package/core/animations.js +0 -277
  61. package/core/ascii-graphics.js +0 -433
  62. package/core/git-integration.js +0 -401
  63. package/core/task-schema.js +0 -342
  64. package/core/workflow-engine.js +0 -213
  65. package/core/workflow-prompts.js +0 -192
  66. package/core/workflow-rules.js +0 -147
  67. package/scripts/post-install.js +0 -121
  68. package/scripts/preuninstall.js +0 -94
  69. package/scripts/verify-installation.sh +0 -158
  70. package/templates/agents/be.template.md +0 -27
  71. package/templates/agents/coordinator.template.md +0 -34
  72. package/templates/agents/data.template.md +0 -27
  73. package/templates/agents/devops.template.md +0 -27
  74. package/templates/agents/fe.template.md +0 -27
  75. package/templates/agents/mobile.template.md +0 -27
  76. package/templates/agents/qa.template.md +0 -27
  77. package/templates/agents/scribe.template.md +0 -29
  78. package/templates/agents/security.template.md +0 -27
  79. package/templates/agents/ux.template.md +0 -27
  80. package/templates/commands/context.md +0 -36
  81. package/templates/commands/stuck.md +0 -36
  82. package/templates/examples/natural-language-examples.md +0 -532
  83. /package/core/{agent-detector.js → infrastructure/agent-detector.js} +0 -0
@@ -20,7 +20,7 @@ class CapabilityInstaller {
20
20
  const { stdout, stderr } = await execAsync(command)
21
21
 
22
22
  const duration = Date.now() - startTime
23
- const durationMin = Math.round(duration / 1000 / 60 * 10) / 10
23
+ const durationMin = Math.round((duration / 1000 / 60) * 10) / 10
24
24
 
25
25
  return {
26
26
  success: true,
@@ -160,10 +160,7 @@ export default defineConfig({
160
160
  },
161
161
  }
162
162
 
163
- await fs.writeFile(
164
- path.join(projectPath, 'jsdoc.json'),
165
- JSON.stringify(config, null, 2),
166
- )
163
+ await fs.writeFile(path.join(projectPath, 'jsdoc.json'), JSON.stringify(config, null, 2))
167
164
 
168
165
  // Add docs script
169
166
  pkg.scripts = pkg.scripts || {}
@@ -180,7 +177,7 @@ export default defineConfig({
180
177
  * Verify installation succeeded
181
178
  */
182
179
  async verify(capability, projectPath) {
183
- const caps = require('./project-capabilities')
180
+ const caps = require('../utils/project-capabilities')
184
181
  const detected = await caps.detect(projectPath)
185
182
 
186
183
  return detected[capability] === true
@@ -39,7 +39,7 @@ class CommandInstaller {
39
39
  async getCommandFiles() {
40
40
  try {
41
41
  const files = await fs.readdir(this.templatesDir)
42
- return files.filter(f => f.endsWith('.md'))
42
+ return files.filter((f) => f.endsWith('.md'))
43
43
  } catch (error) {
44
44
  // Fallback to core commands if template directory not accessible
45
45
  return [
@@ -175,7 +175,9 @@ class CommandInstaller {
175
175
  try {
176
176
  await fs.access(this.claudeCommandsPath)
177
177
  const files = await fs.readdir(this.claudeCommandsPath)
178
- const installedCommands = files.filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''))
178
+ const installedCommands = files
179
+ .filter((f) => f.endsWith('.md'))
180
+ .map((f) => f.replace('.md', ''))
179
181
 
180
182
  return {
181
183
  installed: installedCommands.length > 0,
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs').promises
2
2
  const pathManager = require('./path-manager')
3
- const { VERSION } = require('./version')
3
+ const { VERSION } = require('../utils/version')
4
4
 
5
5
  /**
6
6
  * ConfigManager - Manages prjct.config.json files
@@ -234,7 +234,7 @@ class ConfigManager {
234
234
  const globalConfig = await this.readGlobalConfig(projectId)
235
235
  if (!globalConfig || !globalConfig.authors) return null
236
236
 
237
- return globalConfig.authors.find(a => a.github === githubUsername) || null
237
+ return globalConfig.authors.find((a) => a.github === githubUsername) || null
238
238
  }
239
239
 
240
240
  /**
@@ -248,7 +248,7 @@ class ConfigManager {
248
248
  async addAuthor(projectId, author) {
249
249
  const globalConfig = await this.ensureGlobalConfig(projectId)
250
250
 
251
- const exists = globalConfig.authors.some(a => a.github === author.github)
251
+ const exists = globalConfig.authors.some((a) => a.github === author.github)
252
252
  if (exists) return
253
253
 
254
254
  const now = new Date().toISOString()
@@ -276,7 +276,7 @@ class ConfigManager {
276
276
  const globalConfig = await this.readGlobalConfig(projectId)
277
277
  if (!globalConfig || !globalConfig.authors) return
278
278
 
279
- const author = globalConfig.authors.find(a => a.github === githubUsername)
279
+ const author = globalConfig.authors.find((a) => a.github === githubUsername)
280
280
  if (author) {
281
281
  author.lastActivity = new Date().toISOString()
282
282
  globalConfig.lastSync = author.lastActivity
@@ -64,11 +64,7 @@ class EditorsConfig {
64
64
  path: claudePath,
65
65
  }
66
66
 
67
- await fs.writeFile(
68
- this.configFile,
69
- JSON.stringify(config, null, 2),
70
- 'utf-8',
71
- )
67
+ await fs.writeFile(this.configFile, JSON.stringify(config, null, 2), 'utf-8')
72
68
 
73
69
  return true
74
70
  } catch (error) {
@@ -111,11 +107,7 @@ class EditorsConfig {
111
107
  config.version = version
112
108
  config.lastInstall = new Date().toISOString()
113
109
 
114
- await fs.writeFile(
115
- this.configFile,
116
- JSON.stringify(config, null, 2),
117
- 'utf-8',
118
- )
110
+ await fs.writeFile(this.configFile, JSON.stringify(config, null, 2), 'utf-8')
119
111
 
120
112
  return true
121
113
  } catch (error) {
@@ -64,8 +64,12 @@ class Migrator {
64
64
 
65
65
  const globalConfig = await configManager.readGlobalConfig(projectId)
66
66
  if (globalConfig && globalConfig.authors && globalConfig.authors.length > 0) {
67
- const needsCleanup = localConfig.authors || localConfig.author ||
68
- localConfig.version || localConfig.created || localConfig.lastSync
67
+ const needsCleanup =
68
+ localConfig.authors ||
69
+ localConfig.author ||
70
+ localConfig.version ||
71
+ localConfig.created ||
72
+ localConfig.lastSync
69
73
 
70
74
  if (needsCleanup) {
71
75
  delete localConfig.authors
@@ -182,7 +186,11 @@ class Migrator {
182
186
  return { layer: 'planning', filename }
183
187
  }
184
188
 
185
- if (filename === 'memory.jsonl' || filename === 'context.jsonl' || filename === 'decisions.jsonl') {
189
+ if (
190
+ filename === 'memory.jsonl' ||
191
+ filename === 'context.jsonl' ||
192
+ filename === 'decisions.jsonl'
193
+ ) {
186
194
  return { layer: 'memory', filename }
187
195
  }
188
196
 
@@ -317,9 +325,7 @@ class Migrator {
317
325
  const layerPath = path.join(legacyPath, layer)
318
326
  try {
319
327
  await fs.rm(layerPath, { recursive: true, force: true })
320
- } catch {
321
-
322
- }
328
+ } catch {}
323
329
  }
324
330
  }
325
331
 
@@ -510,10 +516,20 @@ class Migrator {
510
516
  if (deepScan) {
511
517
  searchPaths = [os.homedir()]
512
518
  } else {
513
- const commonDirs = ['Projects', 'Documents', 'Developer', 'Code', 'dev', 'workspace', 'repos', 'src', 'Apps']
519
+ const commonDirs = [
520
+ 'Projects',
521
+ 'Documents',
522
+ 'Developer',
523
+ 'Code',
524
+ 'dev',
525
+ 'workspace',
526
+ 'repos',
527
+ 'src',
528
+ 'Apps',
529
+ ]
514
530
  searchPaths = commonDirs
515
- .map(dir => path.join(os.homedir(), dir))
516
- .filter(dirPath => {
531
+ .map((dir) => path.join(os.homedir(), dir))
532
+ .filter((dirPath) => {
517
533
  try {
518
534
  fs.accessSync(dirPath)
519
535
  return true
@@ -540,13 +556,13 @@ class Migrator {
540
556
  return skipDirs.includes(dirName) || (dirName.startsWith('.') && dirName !== '.prjct')
541
557
  }
542
558
 
543
- const searchDirectory = async(dirPath, depth = 0) => {
559
+ const searchDirectory = async (dirPath, depth = 0) => {
544
560
  if (depth > 10) return
545
561
 
546
562
  try {
547
563
  const entries = await fs.readdir(dirPath, { withFileTypes: true })
548
564
 
549
- if (entries.some(entry => entry.name === '.prjct' && entry.isDirectory())) {
565
+ if (entries.some((entry) => entry.name === '.prjct' && entry.isDirectory())) {
550
566
  projectDirs.push(dirPath)
551
567
  return // Don't search subdirectories if we found a project
552
568
  }
@@ -557,9 +573,7 @@ class Migrator {
557
573
  await searchDirectory(subPath, depth + 1)
558
574
  }
559
575
  }
560
- } catch (error) {
561
-
562
- }
576
+ } catch (error) {}
563
577
  }
564
578
 
565
579
  for (const searchPath of searchPaths) {
@@ -637,7 +651,8 @@ class Migrator {
637
651
 
638
652
  if (status.status === 'migrated' || status.status === 'new') {
639
653
  projectInfo.result = 'skipped'
640
- projectInfo.reason = status.status === 'migrated' ? 'Already migrated' : 'Not initialized'
654
+ projectInfo.reason =
655
+ status.status === 'migrated' ? 'Already migrated' : 'Not initialized'
641
656
  summary.alreadyMigrated++
642
657
  } else if (status.needsMigration) {
643
658
  if (interactive && onProgress) {
@@ -749,8 +764,8 @@ class Migrator {
749
764
  if (summary.successfullyMigrated > 0) {
750
765
  lines.push('✅ Successfully Migrated:')
751
766
  summary.projects
752
- .filter(p => p.result === 'success')
753
- .forEach(project => {
767
+ .filter((p) => p.result === 'success')
768
+ .forEach((project) => {
754
769
  lines.push(` • ${project.name}`)
755
770
  lines.push(` Files: ${project.filesCopied} | ID: ${project.projectId}`)
756
771
  })
@@ -759,9 +774,9 @@ class Migrator {
759
774
 
760
775
  if (summary.errors.length > 0) {
761
776
  lines.push('❌ Errors:')
762
- summary.errors.forEach(error => {
777
+ summary.errors.forEach((error) => {
763
778
  lines.push(` • ${error.project}`)
764
- error.issues.forEach(issue => lines.push(` - ${issue}`))
779
+ error.issues.forEach((issue) => lines.push(` - ${issue}`))
765
780
  })
766
781
  lines.push('')
767
782
  }
@@ -2,6 +2,8 @@ const fs = require('fs').promises
2
2
  const path = require('path')
3
3
  const crypto = require('crypto')
4
4
  const os = require('os')
5
+ const dateHelper = require('../utils/date-helper')
6
+ const fileHelper = require('../utils/file-helper')
5
7
 
6
8
  /**
7
9
  * PathManager - Manages project paths between local and global storage
@@ -90,13 +92,8 @@ class PathManager {
90
92
  * @returns {Promise<boolean>} - True if legacy directory exists
91
93
  */
92
94
  async hasLegacyStructure(projectPath) {
93
- try {
94
- const legacyPath = this.getLegacyPrjctPath(projectPath)
95
- await fs.access(legacyPath)
96
- return true
97
- } catch {
98
- return false
99
- }
95
+ const legacyPath = this.getLegacyPrjctPath(projectPath)
96
+ return await fileHelper.dirExists(legacyPath)
100
97
  }
101
98
 
102
99
  /**
@@ -106,13 +103,8 @@ class PathManager {
106
103
  * @returns {Promise<boolean>} - True if config exists
107
104
  */
108
105
  async hasConfig(projectPath) {
109
- try {
110
- const configPath = this.getLocalConfigPath(projectPath)
111
- await fs.access(configPath)
112
- return true
113
- } catch {
114
- return false
115
- }
106
+ const configPath = this.getLocalConfigPath(projectPath)
107
+ return await fileHelper.fileExists(configPath)
116
108
  }
117
109
 
118
110
  /**
@@ -122,9 +114,9 @@ class PathManager {
122
114
  * @returns {Promise<void>}
123
115
  */
124
116
  async ensureGlobalStructure() {
125
- await fs.mkdir(this.globalBaseDir, { recursive: true })
126
- await fs.mkdir(this.globalProjectsDir, { recursive: true })
127
- await fs.mkdir(this.globalConfigDir, { recursive: true })
117
+ await fileHelper.ensureDir(this.globalBaseDir)
118
+ await fileHelper.ensureDir(this.globalProjectsDir)
119
+ await fileHelper.ensureDir(this.globalConfigDir)
128
120
  }
129
121
 
130
122
  /**
@@ -142,12 +134,11 @@ class PathManager {
142
134
  const layers = ['core', 'progress', 'planning', 'analysis', 'memory']
143
135
 
144
136
  for (const layer of layers) {
145
- await fs.mkdir(path.join(projectPath, layer), { recursive: true })
137
+ await fileHelper.ensureDir(path.join(projectPath, layer))
146
138
  }
147
139
 
148
- await fs.mkdir(path.join(projectPath, 'planning', 'tasks'), { recursive: true })
149
-
150
- await fs.mkdir(path.join(projectPath, 'sessions'), { recursive: true })
140
+ await fileHelper.ensureDir(path.join(projectPath, 'planning', 'tasks'))
141
+ await fileHelper.ensureDir(path.join(projectPath, 'sessions'))
151
142
 
152
143
  return projectPath
153
144
  }
@@ -161,17 +152,9 @@ class PathManager {
161
152
  * @returns {string} - Path to session directory
162
153
  */
163
154
  getSessionPath(projectId, date = new Date()) {
164
- const year = date.getFullYear().toString()
165
- const month = (date.getMonth() + 1).toString().padStart(2, '0')
166
- const day = date.getDate().toString().padStart(2, '0')
167
-
168
- return path.join(
169
- this.getGlobalProjectPath(projectId),
170
- 'sessions',
171
- year,
172
- month,
173
- day,
174
- )
155
+ const { year, month, day } = dateHelper.getYearMonthDay(date)
156
+
157
+ return path.join(this.getGlobalProjectPath(projectId), 'sessions', year, month, day)
175
158
  }
176
159
 
177
160
  /**
@@ -193,7 +176,7 @@ class PathManager {
193
176
  */
194
177
  async ensureSessionPath(projectId, date = new Date()) {
195
178
  const sessionPath = this.getSessionPath(projectId, date)
196
- await fs.mkdir(sessionPath, { recursive: true })
179
+ await fileHelper.ensureDir(sessionPath)
197
180
  return sessionPath
198
181
  }
199
182
 
@@ -258,7 +241,7 @@ class PathManager {
258
241
  async getSessionsInRange(projectId, fromDate, toDate = new Date()) {
259
242
  const allSessions = await this.listSessions(projectId)
260
243
 
261
- return allSessions.filter(session => session.date >= fromDate && session.date <= toDate)
244
+ return allSessions.filter((session) => session.date >= fromDate && session.date <= toDate)
262
245
  }
263
246
 
264
247
  /**
@@ -282,9 +265,7 @@ class PathManager {
282
265
  try {
283
266
  await this.ensureGlobalStructure()
284
267
  const entries = await fs.readdir(this.globalProjectsDir, { withFileTypes: true })
285
- return entries
286
- .filter(entry => entry.isDirectory())
287
- .map(entry => entry.name)
268
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
288
269
  } catch {
289
270
  return []
290
271
  }
@@ -297,13 +278,8 @@ class PathManager {
297
278
  * @returns {Promise<boolean>} - True if project exists
298
279
  */
299
280
  async projectExists(projectId) {
300
- try {
301
- const projectPath = this.getGlobalProjectPath(projectId)
302
- await fs.access(projectPath)
303
- return true
304
- } catch {
305
- return false
306
- }
281
+ const projectPath = this.getGlobalProjectPath(projectId)
282
+ return await fileHelper.dirExists(projectPath)
307
283
  }
308
284
 
309
285
  /**
@@ -1,7 +1,10 @@
1
1
  const fs = require('fs').promises
2
2
  const path = require('path')
3
3
  const pathManager = require('./path-manager')
4
- const { VERSION } = require('./version')
4
+ const { VERSION } = require('../utils/version')
5
+ const dateHelper = require('../utils/date-helper')
6
+ const jsonlHelper = require('../utils/jsonl-helper')
7
+ const fileHelper = require('../utils/file-helper')
5
8
 
6
9
  /**
7
10
  * SessionManager - Manages temporal fragmentation of logs and progress data
@@ -54,18 +57,11 @@ class SessionManager {
54
57
  const sessionPath = await this.getCurrentSession(projectId)
55
58
  const filePath = path.join(sessionPath, filename)
56
59
 
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
- }
60
+ await jsonlHelper.appendJsonLine(filePath, entry)
65
61
 
66
62
  await this._updateSessionMetadata(sessionPath, {
67
- lastActivity: new Date().toISOString(),
68
- entryCount: await this._getFileLineCount(filePath),
63
+ lastActivity: dateHelper.getTimestamp(),
64
+ entryCount: await jsonlHelper.countJsonLines(filePath),
69
65
  })
70
66
  }
71
67
 
@@ -81,19 +77,15 @@ class SessionManager {
81
77
  const sessionPath = await this.getCurrentSession(projectId)
82
78
  const filePath = path.join(sessionPath, filename)
83
79
 
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')
80
+ const exists = await fileHelper.fileExists(filePath)
81
+ if (!exists && filename === 'shipped.md') {
82
+ await fileHelper.writeFile(filePath, '# SHIPPED 🚀\n\n' + content)
83
+ } else {
84
+ await fileHelper.appendToFile(filePath, content)
93
85
  }
94
86
 
95
87
  await this._updateSessionMetadata(sessionPath, {
96
- lastActivity: new Date().toISOString(),
88
+ lastActivity: dateHelper.getTimestamp(),
97
89
  })
98
90
  }
99
91
 
@@ -108,12 +100,7 @@ class SessionManager {
108
100
  const sessionPath = await this.getCurrentSession(projectId)
109
101
  const filePath = path.join(sessionPath, filename)
110
102
 
111
- try {
112
- const content = await fs.readFile(filePath, 'utf-8')
113
- return this._parseJsonLines(content)
114
- } catch {
115
- return []
116
- }
103
+ return await jsonlHelper.readJsonLines(filePath)
117
104
  }
118
105
 
119
106
  /**
@@ -131,18 +118,13 @@ class SessionManager {
131
118
 
132
119
  for (const session of sessions) {
133
120
  const filePath = path.join(session.path, filename)
134
- try {
135
- const content = await fs.readFile(filePath, 'utf-8')
136
- const entries = this._parseJsonLines(content)
121
+ const entries = await jsonlHelper.readJsonLines(filePath)
137
122
 
138
- entries.forEach(entry => {
139
- entry._sessionDate = session.date
140
- })
123
+ entries.forEach((entry) => {
124
+ entry._sessionDate = session.date
125
+ })
141
126
 
142
- allEntries.push(...entries)
143
- } catch {
144
- continue
145
- }
127
+ allEntries.push(...entries)
146
128
  }
147
129
 
148
130
  return allEntries
@@ -163,13 +145,12 @@ class SessionManager {
163
145
 
164
146
  for (const session of sessions) {
165
147
  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
148
+ const content = await fileHelper.readFile(filePath, '')
149
+
150
+ if (content.trim()) {
151
+ allContent.push(
152
+ `## Session: ${session.year}-${session.month}-${session.day}\n\n${content}`
153
+ )
173
154
  }
174
155
  }
175
156
 
@@ -186,8 +167,7 @@ class SessionManager {
186
167
  */
187
168
  async getRecentLogs(projectId, days = 7, filename = 'context.jsonl') {
188
169
  const toDate = new Date()
189
- const fromDate = new Date()
190
- fromDate.setDate(fromDate.getDate() - days)
170
+ const fromDate = dateHelper.getDaysAgo(days)
191
171
 
192
172
  return await this.readSessionRange(projectId, fromDate, toDate, filename)
193
173
  }
@@ -237,7 +217,7 @@ class SessionManager {
237
217
  */
238
218
  async migrateLegacyLogs(projectId, legacyFilePath, sessionFilename) {
239
219
  try {
240
- const content = await fs.readFile(legacyFilePath, 'utf-8')
220
+ const content = await fileHelper.readFile(legacyFilePath)
241
221
 
242
222
  if (sessionFilename.endsWith('.jsonl')) {
243
223
  return await this._migrateLegacyJsonl(projectId, content, sessionFilename)
@@ -258,12 +238,12 @@ class SessionManager {
258
238
  * @private
259
239
  */
260
240
  async _migrateLegacyJsonl(projectId, content, sessionFilename) {
261
- const entries = this._parseJsonLines(content)
241
+ const entries = jsonlHelper.parseJsonLines(content)
262
242
  const sessionGroups = new Map()
263
243
 
264
244
  for (const entry of entries) {
265
245
  const date = new Date(entry.timestamp || entry.data?.timestamp || Date.now())
266
- const dateKey = this._getDateKey(date)
246
+ const dateKey = dateHelper.getDateKey(date)
267
247
 
268
248
  if (!sessionGroups.has(dateKey)) {
269
249
  sessionGroups.set(dateKey, [])
@@ -278,8 +258,7 @@ class SessionManager {
278
258
  const sessionPath = await pathManager.ensureSessionPath(projectId, date)
279
259
  const filePath = path.join(sessionPath, sessionFilename)
280
260
 
281
- const content = groupEntries.map(e => JSON.stringify(e)).join('\n') + '\n'
282
- await fs.writeFile(filePath, content, 'utf-8')
261
+ await jsonlHelper.writeJsonLines(filePath, groupEntries)
283
262
 
284
263
  migratedCount += groupEntries.length
285
264
 
@@ -287,7 +266,7 @@ class SessionManager {
287
266
  await this._updateSessionMetadata(sessionPath, {
288
267
  entryCount: groupEntries.length,
289
268
  migrated: true,
290
- migratedAt: new Date().toISOString(),
269
+ migratedAt: dateHelper.getTimestamp(),
291
270
  })
292
271
  }
293
272
 
@@ -307,11 +286,11 @@ class SessionManager {
307
286
  const sessionPath = await this.getCurrentSession(projectId)
308
287
  const filePath = path.join(sessionPath, sessionFilename)
309
288
 
310
- await fs.writeFile(filePath, content, 'utf-8')
289
+ await fileHelper.writeFile(filePath, content)
311
290
 
312
291
  await this._updateSessionMetadata(sessionPath, {
313
292
  migrated: true,
314
- migratedAt: new Date().toISOString(),
293
+ migratedAt: dateHelper.getTimestamp(),
315
294
  })
316
295
 
317
296
  return {
@@ -333,14 +312,11 @@ class SessionManager {
333
312
  return this.sessionMetadataCache.get(sessionPath)
334
313
  }
335
314
 
336
- try {
337
- const content = await fs.readFile(metadataPath, 'utf-8')
338
- const metadata = JSON.parse(content)
315
+ const metadata = await fileHelper.readJson(metadataPath, null)
316
+ if (metadata) {
339
317
  this.sessionMetadataCache.set(sessionPath, metadata)
340
- return metadata
341
- } catch {
342
- return null
343
318
  }
319
+ return metadata
344
320
  }
345
321
 
346
322
  /**
@@ -350,18 +326,16 @@ class SessionManager {
350
326
  async _ensureSessionMetadata(sessionPath) {
351
327
  const metadataPath = path.join(sessionPath, 'session-meta.json')
352
328
 
353
- try {
354
- await fs.access(metadataPath)
355
- } catch {
356
- // Create initial metadata
329
+ const exists = await fileHelper.fileExists(metadataPath)
330
+ if (!exists) {
357
331
  const metadata = {
358
- created: new Date().toISOString(),
359
- lastActivity: new Date().toISOString(),
332
+ created: dateHelper.getTimestamp(),
333
+ lastActivity: dateHelper.getTimestamp(),
360
334
  entryCount: 0,
361
335
  shipCount: 0,
362
336
  version: VERSION,
363
337
  }
364
- await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8')
338
+ await fileHelper.writeJson(metadataPath, metadata)
365
339
  this.sessionMetadataCache.set(sessionPath, metadata)
366
340
  }
367
341
  }
@@ -371,52 +345,21 @@ class SessionManager {
371
345
  * @private
372
346
  */
373
347
  async _updateSessionMetadata(sessionPath, updates) {
374
- const metadata = await this._getSessionMetadata(sessionPath) || {}
348
+ const metadata = (await this._getSessionMetadata(sessionPath)) || {}
375
349
  Object.assign(metadata, updates)
376
350
 
377
351
  const metadataPath = path.join(sessionPath, 'session-meta.json')
378
- await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8')
352
+ await fileHelper.writeJson(metadataPath, metadata)
379
353
 
380
354
  this.sessionMetadataCache.set(sessionPath, metadata)
381
355
  }
382
356
 
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
357
  /**
415
358
  * Get today's date key (YYYY-MM-DD)
416
359
  * @private
417
360
  */
418
361
  _getTodayKey() {
419
- return this._getDateKey(new Date())
362
+ return dateHelper.getTodayKey()
420
363
  }
421
364
 
422
365
  /**
@@ -424,10 +367,7 @@ class SessionManager {
424
367
  * @private
425
368
  */
426
369
  _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}`
370
+ return dateHelper.getDateKey(date)
431
371
  }
432
372
 
433
373
  clearCache() {