@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,239 +0,0 @@
1
- # Session Event Parsers
2
-
3
- Strategy pattern implementation for parsing session events from multiple formats.
4
-
5
- ## Architecture
6
-
7
- ```
8
- lib/parsers/
9
- ├── base-parser.js # Base parser interface
10
- ├── copilot-parser.js # Copilot CLI format parser
11
- ├── claude-parser.js # Claude Code format parser
12
- ├── pi-mono-parser.js # Pi-Mono format parser
13
- ├── parser-factory.js # Parser factory (auto-detection)
14
- └── index.js # Export all parsers
15
- ```
16
-
17
- ## Design Pattern
18
-
19
- ### Strategy Pattern
20
-
21
- - **Strategy Interface**: `BaseSessionParser` defines methods all parsers must implement
22
- - **Concrete Strategies**: `CopilotSessionParser`, `ClaudeSessionParser`, `PiMonoSessionParser` implement specific parsing logic
23
- - **Context**: `ParserFactory` automatically selects the appropriate strategy
24
-
25
- ### Benefits
26
-
27
- 1. **Extensible**: Add new formats by simply implementing `BaseSessionParser`
28
- 2. **Decoupled**: Parsing logic is separated from consumers
29
- 3. **Auto-detection**: `ParserFactory` automatically identifies formats
30
- 4. **Unified Interface**: Different formats output the same data structure
31
-
32
- ## Usage
33
-
34
- ### Auto-detect Format
35
-
36
- ```javascript
37
- const { ParserFactory } = require('./lib/parsers');
38
-
39
- const events = [...]; // Events read from jsonl
40
- const factory = new ParserFactory();
41
-
42
- // Auto-detect and parse
43
- const result = factory.parse(events);
44
-
45
- // Get parser type
46
- const parserType = factory.getParserType(events); // 'copilot', 'claude', or 'pi-mono'
47
- ```
48
-
49
- ### Use Specific Parser Directly
50
-
51
- ```javascript
52
- const { CopilotSessionParser, ClaudeSessionParser, PiMonoSessionParser } = require('./lib/parsers');
53
-
54
- // Copilot CLI
55
- const copilotParser = new CopilotSessionParser();
56
- if (copilotParser.canParse(events)) {
57
- const result = copilotParser.parse(events);
58
- }
59
-
60
- // Claude Code
61
- const claudeParser = new ClaudeSessionParser();
62
- if (claudeParser.canParse(events)) {
63
- const result = claudeParser.parse(events);
64
- }
65
-
66
- // Pi-Mono
67
- const piMonoParser = new PiMonoSessionParser();
68
- if (piMonoParser.canParse(events)) {
69
- const result = piMonoParser.parse(events);
70
- }
71
- ```
72
-
73
- ## Unified Output Format
74
-
75
- All parsers output the same data structure:
76
-
77
- ```javascript
78
- {
79
- metadata: {
80
- sessionId: "...",
81
- startTime: "...",
82
- model: "...",
83
- version: "...",
84
- cwd: "...",
85
- branch: "...",
86
- // ...
87
- },
88
- turns: [
89
- {
90
- turnId: "...",
91
- userMessage: {
92
- id: "...",
93
- content: "...",
94
- timestamp: "..."
95
- },
96
- assistantMessages: [
97
- {
98
- id: "...",
99
- content: "...",
100
- toolRequests: [...],
101
- timestamp: "..."
102
- }
103
- ],
104
- toolCalls: [...]
105
- }
106
- ],
107
- toolCalls: [...],
108
- allEvents: [...] // Raw events
109
- }
110
- ```
111
-
112
- ## Supported Formats
113
-
114
- ### 1. Copilot CLI Format
115
-
116
- **Characteristics:**
117
- - Event types: `session.start`, `user.message`, `assistant.message`, `tool.execution_start`
118
- - Structure: `{type, data: {...}, id, parentId}`
119
- - Tree relationship: Connected via `parentId`
120
-
121
- **Example:**
122
- ```json
123
- {
124
- "type": "session.start",
125
- "data": {
126
- "sessionId": "...",
127
- "selectedModel": "claude-sonnet-4.5"
128
- },
129
- "id": "...",
130
- "timestamp": "..."
131
- }
132
- ```
133
-
134
- ### 2. Claude Code Format
135
-
136
- **Characteristics:**
137
- - Event types: `user`, `assistant`, `file-history-snapshot`, `queue-operation`
138
- - Structure: `{type, uuid, parentUuid, message: {...}}`
139
- - Tree relationship: Connected via `parentUuid`
140
-
141
- **Example:**
142
- ```json
143
- {
144
- "type": "user",
145
- "uuid": "...",
146
- "parentUuid": null,
147
- "sessionId": "...",
148
- "message": {
149
- "role": "user",
150
- "content": "..."
151
- }
152
- }
153
- ```
154
-
155
- ### 3. Pi-Mono Format
156
-
157
- **Characteristics:**
158
- - Event types: `message` (with role), `model_change`, `thinking_change`
159
- - Structure: `{type, role, message, toolResult, timestamp}`
160
- - Flat structure with parentId linkage for tool results
161
-
162
- **Example:**
163
- ```json
164
- {
165
- "type": "message",
166
- "role": "user",
167
- "message": "...",
168
- "timestamp": "...",
169
- "id": "..."
170
- }
171
- ```
172
-
173
- ## Adding New Formats
174
-
175
- 1. Extend `BaseSessionParser`
176
- 2. Implement all required methods
177
- 3. Register in `ParserFactory`
178
-
179
- ```javascript
180
- const BaseSessionParser = require('./base-parser');
181
-
182
- class MyCustomParser extends BaseSessionParser {
183
- canParse(events) {
184
- // Detection logic
185
- return events.some(e => e.customField);
186
- }
187
-
188
- parse(events) {
189
- return {
190
- metadata: this.getMetadata(events),
191
- turns: this.extractTurns(events),
192
- toolCalls: this.extractToolCalls(events),
193
- allEvents: events
194
- };
195
- }
196
-
197
- getMetadata(events) { /* ... */ }
198
- extractTurns(events) { /* ... */ }
199
- extractToolCalls(events) { /* ... */ }
200
- }
201
-
202
- // Add to parser-factory.js
203
- this.parsers.push(new MyCustomParser());
204
- ```
205
-
206
- ## Testing
207
-
208
- Run example:
209
- ```bash
210
- node examples/parser-usage.js
211
- ```
212
-
213
- ## API Documentation
214
-
215
- ### BaseSessionParser
216
-
217
- Base class for all parsers.
218
-
219
- #### Methods
220
-
221
- - `canParse(events)` - Check if this format can be parsed
222
- - `parse(events)` - Parse events and return unified format
223
- - `getMetadata(events)` - Extract session metadata
224
- - `extractTurns(events)` - Extract conversation turns
225
- - `extractToolCalls(events)` - Extract tool calls
226
-
227
- ### ParserFactory
228
-
229
- Parser factory for automatic format detection.
230
-
231
- #### Methods
232
-
233
- - `getParser(events)` - Return appropriate parser instance
234
- - `parse(events)` - Auto-detect and parse
235
- - `getParserType(events)` - Return parser type name
236
-
237
- ## License
238
-
239
- MIT
@@ -1,53 +0,0 @@
1
- /**
2
- * Base Session Parser Interface
3
- *
4
- * All session parsers must implement these methods
5
- */
6
- class BaseSessionParser {
7
- /**
8
- * Detect if this parser can handle the given events
9
- * @param {Array<Object>} _events - Raw events from jsonl
10
- * @returns {boolean}
11
- */
12
- canParse(_events) {
13
- throw new Error('canParse() must be implemented');
14
- }
15
-
16
- /**
17
- * Parse raw events into normalized format
18
- * @param {Array<Object>} _events - Raw events from jsonl
19
- * @returns {Object} Parsed session data
20
- */
21
- parse(_events) {
22
- throw new Error('parse() must be implemented');
23
- }
24
-
25
- /**
26
- * Get session metadata (sessionId, startTime, model, etc.)
27
- * @param {Array<Object>} _events - Raw events
28
- * @returns {Object} Session metadata
29
- */
30
- getMetadata(_events) {
31
- throw new Error('getMetadata() must be implemented');
32
- }
33
-
34
- /**
35
- * Extract turns (user message + assistant response pairs)
36
- * @param {Array<Object>} _events - Raw events
37
- * @returns {Array<Object>} Array of turns
38
- */
39
- extractTurns(_events) {
40
- throw new Error('extractTurns() must be implemented');
41
- }
42
-
43
- /**
44
- * Extract tool calls/executions
45
- * @param {Array<Object>} _events - Raw events
46
- * @returns {Array<Object>} Array of tool calls
47
- */
48
- extractToolCalls(_events) {
49
- throw new Error('extractToolCalls() must be implemented');
50
- }
51
- }
52
-
53
- module.exports = BaseSessionParser;
@@ -1,181 +0,0 @@
1
- const BaseSessionParser = require('./base-parser');
2
-
3
- /**
4
- * Claude Code Session Parser
5
- *
6
- * Parses events from Anthropic Claude Code CLI
7
- * Format: {type: "user"|"assistant", uuid: "...", parentUuid: "...", message: {...}}
8
- */
9
- class ClaudeSessionParser extends BaseSessionParser {
10
- canParse(events) {
11
- if (!events || events.length === 0) return false;
12
-
13
- // Check for Claude Code specific structure
14
- return events.some(e =>
15
- e.type && ['user', 'assistant'].includes(e.type) &&
16
- e.uuid && Object.prototype.hasOwnProperty.call(e, 'parentUuid') && e.sessionId
17
- );
18
- }
19
-
20
- parse(events) {
21
- return {
22
- metadata: this.getMetadata(events),
23
- turns: this.extractTurns(events),
24
- toolCalls: this.extractToolCalls(events),
25
- allEvents: events
26
- };
27
- }
28
-
29
- getMetadata(events) {
30
- const firstUserMessage = events.find(e => e.type === 'user');
31
- if (!firstUserMessage) return null;
32
-
33
- // Extract model from assistant messages
34
- const firstAssistant = events.find(e => e.type === 'assistant' && e.message?.model);
35
- const model = firstAssistant?.message?.model || 'unknown';
36
-
37
- return {
38
- sessionId: firstUserMessage.sessionId,
39
- startTime: firstUserMessage.timestamp,
40
- model: model,
41
- version: firstUserMessage.version,
42
- producer: 'claude-code',
43
- cwd: firstUserMessage.cwd,
44
- gitRoot: null, // Not available in Claude format
45
- branch: firstUserMessage.gitBranch,
46
- repository: null // Not available in Claude format
47
- };
48
- }
49
-
50
- extractTurns(events) {
51
- const turns = [];
52
- const messageEvents = events.filter(e =>
53
- ['user', 'assistant'].includes(e.type) &&
54
- e.type !== 'file-history-snapshot' &&
55
- e.type !== 'queue-operation'
56
- );
57
-
58
- // Build parent-child tree
59
- const eventMap = new Map();
60
- for (const event of messageEvents) {
61
- eventMap.set(event.uuid, event);
62
- }
63
-
64
- // Find root user messages (no parentUuid or parent is not a message)
65
- const rootMessages = messageEvents.filter(e =>
66
- e.type === 'user' && (!e.parentUuid || !eventMap.has(e.parentUuid))
67
- );
68
-
69
- for (const userMsg of rootMessages) {
70
- const turn = {
71
- turnId: userMsg.uuid,
72
- userMessage: {
73
- id: userMsg.uuid,
74
- content: this._extractMessageContent(userMsg.message),
75
- timestamp: userMsg.timestamp
76
- },
77
- assistantMessages: [],
78
- toolCalls: []
79
- };
80
-
81
- // Find all children of this user message
82
- this._collectAssistantResponses(userMsg.uuid, eventMap, turn);
83
-
84
- turns.push(turn);
85
- }
86
-
87
- return turns;
88
- }
89
-
90
- _collectAssistantResponses(parentUuid, eventMap, turn) {
91
- for (const [_uuid, event] of eventMap.entries()) {
92
- if (event.parentUuid === parentUuid) {
93
- if (event.type === 'assistant') {
94
- const assistantMsg = {
95
- id: event.uuid,
96
- messageId: event.message?.id,
97
- content: this._extractMessageContent(event.message),
98
- model: event.message?.model,
99
- timestamp: event.timestamp
100
- };
101
-
102
- // Extract tool calls from content
103
- const toolUseBlocks = this._extractToolUse(event.message);
104
- if (toolUseBlocks.length > 0) {
105
- assistantMsg.toolRequests = toolUseBlocks;
106
- turn.toolCalls.push(...toolUseBlocks.map(t => ({
107
- toolCallId: t.id,
108
- name: t.name,
109
- arguments: t.input,
110
- parentUuid: event.uuid
111
- })));
112
- }
113
-
114
- turn.assistantMessages.push(assistantMsg);
115
-
116
- // Recursively collect children (follow-up messages)
117
- this._collectAssistantResponses(event.uuid, eventMap, turn);
118
- }
119
- }
120
- }
121
- }
122
-
123
- _extractMessageContent(message) {
124
- if (!message || !message.content) return '';
125
-
126
- if (typeof message.content === 'string') {
127
- return message.content;
128
- }
129
-
130
- if (Array.isArray(message.content)) {
131
- return message.content
132
- .filter(block => block.type === 'text')
133
- .map(block => block.text)
134
- .join('\n');
135
- }
136
-
137
- return '';
138
- }
139
-
140
- _extractToolUse(message) {
141
- if (!message || !message.content || !Array.isArray(message.content)) {
142
- return [];
143
- }
144
-
145
- return message.content
146
- .filter(block => block.type === 'tool_use')
147
- .map(block => ({
148
- id: block.id,
149
- name: block.name,
150
- input: block.input
151
- }));
152
- }
153
-
154
- extractToolCalls(events) {
155
- const toolCalls = [];
156
-
157
- for (const event of events) {
158
- if (event.type === 'assistant' && event.message?.content) {
159
- const toolUseBlocks = this._extractToolUse(event.message);
160
-
161
- for (const tool of toolUseBlocks) {
162
- toolCalls.push({
163
- toolCallId: tool.id,
164
- name: tool.name,
165
- arguments: tool.input,
166
- parentUuid: event.uuid,
167
- timestamp: event.timestamp,
168
- // Claude format doesn't have separate execution events
169
- // Tool result would be in a following user message with tool_result
170
- result: null,
171
- exitCode: null
172
- });
173
- }
174
- }
175
- }
176
-
177
- return toolCalls;
178
- }
179
- }
180
-
181
- module.exports = ClaudeSessionParser;
@@ -1,143 +0,0 @@
1
- const BaseSessionParser = require('./base-parser');
2
-
3
- /**
4
- * Copilot CLI Session Parser
5
- *
6
- * Parses events from GitHub Copilot CLI (copilot-agent)
7
- * Format: {type: "session.start", data: {...}, id: "...", parentId: "..."}
8
- */
9
- class CopilotSessionParser extends BaseSessionParser {
10
- canParse(events) {
11
- if (!events || events.length === 0) return false;
12
-
13
- // Check for Copilot CLI specific event types
14
- const copilotEventTypes = [
15
- 'session.start',
16
- 'user.message',
17
- 'assistant.turn_start',
18
- 'assistant.message',
19
- 'tool.execution_start'
20
- ];
21
-
22
- return events.some(e =>
23
- e.type && copilotEventTypes.some(t => e.type.startsWith(t))
24
- );
25
- }
26
-
27
- parse(events) {
28
- return {
29
- metadata: this.getMetadata(events),
30
- turns: this.extractTurns(events),
31
- toolCalls: this.extractToolCalls(events),
32
- allEvents: events
33
- };
34
- }
35
-
36
- getMetadata(events) {
37
- const sessionStart = events.find(e => e.type === 'session.start');
38
- if (!sessionStart) return null;
39
-
40
- const data = sessionStart.data || {};
41
- return {
42
- sessionId: data.sessionId,
43
- startTime: data.startTime,
44
- model: data.selectedModel,
45
- version: data.copilotVersion,
46
- producer: data.producer,
47
- cwd: data.context?.cwd,
48
- gitRoot: data.context?.gitRoot,
49
- branch: data.context?.branch,
50
- repository: data.context?.repository
51
- };
52
- }
53
-
54
- extractTurns(events) {
55
- const turns = [];
56
- let currentTurn = null;
57
-
58
- for (const event of events) {
59
- if (event.type === 'user.message') {
60
- // Start new turn
61
- if (currentTurn) {
62
- turns.push(currentTurn);
63
- }
64
- currentTurn = {
65
- turnId: event.id,
66
- userMessage: {
67
- id: event.id,
68
- content: event.data?.content || '',
69
- transformedContent: event.data?.transformedContent,
70
- timestamp: event.timestamp
71
- },
72
- assistantMessages: [],
73
- toolCalls: []
74
- };
75
- } else if (event.type === 'assistant.message' && currentTurn) {
76
- currentTurn.assistantMessages.push({
77
- id: event.id,
78
- messageId: event.data?.messageId,
79
- content: event.data?.content || '',
80
- toolRequests: event.data?.toolRequests || [],
81
- reasoningText: event.data?.reasoningText,
82
- timestamp: event.timestamp
83
- });
84
- } else if (event.type.startsWith('tool.execution_') && currentTurn) {
85
- const toolCallId = event.data?.toolCallId;
86
- let toolCall = currentTurn.toolCalls.find(tc => tc.toolCallId === toolCallId);
87
-
88
- if (!toolCall) {
89
- toolCall = { toolCallId, events: [] };
90
- currentTurn.toolCalls.push(toolCall);
91
- }
92
-
93
- toolCall.events.push(event);
94
-
95
- if (event.type === 'tool.execution_start') {
96
- toolCall.name = event.data?.toolName;
97
- toolCall.arguments = event.data?.arguments;
98
- } else if (event.type === 'tool.execution_complete') {
99
- toolCall.result = event.data?.result;
100
- toolCall.exitCode = event.data?.exitCode;
101
- }
102
- }
103
- }
104
-
105
- // Push last turn
106
- if (currentTurn) {
107
- turns.push(currentTurn);
108
- }
109
-
110
- return turns;
111
- }
112
-
113
- extractToolCalls(events) {
114
- const toolCalls = [];
115
- const toolCallMap = new Map();
116
-
117
- for (const event of events) {
118
- if (event.type === 'tool.execution_start') {
119
- const toolCallId = event.data?.toolCallId;
120
- toolCallMap.set(toolCallId, {
121
- toolCallId,
122
- name: event.data?.toolName,
123
- arguments: event.data?.arguments,
124
- startEvent: event,
125
- completeEvent: null
126
- });
127
- } else if (event.type === 'tool.execution_complete') {
128
- const toolCallId = event.data?.toolCallId;
129
- const toolCall = toolCallMap.get(toolCallId);
130
- if (toolCall) {
131
- toolCall.completeEvent = event;
132
- toolCall.result = event.data?.result;
133
- toolCall.exitCode = event.data?.exitCode;
134
- toolCalls.push(toolCall);
135
- }
136
- }
137
- }
138
-
139
- return toolCalls;
140
- }
141
- }
142
-
143
- module.exports = CopilotSessionParser;
@@ -1,15 +0,0 @@
1
- const BaseSessionParser = require('./base-parser');
2
- const CopilotSessionParser = require('./copilot-parser');
3
- const ClaudeSessionParser = require('./claude-parser');
4
- const PiMonoParser = require('./pi-mono-parser');
5
- const VsCodeParser = require('./vscode-parser');
6
- const ParserFactory = require('./parser-factory');
7
-
8
- module.exports = {
9
- BaseSessionParser,
10
- CopilotSessionParser,
11
- ClaudeSessionParser,
12
- PiMonoParser,
13
- VsCodeParser,
14
- ParserFactory
15
- };
@@ -1,77 +0,0 @@
1
- const CopilotSessionParser = require('./copilot-parser');
2
- const ClaudeSessionParser = require('./claude-parser');
3
- const PiMonoParser = require('./pi-mono-parser');
4
-
5
- /**
6
- * Parser Factory
7
- *
8
- * Automatically detects the session format and returns the appropriate parser
9
- */
10
- class ParserFactory {
11
- constructor() {
12
- this.parsers = [
13
- new CopilotSessionParser(),
14
- new ClaudeSessionParser(),
15
- new PiMonoParser()
16
- ];
17
-
18
- // Name-to-parser mapping
19
- this.parserMap = {
20
- 'copilot': new CopilotSessionParser(),
21
- 'claude': new ClaudeSessionParser(),
22
- 'pi-mono': new PiMonoParser()
23
- };
24
- }
25
-
26
- /**
27
- * Get parser by name or auto-detect from events
28
- * @param {string|Array<Object>} nameOrEvents - Parser name or events array
29
- * @returns {BaseSessionParser|null}
30
- */
31
- getParser(nameOrEvents) {
32
- // If it's a string, get parser by name
33
- if (typeof nameOrEvents === 'string') {
34
- return this.parserMap[nameOrEvents] || null;
35
- }
36
-
37
- // Otherwise, auto-detect from events
38
- const events = nameOrEvents;
39
- for (const parser of this.parsers) {
40
- if (parser.canParse(events)) {
41
- return parser;
42
- }
43
- }
44
- return null;
45
- }
46
-
47
- /**
48
- * Parse events using the appropriate parser
49
- * @param {Array<Object>} events - Raw events from jsonl
50
- * @returns {Object|null} Parsed session data or null if no parser found
51
- */
52
- parse(events) {
53
- const parser = this.getParser(events);
54
- if (!parser) {
55
- return null;
56
- }
57
- return parser.parse(events);
58
- }
59
-
60
- /**
61
- * Get parser type name
62
- * @param {Array<Object>} events - Raw events from jsonl
63
- * @returns {string|null} Parser name ('copilot', 'claude', 'pi-mono') or null
64
- */
65
- getParserType(events) {
66
- const parser = this.getParser(events);
67
- if (!parser) return null;
68
-
69
- if (parser instanceof CopilotSessionParser) return 'copilot';
70
- if (parser instanceof ClaudeSessionParser) return 'claude';
71
- if (parser instanceof PiMonoParser) return 'pi-mono';
72
-
73
- return 'unknown';
74
- }
75
- }
76
-
77
- module.exports = ParserFactory;