@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.5

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 (42) hide show
  1. package/bin/copilot-session-viewer +2 -2
  2. package/dist/server.min.js +99 -0
  3. package/package.json +5 -17
  4. package/public/vendor/marked.umd.min.js +8 -0
  5. package/public/vendor/purify.min.js +3 -0
  6. package/public/vendor/vue-virtual-scroller.css +1 -0
  7. package/public/vendor/vue-virtual-scroller.min.js +2 -0
  8. package/public/vendor/vue.global.prod.min.js +19 -0
  9. package/views/session-vue.ejs +5 -5
  10. package/views/time-analyze.ejs +2 -2
  11. package/lib/parsers/README.md +0 -239
  12. package/lib/parsers/base-parser.js +0 -53
  13. package/lib/parsers/claude-parser.js +0 -181
  14. package/lib/parsers/copilot-parser.js +0 -143
  15. package/lib/parsers/index.js +0 -15
  16. package/lib/parsers/parser-factory.js +0 -77
  17. package/lib/parsers/pi-mono-parser.js +0 -119
  18. package/lib/parsers/vscode-parser.js +0 -591
  19. package/server.js +0 -29
  20. package/src/app.js +0 -129
  21. package/src/config/index.js +0 -27
  22. package/src/controllers/insightController.js +0 -136
  23. package/src/controllers/sessionController.js +0 -449
  24. package/src/controllers/tagController.js +0 -113
  25. package/src/controllers/uploadController.js +0 -648
  26. package/src/middleware/common.js +0 -67
  27. package/src/middleware/rateLimiting.js +0 -62
  28. package/src/models/Session.js +0 -146
  29. package/src/routes/api.js +0 -11
  30. package/src/routes/insights.js +0 -12
  31. package/src/routes/pages.js +0 -12
  32. package/src/routes/uploads.js +0 -14
  33. package/src/schemas/event.schema.js +0 -73
  34. package/src/services/eventNormalizer.js +0 -291
  35. package/src/services/insightService.js +0 -535
  36. package/src/services/sessionRepository.js +0 -1092
  37. package/src/services/sessionService.js +0 -1919
  38. package/src/services/tagService.js +0 -205
  39. package/src/telemetry.js +0 -152
  40. package/src/utils/fileUtils.js +0 -305
  41. package/src/utils/helpers.js +0 -45
  42. package/src/utils/processManager.js +0 -85
@@ -1,535 +0,0 @@
1
- /**
2
- * Insight Generation Service
3
- * Handles session insight report generation with atomic operations
4
- */
5
-
6
- const fs = require('fs').promises;
7
- const fsSync = require('fs');
8
- const path = require('path');
9
- const os = require('os');
10
- const { spawn } = require('child_process');
11
- const config = require('../config');
12
- const processManager = require('../utils/processManager');
13
-
14
- class InsightService {
15
- constructor() {
16
- // No longer needs session directories - paths are passed directly
17
- }
18
-
19
- /**
20
- * Get CLI tool configuration based on session source
21
- * @private
22
- */
23
- _getToolConfig(source, sessionPath) {
24
- const configs = {
25
- copilot: {
26
- name: 'Copilot',
27
- cli: 'copilot',
28
- args: (tmpDir, prompt) => ['--config-dir', tmpDir, '--yolo', '-p', prompt],
29
- cwd: sessionPath
30
- },
31
- vscode: {
32
- name: 'Copilot',
33
- cli: 'copilot',
34
- args: (tmpDir, prompt) => ['--config-dir', tmpDir, '--yolo', '-p', prompt],
35
- cwd: sessionPath
36
- },
37
- claude: {
38
- name: 'Claude Code',
39
- cli: 'claude',
40
- args: (_tmpDir, prompt) => ['-p', prompt, '--dangerously-skip-permissions'],
41
- cwd: sessionPath
42
- },
43
- 'pi-mono': {
44
- name: 'Pi',
45
- cli: 'pi',
46
- args: (_tmpDir, prompt) => ['-p', prompt],
47
- cwd: sessionPath
48
- }
49
- };
50
-
51
- return configs[source] || configs.copilot; // fallback to copilot
52
- }
53
-
54
- /**
55
- * Generate or retrieve insight report
56
- * @param {string} sessionId - Session ID
57
- * @param {string} sessionPath - Full path to session directory
58
- * @param {string} source - Session source: 'copilot', 'claude', or 'pi-mono'
59
- * @param {boolean} forceRegenerate - Force new generation
60
- * @returns {Promise<Object>} Insight status and report
61
- */
62
- async generateInsight(sessionId, sessionPath, source = 'copilot', forceRegenerate = false) {
63
- // Use per-session insight file to avoid collisions in shared directories
64
- const insightFile = path.join(sessionPath, `${sessionId}.agent-review.md`);
65
- const lockFile = path.join(sessionPath, `${sessionId}.agent-review.md.lock`);
66
-
67
- // Determine events file location based on directory structure
68
- // Try standard events.jsonl first, then <sessionId>.jsonl (for file-type sessions),
69
- // finally *_<sessionId>.jsonl (for Pi-Mono timestamped sessions)
70
- let eventsFile = path.join(sessionPath, 'events.jsonl');
71
- try {
72
- await fs.access(eventsFile);
73
- } catch {
74
- // Try <sessionId>.jsonl (common for Claude file-type sessions)
75
- try {
76
- eventsFile = path.join(sessionPath, `${sessionId}.jsonl`);
77
- await fs.access(eventsFile);
78
- } catch {
79
- // Try *_<sessionId>.jsonl (Pi-Mono format: YYYY-MM-DDTHH-mm-ss-SSSZ_<uuid>.jsonl)
80
- const entries = await fs.readdir(sessionPath);
81
- const piFile = entries.find(f => f.endsWith(`_${sessionId}.jsonl`));
82
- if (piFile) {
83
- eventsFile = path.join(sessionPath, piFile);
84
- }
85
- }
86
- }
87
-
88
- const toolConfig = this._getToolConfig(source, sessionPath);
89
- const toolName = toolConfig.name;
90
-
91
- // Check if complete insight exists
92
- if (!forceRegenerate) {
93
- try {
94
- const report = await fs.readFile(insightFile, 'utf-8');
95
- const stats = await fs.stat(insightFile);
96
- return {
97
- status: 'completed',
98
- report,
99
- generatedAt: stats.mtime
100
- };
101
- } catch (_err) {
102
- // File doesn't exist, continue to generation
103
- }
104
- }
105
-
106
- // Check if generation is already in progress (atomic check)
107
- try {
108
- // Try to create lock file exclusively (fails if exists)
109
- await fs.writeFile(lockFile, JSON.stringify({
110
- sessionId,
111
- startTime: new Date().toISOString(),
112
- pid: process.pid
113
- }), { flag: 'wx' });
114
- } catch (err) {
115
- if (err.code === 'EEXIST') {
116
- // Another process is generating, check if it's stale
117
- try {
118
- const _lockData = JSON.parse(await fs.readFile(lockFile, 'utf-8'));
119
- const lockStats = await fs.stat(lockFile);
120
- const ageMs = Date.now() - lockStats.mtime.getTime();
121
-
122
- if (ageMs < config.INSIGHT_TIMEOUT_MS) {
123
- // Still valid, return generating status
124
- return {
125
- status: 'generating',
126
- report: `# Generating ${toolName} Insight...\n\nAnother request is currently generating this insight. Please wait.`,
127
- startedAt: lockStats.birthtime,
128
- lastUpdate: lockStats.mtime,
129
- ageMs: Date.now() - lockStats.birthtime.getTime()
130
- };
131
- }
132
-
133
- // Stale lock, remove it
134
- console.log(`⚠️ Removing stale lock file (${Math.floor(ageMs/1000)}s old)`);
135
- await fs.unlink(lockFile);
136
-
137
- // Retry lock creation
138
- await fs.writeFile(lockFile, JSON.stringify({
139
- sessionId,
140
- startTime: new Date().toISOString(),
141
- pid: process.pid
142
- }), { flag: 'wx' });
143
- } catch (_retryErr) {
144
- throw new Error('Failed to acquire lock for insight generation', { cause: _retryErr });
145
- }
146
- } else {
147
- throw err;
148
- }
149
- }
150
-
151
- // Check if events file exists
152
- try {
153
- await fs.access(eventsFile);
154
- } catch (_err) {
155
- await fs.unlink(lockFile);
156
- throw new Error('Events file not found', { cause: _err });
157
- }
158
-
159
- // Clean up old files if force regenerate
160
- if (forceRegenerate) {
161
- try {
162
- await fs.unlink(insightFile);
163
- } catch (_err) {
164
- // File might not exist
165
- }
166
- }
167
-
168
- // Start generation
169
- await this._spawnAnalysisProcess(sessionPath, eventsFile, insightFile, lockFile, toolConfig);
170
-
171
- return {
172
- status: 'generating',
173
- report: `# Generating ${toolName} Insight...\n\nAnalysis in progress. Please wait.`,
174
- startedAt: new Date()
175
- };
176
- }
177
-
178
- /**
179
- * Spawn analysis process safely (no shell)
180
- * @private
181
- */
182
- async _spawnAnalysisProcess(sessionPath, eventsFile, insightFile, lockFile, toolConfig) {
183
- const sessionId = path.basename(sessionPath); // Extract session ID from path
184
- const tmpDir = path.join(os.tmpdir(), `agent-review-${sessionId}-${Date.now()}`);
185
- await fs.mkdir(tmpDir, { recursive: true});
186
-
187
- const prompt = this._buildPrompt(insightFile, eventsFile);
188
- const outputFile = path.join(sessionPath, `${sessionId}.agent-review.md.tmp`);
189
-
190
- // Spawn analysis tool directly (no shell)
191
- const cliPath = toolConfig.cli;
192
- const args = toolConfig.args(tmpDir, prompt);
193
-
194
- console.log(`🤖 Starting ${toolConfig.name} analysis: ${cliPath} ${args.slice(0, 2).join(' ')}...`);
195
- console.log(`📋 Args count: ${args.length}, prompt length: ${prompt.length} chars`);
196
-
197
- // Use system PATH - CLI should be in the user's PATH
198
- const analysisProcess = spawn(cliPath, args, {
199
- env: { ...process.env },
200
- cwd: sessionPath,
201
- stdio: ['pipe', 'pipe', 'pipe']
202
- });
203
-
204
- // Register for cleanup
205
- processManager.register(analysisProcess, { name: `insight-${sessionId}` });
206
-
207
- // Pipe events file to stdin (for tools that read from stdin like copilot)
208
- // Claude Code and Pi read files directly, so they don't need stdin
209
- if (toolConfig.cli === 'copilot') {
210
- const eventsStream = fsSync.createReadStream(eventsFile);
211
- // Handle EPIPE: if process exits before stdin is fully written, suppress the error
212
- analysisProcess.stdin.on('error', (err) => {
213
- if (err.code === 'EPIPE') {
214
- eventsStream.destroy();
215
- } else {
216
- console.error('❌ stdin error:', err);
217
- }
218
- });
219
- eventsStream.pipe(analysisProcess.stdin);
220
- } else {
221
- // Close stdin for tools that don't need it
222
- analysisProcess.stdin.end();
223
- }
224
-
225
- // Capture output
226
- const outputStream = fsSync.createWriteStream(outputFile);
227
- analysisProcess.stdout.pipe(outputStream);
228
-
229
- // Capture stderr with size limit
230
- const stderrChunks = [];
231
- let stderrSize = 0;
232
- const MAX_STDERR = 64 * 1024; // 64KB cap
233
-
234
- analysisProcess.stderr.on('data', (data) => {
235
- if (stderrSize < MAX_STDERR) {
236
- stderrChunks.push(data);
237
- stderrSize += data.length;
238
- }
239
- });
240
-
241
- analysisProcess.on('close', async (code) => {
242
- try {
243
- outputStream.end();
244
-
245
- const stderr = Buffer.concat(stderrChunks).toString('utf-8').slice(0, MAX_STDERR);
246
- console.log(`📋 ${toolConfig.name} process exited with code ${code}`);
247
- if (stderr) {
248
- console.log(`📋 ${toolConfig.name} stderr:`, stderr.substring(0, 500));
249
- }
250
-
251
- if (code !== 0) {
252
- console.error(`❌ ${toolConfig.name} CLI failed (code ${code}):`, stderr);
253
- await fs.writeFile(insightFile,
254
- `# ❌ Generation Failed\n\nExit code: ${code}\n\n\`\`\`\n${stderr || '(no error output)'}\n\`\`\`\n`,
255
- 'utf-8'
256
- );
257
- } else {
258
- // The agent was told to write directly to insightFile.
259
- // Check if it did; if not, fall back to cleaning stdout from .tmp.
260
- let hasDirectOutput = false;
261
- try {
262
- const direct = await fs.readFile(insightFile, 'utf-8');
263
- if (direct && direct.trim().length > 50) {
264
- hasDirectOutput = true;
265
- console.log(`✅ Insight generated for session ${sessionId} (agent wrote directly)`);
266
- }
267
- } catch (_e) { /* file doesn't exist */ }
268
-
269
- if (!hasDirectOutput) {
270
- let report = await fs.readFile(outputFile, 'utf-8');
271
- report = this._cleanReport(report);
272
- await fs.writeFile(insightFile, report, 'utf-8');
273
- console.log(`✅ Insight generated for session ${sessionId} (cleaned from stdout)`);
274
- }
275
- }
276
-
277
- // Cleanup
278
- await fs.unlink(outputFile).catch(() => {});
279
- await fs.unlink(lockFile).catch(() => {});
280
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
281
- // Clean up sub-agent working directory (safety net)
282
- await fs.rm(path.join(sessionPath, '.output'), { recursive: true, force: true }).catch(() => {});
283
- } catch (err) {
284
- console.error('❌ Error finalizing insight:', err);
285
- await fs.unlink(lockFile).catch(() => {});
286
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
287
- }
288
- });
289
-
290
- analysisProcess.on('error', async (err) => {
291
- console.error(`❌ Failed to spawn ${toolConfig.name}:`, err);
292
- await fs.unlink(lockFile).catch(() => {});
293
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
294
- });
295
- }
296
-
297
- /**
298
- * Build insight generation prompt
299
- * @private
300
- */
301
- _buildPrompt(outputPath, eventsFile) {
302
- const sessionDir = path.dirname(outputPath);
303
- const eventsFilename = path.basename(eventsFile);
304
- const workDir = `${sessionDir}/.output`;
305
-
306
- // For Pi-Mono: emphasize analyzing ONLY the specified file
307
- const fileInstruction = eventsFilename.includes('_')
308
- ? `\n**IMPORTANT**: This directory may contain multiple .jsonl files. You MUST analyze ONLY the file named \`${eventsFilename}\`. Do NOT read or analyze any other .jsonl files in this directory.\n`
309
- : '';
310
-
311
- return `You are an expert AI agent evaluator. The current working directory is an AI coding agent session folder. It contains the raw session data from an agent run.
312
- ${fileInstruction}
313
- **Step 1 — Discover session files.** Run \`ls -la\` to see what's available, then note which files exist:
314
- - \`${eventsFilename}\` — the main session event log (JSONL, one JSON event per line). Primary data source. May be large. **This is the ONLY events file you should analyze.**
315
- - \`plan.md\` — the agent's plan (if it exists).
316
- - \`workspace.yaml\` — workspace configuration (if it exists).
317
- - Any other relevant files.
318
-
319
- **Step 2 — Spawn 3 sub-agents for parallel analysis.** First create the working directory: \`mkdir -p ${workDir}\`. Then use the Task tool to launch ALL of the following sub-agents simultaneously (in a single message with multiple Task tool calls). Each sub-agent should:
320
- - Read \`${eventsFilename}\` from \`${sessionDir}\` (use Bash: \`cat\`, \`jq\`, or \`python3\` to parse) **— ONLY this file, ignore others**
321
- - Read other session files as needed
322
- - Write its findings to an intermediate file in \`${workDir}/\`
323
- - Return a summary of its findings
324
-
325
- Sub-agents to spawn:
326
-
327
- 1. **Tool Usage Analyst** — Analyze tool selection quality, redundant/wasted calls, error handling patterns, tool call counts and durations. Write findings to \`${workDir}/tools.md\`.
328
-
329
- 2. **Workflow Strategist** — Evaluate planning quality, sequencing logic, sub-agent decomposition, backtracking/wandering patterns. Write findings to \`${workDir}/workflow.md\`.
330
-
331
- 3. **Performance Profiler** — Calculate time distribution (LLM thinking vs tool execution vs idle gaps), identify bottlenecks, assess concurrency usage. Write findings to \`${workDir}/performance.md\`.
332
-
333
- **CRITICAL: You MUST wait for ALL 3 sub-agents to complete before proceeding to Step 3.** Do NOT move on until every sub-agent has returned its results. After launching them, poll or wait for their completion.
334
-
335
- **Step 3 — Synthesize the final report.** Once all sub-agents are done:
336
- 1. Read the intermediate files from \`${workDir}/\`
337
- 2. Synthesize a unified report
338
- 3. Write the final report to \`${outputPath}\`
339
- 4. Clean up by removing the entire working directory: \`rm -rf ${workDir}\`
340
-
341
- The final report must be a markdown file with these sections:
342
-
343
- ## 🎯 Effectiveness Score: X/100
344
- One-line verdict on how well the agent fulfilled the user's intent.
345
-
346
- ## 🔧 Tool Usage Analysis
347
- - **Tool selection quality**: Did the agent pick the right tools? Any unnecessary or redundant tool calls? (e.g. repeated Read calls on the same file, Grep when Glob would suffice, excessive Bash calls)
348
- - **Error handling**: How did the agent recover from tool errors? Did it retry blindly or adapt?
349
- - **Efficiency**: Tool call count vs. actual value delivered. Identify wasted calls.
350
-
351
- ## 🔄 Workflow & Strategy
352
- - **Planning quality**: Did the agent have a coherent strategy, or did it wander? Look for signs of backtracking, repeated attempts, or lack of direction.
353
- - **Sub-agent usage** (if any): Were sub-agents spawned effectively? Was the decomposition logical? Any sub-agents that were unnecessary or too narrow/broad?
354
- - **Sequencing**: Were operations done in a logical order, or was there unnecessary back-and-forth?
355
-
356
- ## ⚡ Performance
357
- - **Time distribution**: Where did the wall-clock time actually go? (LLM thinking vs. tool execution vs. idle gaps)
358
- - **Bottlenecks**: Identify the biggest time sinks and whether they were avoidable.
359
- - **Concurrency**: Did the agent parallelize where it could? Missed opportunities?
360
-
361
- ## 💡 Top 3 Improvements
362
- Specific, actionable recommendations to make this agent workflow better. Examples:
363
- - "Batch the 12 sequential Read calls into a single Glob + targeted Reads"
364
- - "The agent re-read file X 4 times — cache the content across turns"
365
- - "Sub-agent 'code-explorer' ran for 45s but its output was barely used — consider inlining"
366
-
367
- Be brutally honest. Generic advice like "add error handling" is useless — always tie recommendations to specific evidence from the session data.
368
-
369
- IMPORTANT CONSTRAINTS:
370
- - Be precise and concise. Every sentence must carry data or actionable insight — no filler, no fluff.
371
- - The entire report MUST be under 3000 characters (including markdown formatting). Cut ruthlessly if needed.`;
372
- }
373
-
374
- /**
375
- * Clean report output — strip copilot CLI working logs, thinking blocks, meta-commentary.
376
- * The copilot CLI dumps tool call logs (● Tool name, $ command, └ output) to stdout
377
- * alongside the actual report. We need to extract just the final markdown.
378
- * @private
379
- */
380
- _cleanReport(report) {
381
- // Remove <thinking>...</thinking> blocks
382
- report = report.replace(/<thinking>[\s\S]*?<\/thinking>/g, '');
383
-
384
- // Remove meta-commentary lines
385
- report = report.replace(/^(Let me analyze|I'll analyze|Analyzing|Here's my analysis of|I need the session data).*$/gm, '');
386
-
387
- // Strategy: try to find the final markdown report section.
388
- // The copilot CLI log has patterns like:
389
- // ● Tool name (tool call header)
390
- // $ command (bash command)
391
- // └ N lines... (output summary)
392
- // (+N lines) (file write indicator)
393
- // The actual report starts with a markdown heading like "## 🎯"
394
-
395
- // Look for the last occurrence of the report structure (## 🎯 Effectiveness Score)
396
- // which indicates the final output vs. intermediate attempts
397
- const reportStartPattern = /^## 🎯\s*Effectiveness Score/m;
398
- const matches = [...report.matchAll(new RegExp(reportStartPattern.source, 'gm'))];
399
-
400
- if (matches.length > 0) {
401
- // Take from the last match onward (in case the agent generated it multiple times)
402
- const lastMatch = matches[matches.length - 1];
403
- report = report.slice(lastMatch.index);
404
- } else {
405
- // Try broader: find any markdown heading (## with emoji or #)
406
- const anyHeadingMatch = report.match(/^(## [🎯🔧🔄⚡💡#])/mu);
407
- if (anyHeadingMatch) {
408
- report = report.slice(anyHeadingMatch.index);
409
- } else {
410
- // Last resort: strip known copilot CLI log patterns line by line
411
- const lines = report.split('\n');
412
- const cleanedLines = [];
413
- let skipBlock = false;
414
-
415
- for (const line of lines) {
416
- // Skip copilot CLI working log patterns
417
- if (/^● /.test(line)) { skipBlock = true; continue; }
418
- if (/^ {2}\$ /.test(line)) { skipBlock = true; continue; }
419
- if (/^ {2}└ /.test(line)) { skipBlock = false; continue; }
420
- if (/^\(\+\d+ lines?\)/.test(line)) { continue; }
421
- if (/^ {2}└ \d+ lines/.test(line)) { continue; }
422
- // Skip "Asked user" and "User responded" log lines
423
- if (/^● Asked user:/.test(line)) { skipBlock = true; continue; }
424
- if (/^ {2}└ User responded:/.test(line)) { skipBlock = false; continue; }
425
-
426
- // If we hit a non-log line, stop skipping
427
- if (skipBlock && /\S/.test(line) && !/^ {2}/.test(line)) {
428
- skipBlock = false;
429
- }
430
- if (skipBlock) continue;
431
-
432
- cleanedLines.push(line);
433
- }
434
- report = cleanedLines.join('\n');
435
- }
436
- }
437
-
438
- // Trim excessive whitespace
439
- report = report.replace(/\n{3,}/g, '\n\n').trim();
440
-
441
- return report;
442
- }
443
-
444
- /**
445
- * Get insight status
446
- */
447
- /**
448
- * Get insight status
449
- * @param {string} sessionId - Session ID
450
- * @param {string} sessionPath - Full path to session directory
451
- * @param {string} source - Session source
452
- * @returns {Promise<Object>} Status object
453
- */
454
- async getInsightStatus(sessionId, sessionPath, _source = 'copilot') {
455
- return await this._getStatusForSource(sessionId, sessionPath);
456
- }
457
-
458
- /**
459
- * Get status for a specific session directory
460
- * @private
461
- */
462
- async _getStatusForSource(sessionId, sessionPath) {
463
- const insightFile = path.join(sessionPath, `${sessionId}.agent-review.md`);
464
- const lockFile = path.join(sessionPath, `${sessionId}.agent-review.md.lock`);
465
- const tmpFile = path.join(sessionPath, `${sessionId}.agent-review.md.tmp`);
466
-
467
- try {
468
- const report = await fs.readFile(insightFile, 'utf-8');
469
- const stats = await fs.stat(insightFile);
470
- return {
471
- status: 'completed',
472
- report,
473
- generatedAt: stats.mtime
474
- };
475
- } catch (_err) {
476
- // Check if generation is in progress
477
- try {
478
- await fs.access(lockFile);
479
- const stats = await fs.stat(lockFile);
480
- const ageMs = Date.now() - stats.birthtime.getTime();
481
-
482
- // Read live working log from tmp file
483
- let log = null;
484
- try {
485
- log = await fs.readFile(tmpFile, 'utf-8');
486
- } catch (_tmpErr) {
487
- // tmp file may not exist yet
488
- }
489
-
490
- if (ageMs >= config.INSIGHT_TIMEOUT_MS) {
491
- return {
492
- status: 'timeout',
493
- log,
494
- startedAt: stats.birthtime,
495
- lastUpdate: stats.mtime,
496
- ageMs
497
- };
498
- }
499
-
500
- return {
501
- status: 'generating',
502
- log,
503
- startedAt: stats.birthtime,
504
- lastUpdate: stats.mtime,
505
- ageMs
506
- };
507
- } catch (_lockErr) {
508
- return { status: 'not_started' };
509
- }
510
- }
511
- }
512
-
513
- /**
514
- * Delete insight report
515
- * @param {string} sessionId - Session ID
516
- * @param {string} sessionPath - Full path to session directory
517
- * @param {string} source - Session source
518
- * @returns {Promise<Object>} Result object
519
- */
520
- async deleteInsight(sessionId, sessionPath, _source = 'copilot') {
521
- const insightFile = path.join(sessionPath, `${sessionId}.agent-review.md`);
522
-
523
- try {
524
- await fs.unlink(insightFile);
525
- return { success: true };
526
- } catch (err) {
527
- if (err.code === 'ENOENT') {
528
- return { success: true, message: 'Insight file not found' };
529
- }
530
- throw err;
531
- }
532
- }
533
- }
534
-
535
- module.exports = InsightService;