prjct-cli 0.10.14 → 0.11.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.
- package/CHANGELOG.md +19 -0
- package/bin/dev.js +217 -0
- package/bin/prjct +10 -0
- package/bin/serve.js +78 -0
- package/core/bus/index.js +322 -0
- package/core/command-registry.js +65 -0
- package/core/domain/snapshot-manager.js +375 -0
- package/core/plugin/hooks.js +313 -0
- package/core/plugin/index.js +52 -0
- package/core/plugin/loader.js +331 -0
- package/core/plugin/registry.js +325 -0
- package/core/plugins/webhook.js +143 -0
- package/core/session/index.js +449 -0
- package/core/session/metrics.js +293 -0
- package/package.json +28 -4
- package/packages/shared/dist/index.d.ts +615 -0
- package/packages/shared/dist/index.js +204 -0
- package/packages/shared/package.json +29 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/schemas.ts +124 -0
- package/packages/shared/src/types.ts +187 -0
- package/packages/shared/src/utils.ts +148 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/web/README.md +36 -0
- package/packages/web/app/api/claude/sessions/route.ts +44 -0
- package/packages/web/app/api/claude/status/route.ts +34 -0
- package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
- package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
- package/packages/web/app/api/projects/[id]/route.ts +29 -0
- package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
- package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
- package/packages/web/app/api/projects/route.ts +16 -0
- package/packages/web/app/api/sessions/history/route.ts +122 -0
- package/packages/web/app/api/stats/route.ts +38 -0
- package/packages/web/app/error.tsx +34 -0
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +155 -0
- package/packages/web/app/layout.tsx +43 -0
- package/packages/web/app/loading.tsx +7 -0
- package/packages/web/app/not-found.tsx +25 -0
- package/packages/web/app/page.tsx +227 -0
- package/packages/web/app/project/[id]/error.tsx +41 -0
- package/packages/web/app/project/[id]/loading.tsx +9 -0
- package/packages/web/app/project/[id]/not-found.tsx +27 -0
- package/packages/web/app/project/[id]/page.tsx +253 -0
- package/packages/web/app/project/[id]/stats/page.tsx +447 -0
- package/packages/web/app/sessions/page.tsx +165 -0
- package/packages/web/app/settings/page.tsx +150 -0
- package/packages/web/components/AppSidebar.tsx +113 -0
- package/packages/web/components/CommandButton.tsx +39 -0
- package/packages/web/components/ConnectionStatus.tsx +29 -0
- package/packages/web/components/Logo.tsx +65 -0
- package/packages/web/components/MarkdownContent.tsx +123 -0
- package/packages/web/components/ProjectAvatar.tsx +54 -0
- package/packages/web/components/TechStackBadges.tsx +20 -0
- package/packages/web/components/TerminalTab.tsx +84 -0
- package/packages/web/components/TerminalTabs.tsx +210 -0
- package/packages/web/components/charts/SessionsChart.tsx +172 -0
- package/packages/web/components/providers.tsx +45 -0
- package/packages/web/components/ui/alert-dialog.tsx +157 -0
- package/packages/web/components/ui/badge.tsx +46 -0
- package/packages/web/components/ui/button.tsx +60 -0
- package/packages/web/components/ui/card.tsx +92 -0
- package/packages/web/components/ui/chart.tsx +385 -0
- package/packages/web/components/ui/dropdown-menu.tsx +257 -0
- package/packages/web/components/ui/scroll-area.tsx +58 -0
- package/packages/web/components/ui/sheet.tsx +139 -0
- package/packages/web/components/ui/tabs.tsx +66 -0
- package/packages/web/components/ui/tooltip.tsx +61 -0
- package/packages/web/components.json +22 -0
- package/packages/web/context/TerminalContext.tsx +45 -0
- package/packages/web/context/TerminalTabsContext.tsx +136 -0
- package/packages/web/eslint.config.mjs +18 -0
- package/packages/web/hooks/useClaudeTerminal.ts +375 -0
- package/packages/web/hooks/useProjectStats.ts +38 -0
- package/packages/web/hooks/useProjects.ts +73 -0
- package/packages/web/hooks/useStats.ts +28 -0
- package/packages/web/lib/format.ts +23 -0
- package/packages/web/lib/parse-prjct-files.ts +1122 -0
- package/packages/web/lib/projects.ts +452 -0
- package/packages/web/lib/pty.ts +101 -0
- package/packages/web/lib/query-config.ts +44 -0
- package/packages/web/lib/utils.ts +6 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +7 -0
- package/packages/web/package.json +53 -0
- package/packages/web/postcss.config.mjs +7 -0
- package/packages/web/public/file.svg +1 -0
- package/packages/web/public/globe.svg +1 -0
- package/packages/web/public/next.svg +1 -0
- package/packages/web/public/vercel.svg +1 -0
- package/packages/web/public/window.svg +1 -0
- package/packages/web/server.ts +262 -0
- package/packages/web/tsconfig.json +34 -0
- package/templates/commands/done.md +176 -54
- package/templates/commands/history.md +176 -0
- package/templates/commands/init.md +28 -1
- package/templates/commands/now.md +191 -9
- package/templates/commands/pause.md +176 -12
- package/templates/commands/redo.md +142 -0
- package/templates/commands/resume.md +166 -62
- package/templates/commands/serve.md +121 -0
- package/templates/commands/ship.md +45 -1
- package/templates/commands/sync.md +34 -1
- package/templates/commands/undo.md +152 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager - Structured Session Tracking
|
|
3
|
+
*
|
|
4
|
+
* Tracks work sessions with metrics, timeline, and duration.
|
|
5
|
+
* Inspired by OpenCode's session system but simplified.
|
|
6
|
+
*
|
|
7
|
+
* Storage: ~/.prjct-cli/projects/{projectId}/sessions/
|
|
8
|
+
*
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises
|
|
13
|
+
const path = require('path')
|
|
14
|
+
const pathManager = require('../infrastructure/path-manager')
|
|
15
|
+
const configManager = require('../infrastructure/config-manager')
|
|
16
|
+
const { eventBus, emit } = require('../bus')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Session Schema
|
|
20
|
+
* @typedef {Object} Session
|
|
21
|
+
* @property {string} id - Unique session ID (sess_xxxx)
|
|
22
|
+
* @property {string} projectId - Project identifier
|
|
23
|
+
* @property {string} task - Task description
|
|
24
|
+
* @property {'active'|'paused'|'completed'} status - Current status
|
|
25
|
+
* @property {string} startedAt - ISO timestamp when started
|
|
26
|
+
* @property {string} [pausedAt] - ISO timestamp when paused
|
|
27
|
+
* @property {string} [completedAt] - ISO timestamp when completed
|
|
28
|
+
* @property {number} duration - Total duration in seconds
|
|
29
|
+
* @property {Object} metrics - Automatic metrics
|
|
30
|
+
* @property {Array} timeline - Event history
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
class SessionManager {
|
|
34
|
+
constructor(projectPath) {
|
|
35
|
+
this.projectPath = projectPath
|
|
36
|
+
this.projectId = null
|
|
37
|
+
this.sessionDir = null
|
|
38
|
+
this.initialized = false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize session manager for project
|
|
43
|
+
*/
|
|
44
|
+
async initialize() {
|
|
45
|
+
this.projectId = await configManager.getProjectId(this.projectPath)
|
|
46
|
+
if (!this.projectId) {
|
|
47
|
+
throw new Error('No prjct project found. Run /p:init first.')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const globalPath = pathManager.getGlobalProjectPath(this.projectId)
|
|
51
|
+
this.sessionDir = path.join(globalPath, 'sessions')
|
|
52
|
+
|
|
53
|
+
await fs.mkdir(this.sessionDir, { recursive: true })
|
|
54
|
+
this.initialized = true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate unique session ID
|
|
59
|
+
*/
|
|
60
|
+
generateId() {
|
|
61
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
62
|
+
let id = 'sess_'
|
|
63
|
+
for (let i = 0; i < 8; i++) {
|
|
64
|
+
id += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
65
|
+
}
|
|
66
|
+
return id
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get current active session
|
|
71
|
+
* @returns {Promise<Session|null>}
|
|
72
|
+
*/
|
|
73
|
+
async getCurrent() {
|
|
74
|
+
if (!this.initialized) await this.initialize()
|
|
75
|
+
|
|
76
|
+
const currentPath = path.join(this.sessionDir, 'current.json')
|
|
77
|
+
try {
|
|
78
|
+
const content = await fs.readFile(currentPath, 'utf-8')
|
|
79
|
+
return JSON.parse(content)
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a new session
|
|
87
|
+
* @param {string} task - Task description
|
|
88
|
+
* @returns {Promise<Session>}
|
|
89
|
+
*/
|
|
90
|
+
async create(task) {
|
|
91
|
+
if (!this.initialized) await this.initialize()
|
|
92
|
+
|
|
93
|
+
// Check if there's already an active session
|
|
94
|
+
const current = await this.getCurrent()
|
|
95
|
+
if (current && current.status === 'active') {
|
|
96
|
+
throw new Error(`Session already active: "${current.task}". Use /p:done or /p:pause first.`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const now = new Date().toISOString()
|
|
100
|
+
const session = {
|
|
101
|
+
id: this.generateId(),
|
|
102
|
+
projectId: this.projectId,
|
|
103
|
+
task,
|
|
104
|
+
status: 'active',
|
|
105
|
+
startedAt: now,
|
|
106
|
+
pausedAt: null,
|
|
107
|
+
completedAt: null,
|
|
108
|
+
duration: 0,
|
|
109
|
+
metrics: {
|
|
110
|
+
filesChanged: 0,
|
|
111
|
+
linesAdded: 0,
|
|
112
|
+
linesRemoved: 0,
|
|
113
|
+
commits: 0,
|
|
114
|
+
snapshots: []
|
|
115
|
+
},
|
|
116
|
+
timeline: [
|
|
117
|
+
{ type: 'start', at: now }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Save as current session
|
|
122
|
+
await this.saveCurrent(session)
|
|
123
|
+
|
|
124
|
+
// Log to session history
|
|
125
|
+
await this.logEvent('session_started', { sessionId: session.id, task })
|
|
126
|
+
|
|
127
|
+
// Emit event for plugins
|
|
128
|
+
await emit.sessionStarted({
|
|
129
|
+
sessionId: session.id,
|
|
130
|
+
task,
|
|
131
|
+
projectId: this.projectId
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return session
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resume a paused session or continue active session
|
|
139
|
+
* @param {string} [task] - Optional new task (creates new session if provided)
|
|
140
|
+
* @returns {Promise<Session>}
|
|
141
|
+
*/
|
|
142
|
+
async resume(task = null) {
|
|
143
|
+
if (!this.initialized) await this.initialize()
|
|
144
|
+
|
|
145
|
+
const current = await this.getCurrent()
|
|
146
|
+
|
|
147
|
+
// If task provided and different from current, create new session
|
|
148
|
+
if (task && (!current || current.task !== task)) {
|
|
149
|
+
return this.create(task)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If no current session, need a task
|
|
153
|
+
if (!current) {
|
|
154
|
+
if (!task) {
|
|
155
|
+
throw new Error('No active session. Provide a task to start one.')
|
|
156
|
+
}
|
|
157
|
+
return this.create(task)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If already active, just return it
|
|
161
|
+
if (current.status === 'active') {
|
|
162
|
+
return current
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Resume paused session
|
|
166
|
+
const now = new Date().toISOString()
|
|
167
|
+
current.status = 'active'
|
|
168
|
+
current.timeline.push({ type: 'resume', at: now })
|
|
169
|
+
|
|
170
|
+
await this.saveCurrent(current)
|
|
171
|
+
await this.logEvent('session_resumed', { sessionId: current.id })
|
|
172
|
+
|
|
173
|
+
// Emit event for plugins
|
|
174
|
+
await emit.sessionResumed({
|
|
175
|
+
sessionId: current.id,
|
|
176
|
+
task: current.task,
|
|
177
|
+
projectId: this.projectId
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return current
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Pause current session
|
|
185
|
+
* @returns {Promise<Session>}
|
|
186
|
+
*/
|
|
187
|
+
async pause() {
|
|
188
|
+
if (!this.initialized) await this.initialize()
|
|
189
|
+
|
|
190
|
+
const current = await this.getCurrent()
|
|
191
|
+
if (!current) {
|
|
192
|
+
throw new Error('No active session to pause.')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (current.status === 'paused') {
|
|
196
|
+
return current // Already paused
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const now = new Date().toISOString()
|
|
200
|
+
current.status = 'paused'
|
|
201
|
+
current.pausedAt = now
|
|
202
|
+
current.duration = this.calculateDuration(current)
|
|
203
|
+
current.timeline.push({ type: 'pause', at: now })
|
|
204
|
+
|
|
205
|
+
await this.saveCurrent(current)
|
|
206
|
+
await this.logEvent('session_paused', { sessionId: current.id, duration: current.duration })
|
|
207
|
+
|
|
208
|
+
// Emit event for plugins
|
|
209
|
+
await emit.sessionPaused({
|
|
210
|
+
sessionId: current.id,
|
|
211
|
+
task: current.task,
|
|
212
|
+
duration: current.duration,
|
|
213
|
+
projectId: this.projectId
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return current
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Complete current session
|
|
221
|
+
* @returns {Promise<Session>}
|
|
222
|
+
*/
|
|
223
|
+
async complete() {
|
|
224
|
+
if (!this.initialized) await this.initialize()
|
|
225
|
+
|
|
226
|
+
const current = await this.getCurrent()
|
|
227
|
+
if (!current) {
|
|
228
|
+
throw new Error('No active session to complete.')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const now = new Date().toISOString()
|
|
232
|
+
current.status = 'completed'
|
|
233
|
+
current.completedAt = now
|
|
234
|
+
current.duration = this.calculateDuration(current)
|
|
235
|
+
current.metrics = await this.calculateMetrics(current)
|
|
236
|
+
current.timeline.push({ type: 'complete', at: now })
|
|
237
|
+
|
|
238
|
+
// Archive session
|
|
239
|
+
await this.archive(current)
|
|
240
|
+
|
|
241
|
+
// Clear current
|
|
242
|
+
await this.clearCurrent()
|
|
243
|
+
|
|
244
|
+
// Log completion
|
|
245
|
+
await this.logEvent('session_completed', {
|
|
246
|
+
sessionId: current.id,
|
|
247
|
+
task: current.task,
|
|
248
|
+
duration: current.duration,
|
|
249
|
+
metrics: current.metrics
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// Emit event for plugins
|
|
253
|
+
await emit.sessionCompleted({
|
|
254
|
+
sessionId: current.id,
|
|
255
|
+
task: current.task,
|
|
256
|
+
duration: current.duration,
|
|
257
|
+
metrics: current.metrics,
|
|
258
|
+
projectId: this.projectId
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
return current
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Calculate total duration in seconds
|
|
266
|
+
* @param {Session} session
|
|
267
|
+
* @returns {number}
|
|
268
|
+
*/
|
|
269
|
+
calculateDuration(session) {
|
|
270
|
+
let totalMs = 0
|
|
271
|
+
let lastStart = null
|
|
272
|
+
|
|
273
|
+
for (const event of session.timeline) {
|
|
274
|
+
if (event.type === 'start' || event.type === 'resume') {
|
|
275
|
+
lastStart = new Date(event.at)
|
|
276
|
+
} else if (event.type === 'pause' || event.type === 'complete') {
|
|
277
|
+
if (lastStart) {
|
|
278
|
+
totalMs += new Date(event.at) - lastStart
|
|
279
|
+
lastStart = null
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If still active, count from last start to now
|
|
285
|
+
if (lastStart && session.status === 'active') {
|
|
286
|
+
totalMs += Date.now() - lastStart
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return Math.round(totalMs / 1000)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Calculate metrics for session
|
|
294
|
+
* @param {Session} session
|
|
295
|
+
* @returns {Promise<Object>}
|
|
296
|
+
*/
|
|
297
|
+
async calculateMetrics(session) {
|
|
298
|
+
const metrics = { ...session.metrics }
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const { exec } = require('child_process')
|
|
302
|
+
const { promisify } = require('util')
|
|
303
|
+
const execAsync = promisify(exec)
|
|
304
|
+
|
|
305
|
+
// Get git stats since session start
|
|
306
|
+
const since = session.startedAt.split('T')[0]
|
|
307
|
+
|
|
308
|
+
// Count commits
|
|
309
|
+
const { stdout: commitCount } = await execAsync(
|
|
310
|
+
`git rev-list --count --since="${since}" HEAD 2>/dev/null || echo "0"`,
|
|
311
|
+
{ cwd: this.projectPath }
|
|
312
|
+
)
|
|
313
|
+
metrics.commits = parseInt(commitCount.trim()) || 0
|
|
314
|
+
|
|
315
|
+
// Get diff stats
|
|
316
|
+
const { stdout: diffStat } = await execAsync(
|
|
317
|
+
`git diff --stat HEAD~${Math.max(metrics.commits, 1)} 2>/dev/null || echo ""`,
|
|
318
|
+
{ cwd: this.projectPath }
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
// Parse diff stats
|
|
322
|
+
const lines = diffStat.split('\n')
|
|
323
|
+
const summaryLine = lines[lines.length - 2] || ''
|
|
324
|
+
const match = summaryLine.match(/(\d+) files? changed(?:, (\d+) insertions?)?(?:, (\d+) deletions?)?/)
|
|
325
|
+
|
|
326
|
+
if (match) {
|
|
327
|
+
metrics.filesChanged = parseInt(match[1]) || 0
|
|
328
|
+
metrics.linesAdded = parseInt(match[2]) || 0
|
|
329
|
+
metrics.linesRemoved = parseInt(match[3]) || 0
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// Keep existing metrics if git fails
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return metrics
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Save current session
|
|
340
|
+
* @param {Session} session
|
|
341
|
+
*/
|
|
342
|
+
async saveCurrent(session) {
|
|
343
|
+
const currentPath = path.join(this.sessionDir, 'current.json')
|
|
344
|
+
await fs.writeFile(currentPath, JSON.stringify(session, null, 2))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Clear current session file
|
|
349
|
+
*/
|
|
350
|
+
async clearCurrent() {
|
|
351
|
+
const currentPath = path.join(this.sessionDir, 'current.json')
|
|
352
|
+
try {
|
|
353
|
+
await fs.unlink(currentPath)
|
|
354
|
+
} catch {
|
|
355
|
+
// File might not exist
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Archive completed session
|
|
361
|
+
* @param {Session} session
|
|
362
|
+
*/
|
|
363
|
+
async archive(session) {
|
|
364
|
+
const date = new Date(session.completedAt)
|
|
365
|
+
const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
|
|
366
|
+
const archiveDir = path.join(this.sessionDir, 'archive', yearMonth)
|
|
367
|
+
|
|
368
|
+
await fs.mkdir(archiveDir, { recursive: true })
|
|
369
|
+
|
|
370
|
+
const archivePath = path.join(archiveDir, `${session.id}.json`)
|
|
371
|
+
await fs.writeFile(archivePath, JSON.stringify(session, null, 2))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get session history
|
|
376
|
+
* @param {number} limit - Max sessions to return
|
|
377
|
+
* @returns {Promise<Session[]>}
|
|
378
|
+
*/
|
|
379
|
+
async getHistory(limit = 10) {
|
|
380
|
+
if (!this.initialized) await this.initialize()
|
|
381
|
+
|
|
382
|
+
const sessions = []
|
|
383
|
+
const archiveDir = path.join(this.sessionDir, 'archive')
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const months = await fs.readdir(archiveDir)
|
|
387
|
+
const sortedMonths = months.sort().reverse()
|
|
388
|
+
|
|
389
|
+
for (const month of sortedMonths) {
|
|
390
|
+
if (sessions.length >= limit) break
|
|
391
|
+
|
|
392
|
+
const monthDir = path.join(archiveDir, month)
|
|
393
|
+
const files = await fs.readdir(monthDir)
|
|
394
|
+
|
|
395
|
+
for (const file of files.sort().reverse()) {
|
|
396
|
+
if (sessions.length >= limit) break
|
|
397
|
+
if (!file.endsWith('.json')) continue
|
|
398
|
+
|
|
399
|
+
const content = await fs.readFile(path.join(monthDir, file), 'utf-8')
|
|
400
|
+
sessions.push(JSON.parse(content))
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
// Archive might not exist yet
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return sessions
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Log event to memory
|
|
412
|
+
* @param {string} action
|
|
413
|
+
* @param {Object} data
|
|
414
|
+
*/
|
|
415
|
+
async logEvent(action, data) {
|
|
416
|
+
const globalPath = pathManager.getGlobalProjectPath(this.projectId)
|
|
417
|
+
const memoryPath = path.join(globalPath, 'memory', 'context.jsonl')
|
|
418
|
+
|
|
419
|
+
const entry = JSON.stringify({
|
|
420
|
+
timestamp: new Date().toISOString(),
|
|
421
|
+
action,
|
|
422
|
+
...data
|
|
423
|
+
}) + '\n'
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
await fs.appendFile(memoryPath, entry)
|
|
427
|
+
} catch {
|
|
428
|
+
// Memory file might not exist
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Format duration as human readable
|
|
434
|
+
* @param {number} seconds
|
|
435
|
+
* @returns {string}
|
|
436
|
+
*/
|
|
437
|
+
static formatDuration(seconds) {
|
|
438
|
+
if (seconds < 60) return `${seconds}s`
|
|
439
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`
|
|
440
|
+
|
|
441
|
+
const hours = Math.floor(seconds / 3600)
|
|
442
|
+
const minutes = Math.round((seconds % 3600) / 60)
|
|
443
|
+
|
|
444
|
+
if (minutes === 0) return `${hours}h`
|
|
445
|
+
return `${hours}h ${minutes}m`
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = SessionManager
|