@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.
- package/README.md +3 -3
- package/bin/copilot-session-viewer +2 -2
- package/dist/server.min.js +99 -0
- package/package.json +5 -17
- package/public/js/homepage.min.js +9 -9
- package/public/js/session-detail.min.js +36 -7
- package/public/vendor/marked.umd.min.js +8 -0
- package/public/vendor/purify.min.js +3 -0
- package/public/vendor/vue-virtual-scroller.css +1 -0
- package/public/vendor/vue-virtual-scroller.min.js +2 -0
- package/public/vendor/vue.global.prod.min.js +19 -0
- package/views/session-vue.ejs +31 -6
- package/views/time-analyze.ejs +2 -2
- package/lib/parsers/README.md +0 -239
- package/lib/parsers/base-parser.js +0 -53
- package/lib/parsers/claude-parser.js +0 -181
- package/lib/parsers/copilot-parser.js +0 -143
- package/lib/parsers/index.js +0 -15
- package/lib/parsers/parser-factory.js +0 -77
- package/lib/parsers/pi-mono-parser.js +0 -119
- package/lib/parsers/vscode-parser.js +0 -591
- package/server.js +0 -29
- package/src/app.js +0 -129
- package/src/config/index.js +0 -27
- package/src/controllers/insightController.js +0 -136
- package/src/controllers/sessionController.js +0 -449
- package/src/controllers/tagController.js +0 -113
- package/src/controllers/uploadController.js +0 -648
- package/src/middleware/common.js +0 -67
- package/src/middleware/rateLimiting.js +0 -62
- package/src/models/Session.js +0 -146
- package/src/routes/api.js +0 -11
- package/src/routes/insights.js +0 -12
- package/src/routes/pages.js +0 -12
- package/src/routes/uploads.js +0 -14
- package/src/schemas/event.schema.js +0 -73
- package/src/services/eventNormalizer.js +0 -291
- package/src/services/insightService.js +0 -535
- package/src/services/sessionRepository.js +0 -1092
- package/src/services/sessionService.js +0 -1919
- package/src/services/tagService.js +0 -205
- package/src/telemetry.js +0 -152
- package/src/utils/fileUtils.js +0 -305
- package/src/utils/helpers.js +0 -45
- package/src/utils/processManager.js +0 -85
package/src/middleware/common.js
DELETED
|
@@ -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
|
-
};
|
package/src/models/Session.js
DELETED
|
@@ -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;
|
package/src/routes/insights.js
DELETED
|
@@ -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;
|
package/src/routes/pages.js
DELETED
|
@@ -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;
|
package/src/routes/uploads.js
DELETED
|
@@ -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;
|