@qiaolei81/copilot-session-viewer 0.1.8 → 0.2.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 (56) hide show
  1. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-be-responsive-on-mobile-viewport-1771605454041.json +435 -0
  2. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-display-sessions-if-available-1771605462872.json +435 -0
  3. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-JavaScript-errors-gracefully-1771605463381.json +435 -0
  4. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-session-import-dialog-1771605466264.json +435 -0
  5. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-have-working-infinite-scroll-elements-1771605454038.json +435 -0
  6. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-homepage-with-basic-elements-1771605454001.json +435 -0
  7. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-time-analysis-page-1771605464990.json +1236 -0
  8. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-navigate-to-session-detail-page-1771605472595.json +1177 -0
  9. package/.nyc_output/coverage-e2e-merged.json +1 -0
  10. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-display-session-list-1771605453565.json +435 -0
  11. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-load-homepage-successfully-1771605453552.json +435 -0
  12. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-navigate-to-session-detail-on-click-1771605469317.json +1134 -0
  13. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-show-session-metadata-1771605460581.json +435 -0
  14. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-display-Load-More-Sessions-button-when-there-are-more-sessions-1771605468486.json +435 -0
  15. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-handle-API-errors-gracefully-during-infinite-scroll-1771605482161.json +471 -0
  16. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-hide-Load-More-button-when-no-more-sessions-available-1771605478370.json +471 -0
  17. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-load-additional-sessions-when-Load-More-button-is-clicked-1771605475059.json +471 -0
  18. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-preserve-session-list-state-during-navigation-1771605494575.json +1633 -0
  19. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-show-loading-state-when-Load-More-button-is-clicked-1771605475401.json +471 -0
  20. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-trigger-infinite-scroll-when-scrolling-near-bottom-1771605476949.json +471 -0
  21. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-clear-search-filter-1771605508542.json +1255 -0
  22. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-event-list-1771605505572.json +1156 -0
  23. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-session-metadata-1771605504552.json +701 -0
  24. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-expand-and-collapse-tool-details-1771605515809.json +1182 -0
  25. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-filter-events-by-search-1771605513421.json +1245 -0
  26. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-load-session-detail-page-1771605494974.json +701 -0
  27. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-toggle-content-visibility-1771605550729.json +1177 -0
  28. package/.nyc_output/coverage-unit.json +21 -0
  29. package/.nycrc +29 -0
  30. package/CHANGELOG.md +36 -0
  31. package/README.md +154 -15
  32. package/examples/parser-usage.js +114 -0
  33. package/lib/parsers/README.md +239 -0
  34. package/lib/parsers/base-parser.js +53 -0
  35. package/lib/parsers/claude-parser.js +181 -0
  36. package/lib/parsers/copilot-parser.js +143 -0
  37. package/lib/parsers/index.js +13 -0
  38. package/lib/parsers/parser-factory.js +77 -0
  39. package/lib/parsers/pi-mono-parser.js +119 -0
  40. package/package.json +12 -4
  41. package/server.js +17 -2
  42. package/src/app.js +45 -20
  43. package/src/controllers/insightController.js +44 -8
  44. package/src/controllers/sessionController.js +217 -3
  45. package/src/controllers/uploadController.js +447 -7
  46. package/src/middleware/rateLimiting.js +7 -1
  47. package/src/models/Session.js +26 -0
  48. package/src/schemas/event.schema.js +73 -0
  49. package/src/services/eventNormalizer.js +291 -0
  50. package/src/services/insightService.js +140 -48
  51. package/src/services/sessionRepository.js +584 -49
  52. package/src/services/sessionService.js +1594 -27
  53. package/src/utils/helpers.js +6 -1
  54. package/views/index.ejs +111 -4
  55. package/views/session-vue.ejs +425 -71
  56. package/views/time-analyze.ejs +140 -57
@@ -0,0 +1,291 @@
1
+ /**
2
+ * EventNormalizer - Unified Event Format Transformer
3
+ *
4
+ * Converts tool events from different AI session formats (Copilot, Claude, Pi-Mono)
5
+ * into a single, consistent schema for frontend consumption.
6
+ *
7
+ * Key transformations:
8
+ * - Normalizes tool call structure to UnifiedToolCall schema
9
+ * - Computes consistent status fields ('pending' | 'running' | 'completed' | 'error')
10
+ * - Adds timing metadata (startTime, endTime, duration)
11
+ * - Handles edge cases (orphaned events, missing fields)
12
+ *
13
+ * Usage:
14
+ * const normalizer = new EventNormalizer();
15
+ * const normalizedEvents = normalizer.normalizeEvents(rawEvents, 'copilot');
16
+ */
17
+
18
+ class EventNormalizer {
19
+ /**
20
+ * Normalize all events to unified format
21
+ * @param {Array} events - Raw events from parsers (after matching)
22
+ * @param {string} _source - 'copilot' | 'claude' | 'pi-mono'
23
+ * @returns {Array} - Normalized events
24
+ */
25
+ normalizeEvents(events, _source) {
26
+ if (!Array.isArray(events)) {
27
+ console.warn('[EventNormalizer] normalizeEvents: events is not an array', typeof events);
28
+ return [];
29
+ }
30
+
31
+ return events
32
+ .filter(event => {
33
+ // Filter out Claude tool_result wrappers (marked by sessionService._matchClaudeToolResults)
34
+ if (event._isToolResultWrapper) {
35
+ return false;
36
+ }
37
+ return true;
38
+ })
39
+ .map(event => this.normalizeEvent(event, _source));
40
+ }
41
+
42
+ /**
43
+ * Normalize a single event
44
+ * @param {Object} event - Raw event
45
+ * @param {string} source - Source format
46
+ * @returns {Object} - Normalized event
47
+ */
48
+ normalizeEvent(event, source) {
49
+ if (!event || typeof event !== 'object') {
50
+ console.warn('[EventNormalizer] normalizeEvent: invalid event', event);
51
+ return event;
52
+ }
53
+
54
+ // Handle assistant messages with tools
55
+ if (this._isAssistantMessage(event)) {
56
+ return this._normalizeAssistantMessage(event, source);
57
+ }
58
+
59
+ // Handle timeline events (tool.execution_start/complete, subagent events)
60
+ if (this._isTimelineEvent(event)) {
61
+ return this._normalizeTimelineEvent(event, source);
62
+ }
63
+
64
+ // Pass through other events unchanged
65
+ return event;
66
+ }
67
+
68
+ /**
69
+ * Check if event is an assistant message with tools (needs normalization)
70
+ * @private
71
+ */
72
+ _isAssistantMessage(event) {
73
+ // Check if event has tools array (works for all sources)
74
+ if (event.data?.tools && Array.isArray(event.data.tools) && event.data.tools.length > 0) {
75
+ return true;
76
+ }
77
+ // Fallback: check specific types (Copilot/Claude legacy)
78
+ return event.type === 'assistant.message' || event.type === 'assistant' || event.type === 'user.message' || event.type === 'user';
79
+ }
80
+
81
+ /**
82
+ * Check if event is a timeline event (tool/subagent events)
83
+ * @private
84
+ */
85
+ _isTimelineEvent(event) {
86
+ return event.type?.startsWith('tool.') || event.type?.startsWith('subagent.');
87
+ }
88
+
89
+ /**
90
+ * Normalize assistant message with embedded tools
91
+ * @private
92
+ */
93
+ _normalizeAssistantMessage(event, source) {
94
+ const normalized = { ...event };
95
+
96
+ // Normalize tools array if present
97
+ if (event.data?.tools && Array.isArray(event.data.tools)) {
98
+ normalized.data = {
99
+ ...event.data,
100
+ tools: event.data.tools
101
+ .filter(tool => tool.type !== 'tool_result') // Filter out orphan tool_result
102
+ .map(tool => this._normalizeToolCall(tool, source, event.timestamp))
103
+ };
104
+ }
105
+
106
+ return normalized;
107
+ }
108
+
109
+ /**
110
+ * Normalize a tool call to unified schema
111
+ *
112
+ * UnifiedToolCall schema:
113
+ * {
114
+ * id: string,
115
+ * name: string,
116
+ * startTime: string (ISO 8601),
117
+ * endTime: string | null,
118
+ * status: 'pending' | 'running' | 'completed' | 'error',
119
+ * input: Record<string, any>,
120
+ * result: string | null,
121
+ * error: string | null,
122
+ * metadata: {
123
+ * source: string,
124
+ * duration?: number,
125
+ * ...
126
+ * }
127
+ * }
128
+ *
129
+ * @private
130
+ */
131
+ _normalizeToolCall(tool, source, messageTimestamp) {
132
+ // Handle Copilot/Claude format with _matched flag
133
+ if (tool.type === 'tool_use') {
134
+ const status = this._computeStatus(tool);
135
+ const startTime = messageTimestamp;
136
+ const endTime = tool._matched ? messageTimestamp : null;
137
+
138
+ return {
139
+ type: 'tool_use', // Preserve type for frontend compatibility
140
+ id: tool.id,
141
+ name: tool.name,
142
+ startTime,
143
+ endTime,
144
+ status,
145
+ input: tool.input || {},
146
+ result: tool.result || null,
147
+ error: tool.error || null,
148
+ metadata: {
149
+ source,
150
+ matched: tool._matched,
151
+ duration: this._computeDuration(startTime, endTime)
152
+ }
153
+ };
154
+ }
155
+
156
+ // Handle Pi-Mono format (already has status)
157
+ if (tool.name && tool.status) {
158
+ const startTime = messageTimestamp;
159
+ // Normalize 'success' to 'completed' for backward compatibility
160
+ const normalizedStatus = tool.status === 'success' ? 'completed' : tool.status;
161
+ const endTime = normalizedStatus === 'completed' || normalizedStatus === 'error'
162
+ ? messageTimestamp
163
+ : null;
164
+
165
+ return {
166
+ id: tool.id || this._generateToolId(),
167
+ name: tool.name,
168
+ startTime,
169
+ endTime,
170
+ status: normalizedStatus,
171
+ input: tool.input || {},
172
+ result: tool.isError ? null : (tool.result || null),
173
+ error: tool.isError ? tool.result : null,
174
+ metadata: {
175
+ source,
176
+ duration: this._computeDuration(startTime, endTime)
177
+ }
178
+ };
179
+ }
180
+
181
+ // Fallback: minimal normalization for unknown formats
182
+ console.warn('[EventNormalizer] Unknown tool format, applying fallback normalization', tool);
183
+ return {
184
+ id: tool.id || this._generateToolId(),
185
+ name: tool.name || 'unknown',
186
+ startTime: messageTimestamp,
187
+ endTime: null,
188
+ status: 'running',
189
+ input: tool.input || {},
190
+ result: null,
191
+ error: null,
192
+ metadata: {
193
+ source,
194
+ fallback: true
195
+ }
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Compute tool status from tool object
201
+ * @private
202
+ */
203
+ _computeStatus(tool) {
204
+ // Explicit error indication
205
+ if (tool.error) {
206
+ return 'error';
207
+ }
208
+
209
+ // Has result = completed (regardless of _matched flag)
210
+ if (tool.result !== undefined && tool.result !== null && tool.result !== '') {
211
+ return 'completed';
212
+ }
213
+
214
+ // Explicitly unmatched with no result
215
+ if (tool._matched === false) {
216
+ return 'running';
217
+ }
218
+
219
+ // Matched = completed
220
+ if (tool._matched) {
221
+ return 'completed';
222
+ }
223
+
224
+ // No match info: assume running
225
+ return 'running';
226
+ }
227
+
228
+ /**
229
+ * Compute duration in milliseconds from start/end timestamps
230
+ * @private
231
+ */
232
+ _computeDuration(startTime, endTime) {
233
+ if (!startTime || !endTime) {
234
+ return undefined;
235
+ }
236
+
237
+ try {
238
+ const start = new Date(startTime);
239
+ const end = new Date(endTime);
240
+
241
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
242
+ return undefined;
243
+ }
244
+
245
+ const duration = end.getTime() - start.getTime();
246
+ return duration >= 0 ? duration : undefined;
247
+ } catch (err) {
248
+ return undefined;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Generate a unique tool ID
254
+ * @private
255
+ */
256
+ _generateToolId() {
257
+ return `tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
258
+ }
259
+
260
+ /**
261
+ * Normalize timeline events (tool.execution_start/complete, subagent events)
262
+ * Ensures consistent schema for these events
263
+ * @private
264
+ */
265
+ _normalizeTimelineEvent(event) {
266
+ // For tool.execution_start/complete, ensure consistent schema
267
+ if (event.type === 'tool.execution_start' || event.type === 'tool.execution_complete') {
268
+ return {
269
+ ...event,
270
+ data: {
271
+ ...event.data,
272
+ // Normalize field names for consistency
273
+ toolCallId: event.data?.toolCallId || event.data?.id,
274
+ toolName: event.data?.toolName || event.data?.tool || event.data?.name,
275
+ // Preserve original fields
276
+ ...event.data
277
+ }
278
+ };
279
+ }
280
+
281
+ // Subagent events: pass through (already have consistent schema)
282
+ if (event.type?.startsWith('subagent.')) {
283
+ return event;
284
+ }
285
+
286
+ // Unknown timeline event: pass through
287
+ return event;
288
+ }
289
+ }
290
+
291
+ module.exports = EventNormalizer;
@@ -12,21 +12,74 @@ const config = require('../config');
12
12
  const processManager = require('../utils/processManager');
13
13
 
14
14
  class InsightService {
15
- constructor(sessionDir) {
16
- this.sessionDir = sessionDir;
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
+ claude: {
32
+ name: 'Claude Code',
33
+ cli: 'claude',
34
+ args: (_tmpDir, prompt) => ['-p', prompt, '--dangerously-skip-permissions'],
35
+ cwd: sessionPath
36
+ },
37
+ 'pi-mono': {
38
+ name: 'Pi',
39
+ cli: 'pi',
40
+ args: (_tmpDir, prompt) => ['-p', prompt],
41
+ cwd: sessionPath
42
+ }
43
+ };
44
+
45
+ return configs[source] || configs.copilot; // fallback to copilot
17
46
  }
18
47
 
19
48
  /**
20
49
  * Generate or retrieve insight report
21
- * @param {string} sessionId - Session UUID
50
+ * @param {string} sessionId - Session ID
51
+ * @param {string} sessionPath - Full path to session directory
52
+ * @param {string} source - Session source: 'copilot', 'claude', or 'pi-mono'
22
53
  * @param {boolean} forceRegenerate - Force new generation
23
54
  * @returns {Promise<Object>} Insight status and report
24
55
  */
25
- async generateInsight(sessionId, forceRegenerate = false) {
26
- const sessionPath = path.join(this.sessionDir, sessionId);
56
+ async generateInsight(sessionId, sessionPath, source = 'copilot', forceRegenerate = false) {
27
57
  const insightFile = path.join(sessionPath, 'agent-review.md');
28
58
  const lockFile = path.join(sessionPath, 'agent-review.md.lock');
29
- const eventsFile = path.join(sessionPath, 'events.jsonl');
59
+
60
+ // Determine events file location based on directory structure
61
+ // Try standard events.jsonl first, then <sessionId>.jsonl (for file-type sessions),
62
+ // finally *_<sessionId>.jsonl (for Pi-Mono timestamped sessions)
63
+ let eventsFile = path.join(sessionPath, 'events.jsonl');
64
+ try {
65
+ await fs.access(eventsFile);
66
+ } catch {
67
+ // Try <sessionId>.jsonl (common for Claude file-type sessions)
68
+ try {
69
+ eventsFile = path.join(sessionPath, `${sessionId}.jsonl`);
70
+ await fs.access(eventsFile);
71
+ } catch {
72
+ // Try *_<sessionId>.jsonl (Pi-Mono format: YYYY-MM-DDTHH-mm-ss-SSSZ_<uuid>.jsonl)
73
+ const entries = await fs.readdir(sessionPath);
74
+ const piFile = entries.find(f => f.endsWith(`_${sessionId}.jsonl`));
75
+ if (piFile) {
76
+ eventsFile = path.join(sessionPath, piFile);
77
+ }
78
+ }
79
+ }
80
+
81
+ const toolConfig = this._getToolConfig(source, sessionPath);
82
+ const toolName = toolConfig.name;
30
83
 
31
84
  // Check if complete insight exists
32
85
  if (!forceRegenerate) {
@@ -63,7 +116,7 @@ class InsightService {
63
116
  // Still valid, return generating status
64
117
  return {
65
118
  status: 'generating',
66
- report: '# Generating Copilot Insight...\n\nAnother request is currently generating this insight. Please wait.',
119
+ report: `# Generating ${toolName} Insight...\n\nAnother request is currently generating this insight. Please wait.`,
67
120
  startedAt: lockStats.birthtime,
68
121
  lastUpdate: lockStats.mtime,
69
122
  ageMs: Date.now() - lockStats.birthtime.getTime()
@@ -106,77 +159,92 @@ class InsightService {
106
159
  }
107
160
 
108
161
  // Start generation
109
- await this._spawnCopilotProcess(sessionId, sessionPath, eventsFile, insightFile, lockFile);
162
+ await this._spawnAnalysisProcess(sessionPath, eventsFile, insightFile, lockFile, toolConfig);
110
163
 
111
164
  return {
112
165
  status: 'generating',
113
- report: '# Generating Copilot Insight...\n\nAnalysis in progress. Please wait.',
166
+ report: `# Generating ${toolName} Insight...\n\nAnalysis in progress. Please wait.`,
114
167
  startedAt: new Date()
115
168
  };
116
169
  }
117
170
 
118
171
  /**
119
- * Spawn copilot process safely (no shell)
172
+ * Spawn analysis process safely (no shell)
120
173
  * @private
121
174
  */
122
- async _spawnCopilotProcess(sessionId, sessionPath, eventsFile, insightFile, lockFile) {
175
+ async _spawnAnalysisProcess(sessionPath, eventsFile, insightFile, lockFile, toolConfig) {
176
+ const sessionId = path.basename(sessionPath); // Extract session ID from path
123
177
  const tmpDir = path.join(os.tmpdir(), `agent-review-${sessionId}-${Date.now()}`);
124
- await fs.mkdir(tmpDir, { recursive: true });
178
+ await fs.mkdir(tmpDir, { recursive: true});
125
179
 
126
- const prompt = this._buildPrompt(insightFile);
180
+ const prompt = this._buildPrompt(insightFile, eventsFile);
127
181
  const outputFile = path.join(sessionPath, 'agent-review.md.tmp');
128
182
 
129
- // Spawn copilot directly (no shell)
130
- const copilotPath = 'copilot';
131
- const args = ['--config-dir', tmpDir, '--yolo', '-p', prompt];
183
+ // Spawn analysis tool directly (no shell)
184
+ const cliPath = toolConfig.cli;
185
+ const args = toolConfig.args(tmpDir, prompt);
186
+
187
+ console.log(`🤖 Starting ${toolConfig.name} analysis: ${cliPath} ${args.slice(0, 2).join(' ')}...`);
188
+ console.log(`📋 Args count: ${args.length}, prompt length: ${prompt.length} chars`);
132
189
 
133
- // Use system PATH - copilot should be in the user's PATH
134
- const copilotProcess = spawn(copilotPath, args, {
190
+ // Use system PATH - CLI should be in the user's PATH
191
+ const analysisProcess = spawn(cliPath, args, {
135
192
  env: { ...process.env },
136
193
  cwd: sessionPath,
137
194
  stdio: ['pipe', 'pipe', 'pipe']
138
195
  });
139
196
 
140
197
  // Register for cleanup
141
- processManager.register(copilotProcess, { name: `insight-${sessionId}` });
142
-
143
- // Pipe events file to stdin
144
- const eventsStream = fsSync.createReadStream(eventsFile);
145
- // Handle EPIPE: if copilot exits before stdin is fully written, suppress the error
146
- copilotProcess.stdin.on('error', (err) => {
147
- if (err.code === 'EPIPE') {
148
- eventsStream.destroy();
149
- } else {
150
- console.error('❌ stdin error:', err);
151
- }
152
- });
153
- eventsStream.pipe(copilotProcess.stdin);
198
+ processManager.register(analysisProcess, { name: `insight-${sessionId}` });
199
+
200
+ // Pipe events file to stdin (for tools that read from stdin like copilot)
201
+ // Claude Code and Pi read files directly, so they don't need stdin
202
+ if (toolConfig.cli === 'copilot') {
203
+ const eventsStream = fsSync.createReadStream(eventsFile);
204
+ // Handle EPIPE: if process exits before stdin is fully written, suppress the error
205
+ analysisProcess.stdin.on('error', (err) => {
206
+ if (err.code === 'EPIPE') {
207
+ eventsStream.destroy();
208
+ } else {
209
+ console.error('❌ stdin error:', err);
210
+ }
211
+ });
212
+ eventsStream.pipe(analysisProcess.stdin);
213
+ } else {
214
+ // Close stdin for tools that don't need it
215
+ analysisProcess.stdin.end();
216
+ }
154
217
 
155
218
  // Capture output
156
219
  const outputStream = fsSync.createWriteStream(outputFile);
157
- copilotProcess.stdout.pipe(outputStream);
220
+ analysisProcess.stdout.pipe(outputStream);
158
221
 
159
222
  // Capture stderr with size limit
160
223
  const stderrChunks = [];
161
224
  let stderrSize = 0;
162
225
  const MAX_STDERR = 64 * 1024; // 64KB cap
163
226
 
164
- copilotProcess.stderr.on('data', (data) => {
227
+ analysisProcess.stderr.on('data', (data) => {
165
228
  if (stderrSize < MAX_STDERR) {
166
229
  stderrChunks.push(data);
167
230
  stderrSize += data.length;
168
231
  }
169
232
  });
170
233
 
171
- copilotProcess.on('close', async (code) => {
234
+ analysisProcess.on('close', async (code) => {
172
235
  try {
173
236
  outputStream.end();
174
237
 
238
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8').slice(0, MAX_STDERR);
239
+ console.log(`📋 ${toolConfig.name} process exited with code ${code}`);
240
+ if (stderr) {
241
+ console.log(`📋 ${toolConfig.name} stderr:`, stderr.substring(0, 500));
242
+ }
243
+
175
244
  if (code !== 0) {
176
- const stderr = Buffer.concat(stderrChunks).toString('utf-8').slice(0, MAX_STDERR);
177
- console.error('❌ Copilot CLI failed:', stderr);
245
+ console.error(`❌ ${toolConfig.name} CLI failed (code ${code}):`, stderr);
178
246
  await fs.writeFile(insightFile,
179
- `# ❌ Generation Failed\n\n\`\`\`\n${stderr}\n\`\`\`\n`,
247
+ `# ❌ Generation Failed\n\nExit code: ${code}\n\n\`\`\`\n${stderr || '(no error output)'}\n\`\`\`\n`,
180
248
  'utf-8'
181
249
  );
182
250
  } else {
@@ -212,8 +280,8 @@ class InsightService {
212
280
  }
213
281
  });
214
282
 
215
- copilotProcess.on('error', async (err) => {
216
- console.error('❌ Failed to spawn copilot:', err);
283
+ analysisProcess.on('error', async (err) => {
284
+ console.error(`❌ Failed to spawn ${toolConfig.name}:`, err);
217
285
  await fs.unlink(lockFile).catch(() => {});
218
286
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
219
287
  });
@@ -223,19 +291,26 @@ class InsightService {
223
291
  * Build insight generation prompt
224
292
  * @private
225
293
  */
226
- _buildPrompt(outputPath) {
294
+ _buildPrompt(outputPath, eventsFile) {
227
295
  const sessionDir = path.dirname(outputPath);
296
+ const eventsFilename = path.basename(eventsFile);
228
297
  const workDir = `${sessionDir}/.output`;
229
- return `You are an expert AI agent evaluator. The current working directory is a Copilot CLI session folder (located at ~/.copilot/session-state/<session_id>/). It contains the raw session data from an AI coding agent run.
230
-
298
+
299
+ // For Pi-Mono: emphasize analyzing ONLY the specified file
300
+ const fileInstruction = eventsFilename.includes('_')
301
+ ? `\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`
302
+ : '';
303
+
304
+ 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.
305
+ ${fileInstruction}
231
306
  **Step 1 — Discover session files.** Run \`ls -la\` to see what's available, then note which files exist:
232
- - \`events.jsonl\` — the main session event log (JSONL, one JSON event per line). Primary data source. May be large.
307
+ - \`${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.**
233
308
  - \`plan.md\` — the agent's plan (if it exists).
234
309
  - \`workspace.yaml\` — workspace configuration (if it exists).
235
310
  - Any other relevant files.
236
311
 
237
312
  **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:
238
- - Read \`events.jsonl\` from \`${sessionDir}\` (use Bash: \`cat\`, \`jq\`, or \`python3\` to parse)
313
+ - Read \`${eventsFilename}\` from \`${sessionDir}\` (use Bash: \`cat\`, \`jq\`, or \`python3\` to parse) **— ONLY this file, ignore others**
239
314
  - Read other session files as needed
240
315
  - Write its findings to an intermediate file in \`${workDir}/\`
241
316
  - Return a summary of its findings
@@ -362,8 +437,22 @@ IMPORTANT CONSTRAINTS:
362
437
  /**
363
438
  * Get insight status
364
439
  */
365
- async getInsightStatus(sessionId) {
366
- const sessionPath = path.join(this.sessionDir, sessionId);
440
+ /**
441
+ * Get insight status
442
+ * @param {string} sessionId - Session ID
443
+ * @param {string} sessionPath - Full path to session directory
444
+ * @param {string} source - Session source
445
+ * @returns {Promise<Object>} Status object
446
+ */
447
+ async getInsightStatus(sessionId, sessionPath, _source = 'copilot') {
448
+ return await this._getStatusForSource(sessionPath);
449
+ }
450
+
451
+ /**
452
+ * Get status for a specific session directory
453
+ * @private
454
+ */
455
+ async _getStatusForSource(sessionPath) {
367
456
  const insightFile = path.join(sessionPath, 'agent-review.md');
368
457
  const lockFile = path.join(sessionPath, 'agent-review.md.lock');
369
458
  const tmpFile = path.join(sessionPath, 'agent-review.md.tmp');
@@ -416,9 +505,12 @@ IMPORTANT CONSTRAINTS:
416
505
 
417
506
  /**
418
507
  * Delete insight report
508
+ * @param {string} sessionId - Session ID
509
+ * @param {string} sessionPath - Full path to session directory
510
+ * @param {string} source - Session source
511
+ * @returns {Promise<Object>} Result object
419
512
  */
420
- async deleteInsight(sessionId) {
421
- const sessionPath = path.join(this.sessionDir, sessionId);
513
+ async deleteInsight(sessionId, sessionPath, _source = 'copilot') {
422
514
  const insightFile = path.join(sessionPath, 'agent-review.md');
423
515
 
424
516
  try {