@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.6

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 (45) hide show
  1. package/README.md +3 -3
  2. package/bin/copilot-session-viewer +2 -2
  3. package/dist/server.min.js +99 -0
  4. package/package.json +5 -17
  5. package/public/js/homepage.min.js +9 -9
  6. package/public/js/session-detail.min.js +36 -7
  7. package/public/vendor/marked.umd.min.js +8 -0
  8. package/public/vendor/purify.min.js +3 -0
  9. package/public/vendor/vue-virtual-scroller.css +1 -0
  10. package/public/vendor/vue-virtual-scroller.min.js +2 -0
  11. package/public/vendor/vue.global.prod.min.js +19 -0
  12. package/views/session-vue.ejs +31 -6
  13. package/views/time-analyze.ejs +2 -2
  14. package/lib/parsers/README.md +0 -239
  15. package/lib/parsers/base-parser.js +0 -53
  16. package/lib/parsers/claude-parser.js +0 -181
  17. package/lib/parsers/copilot-parser.js +0 -143
  18. package/lib/parsers/index.js +0 -15
  19. package/lib/parsers/parser-factory.js +0 -77
  20. package/lib/parsers/pi-mono-parser.js +0 -119
  21. package/lib/parsers/vscode-parser.js +0 -591
  22. package/server.js +0 -29
  23. package/src/app.js +0 -129
  24. package/src/config/index.js +0 -27
  25. package/src/controllers/insightController.js +0 -136
  26. package/src/controllers/sessionController.js +0 -449
  27. package/src/controllers/tagController.js +0 -113
  28. package/src/controllers/uploadController.js +0 -648
  29. package/src/middleware/common.js +0 -67
  30. package/src/middleware/rateLimiting.js +0 -62
  31. package/src/models/Session.js +0 -146
  32. package/src/routes/api.js +0 -11
  33. package/src/routes/insights.js +0 -12
  34. package/src/routes/pages.js +0 -12
  35. package/src/routes/uploads.js +0 -14
  36. package/src/schemas/event.schema.js +0 -73
  37. package/src/services/eventNormalizer.js +0 -291
  38. package/src/services/insightService.js +0 -535
  39. package/src/services/sessionRepository.js +0 -1092
  40. package/src/services/sessionService.js +0 -1919
  41. package/src/services/tagService.js +0 -205
  42. package/src/telemetry.js +0 -152
  43. package/src/utils/fileUtils.js +0 -305
  44. package/src/utils/helpers.js +0 -45
  45. package/src/utils/processManager.js +0 -85
@@ -1,67 +0,0 @@
1
- const config = require('../config');
2
- const { trackException, isEnabled: isTelemetryEnabled } = require('../telemetry');
3
-
4
- // Request timeout middleware
5
- const requestTimeout = (req, res, next) => {
6
- req.setTimeout(config.REQUEST_TIMEOUT_MS);
7
- next();
8
- };
9
-
10
- // Telemetry middleware - makes telemetry settings available to templates
11
- const telemetryLocals = (req, res, next) => {
12
- res.locals.telemetryEnabled = isTelemetryEnabled;
13
- res.locals.telemetryConnectionString = isTelemetryEnabled
14
- ? (process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || 'InstrumentationKey=39f4fbf1-d82f-42c3-b4ef-ea92a1fd82cb;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=7d4bb432-f2f5-4526-a5e6-31901e5a2db2')
15
- : null;
16
- next();
17
- };
18
-
19
- // CORS middleware for development
20
- const developmentCors = (req, res, next) => {
21
- if (config.NODE_ENV === 'development') {
22
- const allowedOrigins = ['http://localhost:3838', 'http://127.0.0.1:3838'];
23
- const origin = req.headers.origin;
24
- if (allowedOrigins.includes(origin)) {
25
- res.header('Access-Control-Allow-Origin', origin);
26
- res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
27
- res.header('Access-Control-Allow-Headers', 'Content-Type');
28
- }
29
- }
30
- next();
31
- };
32
-
33
- // Error handling middleware
34
- const errorHandler = (err, req, res, _next) => {
35
- console.error('Unhandled error:', err.stack);
36
-
37
- // Track exception in Application Insights
38
- trackException(err, {
39
- url: req.url,
40
- method: req.method,
41
- statusCode: (err.status || 500).toString(),
42
- userAgent: (req.headers && req.headers['user-agent']) || 'unknown'
43
- });
44
-
45
- const statusCode = err.status || 500;
46
- // Default to production-safe behavior if NODE_ENV is not set
47
- const isDevelopment = config.NODE_ENV === 'development';
48
- const message = isDevelopment ? err.message : 'Internal server error';
49
-
50
- res.status(statusCode).json({
51
- error: message,
52
- ...(isDevelopment && { stack: err.stack })
53
- });
54
- };
55
-
56
- // 404 handler
57
- const notFoundHandler = (req, res) => {
58
- res.status(404).json({ error: 'Not found' });
59
- };
60
-
61
- module.exports = {
62
- requestTimeout,
63
- developmentCors,
64
- errorHandler,
65
- notFoundHandler,
66
- telemetryLocals
67
- };
@@ -1,62 +0,0 @@
1
- const rateLimit = require('express-rate-limit');
2
-
3
- // Disable rate limiting in E2E tests (when NODE_ENV is test or when running via Playwright)
4
- const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.PLAYWRIGHT === '1';
5
-
6
- // Global rate limiting for all routes
7
- const globalLimiter = rateLimit({
8
- windowMs: 15 * 60 * 1000, // 15 minutes
9
- max: isTestEnvironment ? 10000 : 100, // Much higher limit for tests
10
- message: { error: 'Too many requests. Please try again later.' },
11
- standardHeaders: true,
12
- legacyHeaders: false,
13
- skip: (req) => {
14
- // Skip rate limiting entirely in test environment
15
- if (isTestEnvironment) return true;
16
-
17
- // Skip static files
18
- if (req.path.startsWith('/public')) return true;
19
-
20
- // Skip insight status checks (GET requests)
21
- if (req.method === 'GET' && req.path.includes('/insight')) return true;
22
-
23
- return false;
24
- }
25
- });
26
-
27
- // Rate limiting for insight generation (stricter for POST)
28
- const insightGenerationLimiter = rateLimit({
29
- windowMs: 5 * 60 * 1000, // 5 minutes (shorter window)
30
- max: 3, // 3 generations per 5-minute window (expensive operations)
31
- message: {
32
- error: 'Too many insight generation requests. Please wait 5 minutes before generating another insight.',
33
- retryAfter: 5 * 60 // 5 minutes in seconds
34
- },
35
- standardHeaders: true,
36
- legacyHeaders: false
37
- });
38
-
39
- // Rate limiting for insight status/retrieval (very lenient for GET/DELETE)
40
- const insightAccessLimiter = rateLimit({
41
- windowMs: 1 * 60 * 1000, // 1 minute (shorter window)
42
- max: 50, // 50 requests per minute (very lenient)
43
- message: { error: 'Too many insight requests. Please try again in a minute.' },
44
- standardHeaders: true,
45
- legacyHeaders: false
46
- });
47
-
48
- // Rate limiting for file uploads (strict)
49
- const uploadLimiter = rateLimit({
50
- windowMs: 15 * 60 * 1000, // 15 minutes
51
- max: 5, // 5 uploads per window
52
- message: { error: 'Too many upload requests. Please try again later.' },
53
- standardHeaders: true,
54
- legacyHeaders: false
55
- });
56
-
57
- module.exports = {
58
- globalLimiter,
59
- insightGenerationLimiter,
60
- insightAccessLimiter,
61
- uploadLimiter
62
- };
@@ -1,146 +0,0 @@
1
- const path = require('path');
2
-
3
- /**
4
- * Session domain model
5
- */
6
- class Session {
7
- constructor(id, type, options = {}) {
8
- this.id = id;
9
- this.type = type; // 'directory' or 'file'
10
- this.source = options.source || 'copilot'; // 'copilot' or 'claude'
11
- this.directory = options.directory || null; // Full path to session directory
12
- this.filePath = options.filePath || null; // Full path to session file (for file-based sessions)
13
- this.workspace = options.workspace || {};
14
- this.createdAt = options.createdAt;
15
- this.updatedAt = options.updatedAt;
16
- this.summary = options.summary || (type === 'file' ? 'Legacy session' : 'No summary');
17
- this.hasEvents = options.hasEvents || false;
18
- this.eventCount = options.eventCount || 0;
19
- this.duration = options.duration || null; // Duration in milliseconds
20
- this.isImported = options.isImported || false; // Whether session was imported
21
- this.hasInsight = options.hasInsight || false; // Whether session has insight report
22
- this.copilotVersion = options.copilotVersion || null; // Copilot CLI version
23
- this.selectedModel = options.selectedModel || null; // LLM model used
24
- this.sessionStatus = options.sessionStatus || 'completed'; // 'completed' | 'wip'
25
- }
26
-
27
- /**
28
- * Create Session from directory
29
- * @param {string} dirPath - Directory path
30
- * @param {string} id - Session ID
31
- * @param {object} stats - fs.Stats object
32
- * @param {object} workspace - Parsed workspace.yaml
33
- * @param {number} eventCount - Number of events
34
- * @param {number|null} duration - Duration in milliseconds
35
- * @param {boolean} isImported - Whether session was imported
36
- * @param {boolean} hasInsight - Whether session has insight report
37
- * @param {string|null} copilotVersion - Copilot CLI version
38
- * @param {string|null} selectedModel - LLM model used
39
- * @param {string} sessionStatus - Session status: 'completed' or 'wip'
40
- * @returns {Session}
41
- */
42
- static fromDirectory(dirPath, id, stats, workspace, eventCount, duration, isImported, hasInsight, copilotVersion, selectedModel, sessionStatus) {
43
- const createdAt = workspace?.created_at
44
- ? new Date(workspace.created_at)
45
- : workspace?.startTime
46
- ? new Date(workspace.startTime)
47
- : stats.birthtime;
48
- const updatedAt = workspace?.updated_at
49
- ? new Date(workspace.updated_at)
50
- : workspace?.endTime
51
- ? new Date(workspace.endTime)
52
- : stats.mtime;
53
- return new Session(id, 'directory', {
54
- directory: dirPath, // Add directory path
55
- workspace: workspace,
56
- createdAt,
57
- updatedAt,
58
- summary: workspace?.summary || 'No summary',
59
- hasEvents: eventCount > 0,
60
- eventCount: eventCount,
61
- duration: duration,
62
- isImported: isImported,
63
- hasInsight: hasInsight,
64
- copilotVersion: copilotVersion,
65
- selectedModel: selectedModel,
66
- sessionStatus: sessionStatus
67
- });
68
- }
69
-
70
- /**
71
- * Create Session from .jsonl file
72
- * @param {string} filePath - File path
73
- * @param {string} id - Session ID
74
- * @param {object} stats - fs.Stats object
75
- * @param {number} eventCount - Number of events
76
- * @param {string} [summary] - Optional summary (e.g. first user message)
77
- * @param {number|null} duration - Duration in milliseconds
78
- * @param {string|null} copilotVersion - Copilot CLI version
79
- * @param {string|null} selectedModel - LLM model used
80
- * @param {string} sessionStatus - Session status: 'completed' or 'wip'
81
- * @returns {Session}
82
- */
83
- static fromFile(filePath, id, stats, eventCount, summary, duration, copilotVersion, selectedModel, sessionStatus) {
84
- return new Session(id, 'file', {
85
- filePath: filePath,
86
- directory: path.dirname(filePath), // Directory containing the file
87
- createdAt: stats.birthtime,
88
- updatedAt: stats.mtime,
89
- summary: summary || 'Legacy session',
90
- hasEvents: true,
91
- eventCount: eventCount,
92
- duration: duration,
93
- isImported: false, // .jsonl files can't be imported
94
- hasInsight: false, // .jsonl files don't have insights
95
- copilotVersion: copilotVersion,
96
- selectedModel: selectedModel,
97
- sessionStatus: sessionStatus
98
- });
99
- }
100
-
101
- /**
102
- * Convert to plain object
103
- * @returns {object}
104
- */
105
- toJSON() {
106
- // Generate display-ready source metadata (Violation #3 & #5 fix)
107
- const sourceMetadata = this._getSourceDisplayMetadata(this.source);
108
-
109
- return {
110
- id: this.id,
111
- type: this.type,
112
- source: this.source,
113
- sourceName: sourceMetadata.name,
114
- sourceBadgeClass: sourceMetadata.badgeClass,
115
- directory: this.directory, // Include directory path
116
- workspace: this.workspace,
117
- createdAt: this.createdAt,
118
- updatedAt: this.updatedAt,
119
- summary: this.summary,
120
- hasEvents: this.hasEvents,
121
- eventCount: this.eventCount,
122
- duration: this.duration,
123
- isImported: this.isImported,
124
- hasInsight: this.hasInsight,
125
- copilotVersion: this.copilotVersion,
126
- selectedModel: this.selectedModel,
127
- sessionStatus: this.sessionStatus
128
- };
129
- }
130
-
131
- /**
132
- * Get display metadata for source
133
- * @private
134
- */
135
- _getSourceDisplayMetadata(source) {
136
- const metadata = {
137
- 'copilot': { name: 'Copilot CLI', badgeClass: 'source-copilot' },
138
- 'claude': { name: 'Claude', badgeClass: 'source-claude' },
139
- 'pi-mono': { name: 'Pi', badgeClass: 'source-pi-mono' },
140
- 'vscode': { name: 'Copilot Chat', badgeClass: 'source-vscode' }
141
- };
142
- return metadata[source] || { name: source, badgeClass: 'source-unknown' };
143
- }
144
- }
145
-
146
- module.exports = Session;
package/src/routes/api.js DELETED
@@ -1,11 +0,0 @@
1
- const express = require('express');
2
- const SessionController = require('../controllers/sessionController');
3
-
4
- const router = express.Router();
5
- const sessionController = new SessionController();
6
-
7
- // API Routes
8
- router.get('/sessions', sessionController.getSessions.bind(sessionController));
9
- router.get('/sessions/:id/events', sessionController.getSessionEvents.bind(sessionController));
10
-
11
- module.exports = router;
@@ -1,12 +0,0 @@
1
- const express = require('express');
2
- const InsightController = require('../controllers/insightController');
3
-
4
- const router = express.Router();
5
- const insightController = new InsightController();
6
-
7
- // Insight Routes
8
- router.post('/session/:id/insight', insightController.generateInsight.bind(insightController));
9
- router.get('/session/:id/insight', insightController.getInsightStatus.bind(insightController));
10
- router.delete('/session/:id/insight', insightController.deleteInsight.bind(insightController));
11
-
12
- module.exports = router;
@@ -1,12 +0,0 @@
1
- const express = require('express');
2
- const SessionController = require('../controllers/sessionController');
3
-
4
- const router = express.Router();
5
- const sessionController = new SessionController();
6
-
7
- // Page Routes
8
- router.get('/', sessionController.getHomepage.bind(sessionController));
9
- router.get('/session/:id', sessionController.getSessionDetail.bind(sessionController));
10
- router.get('/session/:id/time-analyze', sessionController.getTimeAnalysis.bind(sessionController));
11
-
12
- module.exports = router;
@@ -1,14 +0,0 @@
1
- const express = require('express');
2
- const UploadController = require('../controllers/uploadController');
3
-
4
- const router = express.Router();
5
- const uploadController = new UploadController();
6
-
7
- // Upload Routes
8
- router.get('/session/:id/share', uploadController.shareSession.bind(uploadController));
9
- router.post('/session/import',
10
- (req, res, next) => uploadController.getUploadMiddleware()(req, res, next),
11
- uploadController.importSession.bind(uploadController)
12
- );
13
-
14
- module.exports = router;
@@ -1,73 +0,0 @@
1
- const { z } = require('zod');
2
-
3
- /**
4
- * Unified Event Schema for Copilot Session Viewer
5
- *
6
- * This schema defines the standardized event format that the API returns to the frontend.
7
- * Both Copilot and Claude events are normalized to match this schema.
8
- */
9
-
10
- // Tool call schema (unified format for both Copilot and Claude)
11
- const ToolSchema = z.object({
12
- type: z.literal('tool_use'),
13
- id: z.string(),
14
- name: z.string(),
15
- input: z.record(z.any()),
16
- result: z.any().optional(), // Tool execution result (when matched)
17
- status: z.enum(['success', 'error', 'running']).optional(),
18
- error: z.string().optional(),
19
- _matched: z.boolean().optional() // Internal flag: whether result was matched
20
- });
21
-
22
- // Subagent metadata (for events belonging to a subagent)
23
- const SubagentMetadataSchema = z.object({
24
- id: z.string(),
25
- name: z.string()
26
- }).optional();
27
-
28
- // Event data schema (standardized data field)
29
- const EventDataSchema = z.object({
30
- // Message content (text)
31
- message: z.string().optional(),
32
- text: z.string().optional(), // Alternative field name (legacy)
33
-
34
- // Tool calls (unified format)
35
- tools: z.array(ToolSchema).optional(),
36
-
37
- // Original fields preserved for reference
38
- // (Copilot-specific fields)
39
- messageId: z.string().optional(),
40
- content: z.string().optional(), // Original Copilot content field
41
- toolRequests: z.array(z.any()).optional(), // Original Copilot toolRequests
42
-
43
- // (Claude-specific fields)
44
- // ... other fields as needed
45
- }).passthrough(); // Allow additional fields
46
-
47
- // Base event schema
48
- const EventSchema = z.object({
49
- // Core fields
50
- type: z.string(),
51
- id: z.string().optional(),
52
- timestamp: z.string(),
53
- parentId: z.string().nullable().optional(),
54
-
55
- // Standardized data
56
- data: EventDataSchema.optional(),
57
-
58
- // Metadata
59
- _subagent: SubagentMetadataSchema,
60
- _fileIndex: z.number().optional(),
61
-
62
- // Virtual fields (computed by frontend)
63
- stableId: z.string().optional(),
64
- virtualIndex: z.number().optional()
65
- }).passthrough(); // Allow additional fields for flexibility
66
-
67
- // Export schemas
68
- module.exports = {
69
- EventSchema,
70
- EventDataSchema,
71
- ToolSchema,
72
- SubagentMetadataSchema
73
- };
@@ -1,291 +0,0 @@
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;