@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/lib/parsers/README.md
DELETED
|
@@ -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;
|
package/lib/parsers/index.js
DELETED
|
@@ -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;
|