-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser.go
More file actions
172 lines (146 loc) · 3.99 KB
/
parser.go
File metadata and controls
172 lines (146 loc) · 3.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package main
import (
"encoding/json"
"path/filepath"
"strings"
)
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// SessionImage is the JSON structure sent over WebSocket to the browser.
type SessionImage struct {
Filename string `json:"filename"`
SessionID string `json:"sessionId"`
ProjectKey string `json:"-"`
Title string `json:"title"`
UpdatedAt string `json:"updatedAt"`
}
// PromptWithSession carries a prompt along with session metadata through the pipeline.
type PromptWithSession struct {
Prompt string
SessionID string
ProjectKey string
Title string
}
// rawEntry represents a single line in the JSONL log.
type rawEntry struct {
Type string `json:"type"`
Message json.RawMessage `json:"message"`
}
// rawMessage is the message field inside a rawEntry.
type rawMessage struct {
Content json.RawMessage `json:"content"`
}
// contentBlock represents one element of the assistant's content array.
type contentBlock struct {
Type string `json:"type"`
Text string `json:"text"`
}
// ParseJSONL parses JSONL bytes and extracts user/assistant conversation messages.
func ParseJSONL(data []byte) []Message {
var messages []Message
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var entry rawEntry
if err := json.Unmarshal([]byte(line), &entry); err != nil {
continue
}
switch entry.Type {
case "user":
msg := parseUserEntry(entry.Message)
if msg != nil {
messages = append(messages, *msg)
}
case "assistant":
msg := parseAssistantEntry(entry.Message)
if msg != nil {
messages = append(messages, *msg)
}
}
}
return messages
}
func parseUserEntry(raw json.RawMessage) *Message {
if raw == nil {
return nil
}
var msg rawMessage
if err := json.Unmarshal(raw, &msg); err != nil {
return nil
}
// content is a plain string → user's direct input
var strContent string
if err := json.Unmarshal(msg.Content, &strContent); err == nil {
strContent = strings.TrimSpace(strContent)
if strContent != "" {
return &Message{Role: "user", Content: strContent}
}
return nil
}
// content is an array → likely tool_result, skip
return nil
}
func parseAssistantEntry(raw json.RawMessage) *Message {
if raw == nil {
return nil
}
var msg rawMessage
if err := json.Unmarshal(raw, &msg); err != nil {
return nil
}
// content should be an array of blocks
var blocks []contentBlock
if err := json.Unmarshal(msg.Content, &blocks); err != nil {
return nil
}
var textParts []string
for _, b := range blocks {
if b.Type == "text" && strings.TrimSpace(b.Text) != "" {
textParts = append(textParts, strings.TrimSpace(b.Text))
}
}
if len(textParts) == 0 {
return nil
}
return &Message{
Role: "assistant",
Content: strings.Join(textParts, "\n"),
}
}
// TailMessages returns the last n messages from the slice.
func TailMessages(msgs []Message, n int) []Message {
if len(msgs) <= n {
return msgs
}
return msgs[len(msgs)-n:]
}
// ExtractTitle returns the first real user message's text, truncated to maxLen runes.
// Messages starting with '<' are skipped as they are typically system/tool content.
func ExtractTitle(messages []Message, maxLen int) string {
for _, m := range messages {
if m.Role == "user" && !strings.HasPrefix(m.Content, "<") {
r := []rune(m.Content)
if len(r) > maxLen {
return string(r[:maxLen]) + "..."
}
return m.Content
}
}
return ""
}
// SessionIDFromPath extracts a session ID from a JSONL file path.
// It returns the basename without the .jsonl extension.
func SessionIDFromPath(path string) string {
base := filepath.Base(path)
return strings.TrimSuffix(base, ".jsonl")
}
// ProjectKeyFromPath extracts the project directory name from a JSONL file path.
// For example, given "/Users/x/.claude/projects/-Users-x-myproject/abc.jsonl",
// it returns "-Users-x-myproject".
func ProjectKeyFromPath(path string) string {
return filepath.Base(filepath.Dir(path))
}