f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
672 lines
17 KiB
Go
672 lines
17 KiB
Go
package parser
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
var _ SourceSet = (*openClaudeSourceSet)(nil)
|
|
|
|
type openClaudeSource struct {
|
|
Root string
|
|
Path string
|
|
}
|
|
|
|
type openClaudeSourceSet struct {
|
|
roots []string
|
|
}
|
|
|
|
func newOpenClaudeProviderFactory(def AgentDef) ProviderFactory {
|
|
return NewSourceSetFactory(
|
|
def,
|
|
openClaudeProviderCapabilities(),
|
|
func(cfg ProviderConfig) SourceSet {
|
|
return newOpenClaudeSourceSet(cfg.Roots)
|
|
},
|
|
)
|
|
}
|
|
|
|
func openClaudeProviderCapabilities() Capabilities {
|
|
return Capabilities{
|
|
Source: SourceCapabilities{
|
|
DiscoverSources: CapabilitySupported,
|
|
WatchSources: CapabilitySupported,
|
|
ClassifyChangedPath: CapabilitySupported,
|
|
FindSource: CapabilitySupported,
|
|
CompositeFingerprint: CapabilitySupported,
|
|
IncrementalAppend: CapabilityNotApplicable,
|
|
MultiSessionSource: CapabilityNotApplicable,
|
|
PerSessionErrors: CapabilityNotApplicable,
|
|
ExcludedSessions: CapabilityNotApplicable,
|
|
ForceReplaceOnParse: CapabilitySupported,
|
|
},
|
|
Content: ContentCapabilities{
|
|
FirstMessage: CapabilitySupported,
|
|
SessionName: CapabilitySupported,
|
|
Cwd: CapabilitySupported,
|
|
GitBranch: CapabilitySupported,
|
|
Relationships: CapabilitySupported,
|
|
Thinking: CapabilitySupported,
|
|
ToolCalls: CapabilitySupported,
|
|
ToolResults: CapabilitySupported,
|
|
PerMessageTokenUsage: CapabilitySupported,
|
|
TerminationStatus: CapabilitySupported,
|
|
MalformedLineCount: CapabilitySupported,
|
|
Model: CapabilitySupported,
|
|
StopReason: CapabilitySupported,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newOpenClaudeSourceSet(roots []string) openClaudeSourceSet {
|
|
return openClaudeSourceSet{roots: cleanJSONLRoots(roots)}
|
|
}
|
|
|
|
func (s openClaudeSourceSet) Discover(ctx context.Context) ([]SourceRef, error) {
|
|
var sources []SourceRef
|
|
seen := make(map[string]struct{})
|
|
for _, root := range s.roots {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, file := range ClaudeProjectSessionFiles(root) {
|
|
source, ok := s.discoveredSourceRef(root, file)
|
|
if !ok {
|
|
continue
|
|
}
|
|
addJSONLSource(source, &sources, seen)
|
|
}
|
|
}
|
|
sortJSONLSources(sources)
|
|
return sources, nil
|
|
}
|
|
|
|
func (s openClaudeSourceSet) discoveredSourceRef(
|
|
root string, file DiscoveredFile,
|
|
) (SourceRef, bool) {
|
|
return s.sourceRef(root, file.Path)
|
|
}
|
|
|
|
func (s openClaudeSourceSet) WatchPlan(context.Context) (WatchPlan, error) {
|
|
roots := make([]WatchRoot, 0, len(s.roots))
|
|
for _, root := range s.roots {
|
|
roots = append(roots, WatchRoot{
|
|
Path: root,
|
|
Recursive: true,
|
|
IncludeGlobs: []string{"*.jsonl"},
|
|
DebounceKey: string(AgentOpenClaude) + ":projects:" + root,
|
|
})
|
|
}
|
|
return WatchPlan{Roots: roots}, nil
|
|
}
|
|
|
|
func (s openClaudeSourceSet) SourcesForChangedPath(
|
|
ctx context.Context,
|
|
req ChangedPathRequest,
|
|
) ([]SourceRef, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
allowMissing := jsonlMissingPathFallbackAllowed(req) ||
|
|
claudeChangedPathPresentButUnstatable(req.Path)
|
|
if req.WatchRoot != "" {
|
|
root := filepath.Clean(req.WatchRoot)
|
|
if !s.hasRoot(root) {
|
|
return nil, nil
|
|
}
|
|
source, ok := s.sourceForChangedPath(root, req.Path, allowMissing)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return []SourceRef{source}, nil
|
|
}
|
|
for _, root := range s.roots {
|
|
source, ok := s.sourceForChangedPath(root, req.Path, allowMissing)
|
|
if ok {
|
|
return []SourceRef{source}, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (s openClaudeSourceSet) FindSource(
|
|
ctx context.Context,
|
|
req FindSourceRequest,
|
|
) (SourceRef, bool, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return SourceRef{}, false, err
|
|
}
|
|
for _, path := range []string{req.StoredFilePath, req.FingerprintKey} {
|
|
if path == "" {
|
|
continue
|
|
}
|
|
for _, root := range s.roots {
|
|
if source, ok := s.sourceForPath(root, path); ok {
|
|
return source, true, nil
|
|
}
|
|
}
|
|
}
|
|
if req.RawSessionID == "" {
|
|
return SourceRef{}, false, nil
|
|
}
|
|
for _, root := range s.roots {
|
|
path := claudeFindSourceFile(root, req.RawSessionID)
|
|
if path == "" {
|
|
continue
|
|
}
|
|
if source, ok := s.sourceRef(root, path); ok {
|
|
return source, true, nil
|
|
}
|
|
}
|
|
return SourceRef{}, false, nil
|
|
}
|
|
|
|
func (s openClaudeSourceSet) Fingerprint(
|
|
ctx context.Context,
|
|
source SourceRef,
|
|
) (SourceFingerprint, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return SourceFingerprint{}, err
|
|
}
|
|
path, ok := s.pathFromSource(source)
|
|
if !ok {
|
|
return SourceFingerprint{}, fmt.Errorf("openclaude source path unavailable")
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return SourceFingerprint{}, fmt.Errorf("stat %s: %w", path, err)
|
|
}
|
|
if info.IsDir() {
|
|
return SourceFingerprint{}, fmt.Errorf("stat %s: source is a directory", path)
|
|
}
|
|
hash, err := hashJSONLSourceFile(path)
|
|
if err != nil {
|
|
return SourceFingerprint{}, err
|
|
}
|
|
inode, device := sourceFileIdentity(info)
|
|
return SourceFingerprint{
|
|
Key: firstNonEmptyJSONLString(source.FingerprintKey, source.Key, path),
|
|
Size: info.Size(),
|
|
MTimeNS: info.ModTime().UnixNano(),
|
|
Inode: inode,
|
|
Device: device,
|
|
Hash: hash,
|
|
}, nil
|
|
}
|
|
|
|
func (s openClaudeSourceSet) Parse(
|
|
ctx context.Context,
|
|
req ParseRequest,
|
|
) (ParseOutcome, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return ParseOutcome{}, err
|
|
}
|
|
path, ok := s.pathFromSource(req.Source)
|
|
if !ok {
|
|
return ParseOutcome{}, fmt.Errorf("openclaude source path unavailable")
|
|
}
|
|
machine := firstNonEmptyJSONLString(req.Machine)
|
|
project := GetProjectName(firstNonEmptyJSONLString(
|
|
req.Source.ProjectHint,
|
|
filepath.Base(filepath.Dir(path)),
|
|
))
|
|
sess, msgs, err := parseOpenClaudeSession(path, project, machine)
|
|
if err != nil {
|
|
return ParseOutcome{}, err
|
|
}
|
|
if sess == nil {
|
|
return ParseOutcome{
|
|
ResultSetComplete: true,
|
|
SkipReason: SkipNoSession,
|
|
}, nil
|
|
}
|
|
if req.Fingerprint.Hash != "" {
|
|
sess.File.Hash = req.Fingerprint.Hash
|
|
}
|
|
return ParseOutcome{
|
|
Results: []ParseResultOutcome{{
|
|
Result: ParseResult{
|
|
Session: *sess,
|
|
Messages: msgs,
|
|
},
|
|
DataVersion: DataVersionCurrent,
|
|
}},
|
|
ResultSetComplete: true,
|
|
ForceReplace: true,
|
|
}, nil
|
|
}
|
|
|
|
func (s openClaudeSourceSet) pathFromSource(source SourceRef) (string, bool) {
|
|
switch src := source.Opaque.(type) {
|
|
case openClaudeSource:
|
|
return src.Path, src.Path != ""
|
|
case *openClaudeSource:
|
|
if src != nil && src.Path != "" {
|
|
return src.Path, true
|
|
}
|
|
case MaterializedFileSource:
|
|
return src.Path, src.Path != ""
|
|
}
|
|
for _, candidate := range []string{
|
|
source.DisplayPath,
|
|
source.FingerprintKey,
|
|
source.Key,
|
|
} {
|
|
for _, root := range s.roots {
|
|
if ref, ok := s.sourceForPath(root, candidate); ok {
|
|
src := ref.Opaque.(openClaudeSource)
|
|
return src.Path, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (s openClaudeSourceSet) sourceForPath(root, path string) (SourceRef, bool) {
|
|
return s.sourceForChangedPath(root, path, false)
|
|
}
|
|
|
|
func (s openClaudeSourceSet) sourceForChangedPath(
|
|
root, path string, allowMissing bool,
|
|
) (SourceRef, bool) {
|
|
root = filepath.Clean(root)
|
|
path = filepath.Clean(path)
|
|
if allowMissing {
|
|
return s.sourceRefFromPath(root, path)
|
|
}
|
|
return s.sourceRef(root, path)
|
|
}
|
|
|
|
func (s openClaudeSourceSet) sourceRef(root, path string) (SourceRef, bool) {
|
|
root = filepath.Clean(root)
|
|
path = filepath.Clean(path)
|
|
if !IsRegularFile(path) {
|
|
return SourceRef{}, false
|
|
}
|
|
return s.sourceRefFromPath(root, path)
|
|
}
|
|
|
|
func (s openClaudeSourceSet) sourceRefFromPath(
|
|
root, path string,
|
|
) (SourceRef, bool) {
|
|
root = filepath.Clean(root)
|
|
path = filepath.Clean(path)
|
|
project, ok := claudeProjectHintFromPath(root, path)
|
|
if !ok {
|
|
return SourceRef{}, false
|
|
}
|
|
return SourceRef{
|
|
Provider: AgentOpenClaude,
|
|
Key: path,
|
|
DisplayPath: path,
|
|
FingerprintKey: path,
|
|
ProjectHint: project,
|
|
Opaque: openClaudeSource{
|
|
Root: root,
|
|
Path: path,
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func (s openClaudeSourceSet) hasRoot(root string) bool {
|
|
for _, configured := range s.roots {
|
|
if samePath(root, configured) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseOpenClaudeSession(
|
|
path, project, machine string,
|
|
) (*ParsedSession, []ParsedMessage, error) {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("stat %s: %w", path, err)
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("open %s: %w", path, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
lr := newLineReader(f, maxLineSize)
|
|
defer releaseLineReader(lr)
|
|
lastLine := ""
|
|
malformedLines := 0
|
|
ordinal := 0
|
|
var (
|
|
messages []ParsedMessage
|
|
queuedCommands []claudeQueuedCommand
|
|
startedAt time.Time
|
|
endedAt time.Time
|
|
firstUser string
|
|
userCount int
|
|
sessionID = strings.TrimSuffix(filepath.Base(path), ".jsonl")
|
|
sessionName string
|
|
customTitle string
|
|
aiTitle string
|
|
cwd string
|
|
gitBranch string
|
|
)
|
|
|
|
for {
|
|
line, ok := lr.next()
|
|
if !ok {
|
|
break
|
|
}
|
|
lastLine = line
|
|
if !gjson.Valid(line) {
|
|
malformedLines++
|
|
continue
|
|
}
|
|
|
|
switch strings.TrimSpace(gjson.Get(line, "type").Str) {
|
|
case "attachment":
|
|
if qc, ok := extractOpenClaudeQueuedCommand(line); ok {
|
|
queuedCommands = append(queuedCommands, qc)
|
|
}
|
|
continue
|
|
case "custom-title":
|
|
if v := strings.TrimSpace(gjson.Get(line, "customTitle").Str); v != "" {
|
|
customTitle = v
|
|
}
|
|
continue
|
|
case "ai-title":
|
|
if v := strings.TrimSpace(gjson.Get(line, "aiTitle").Str); v != "" {
|
|
aiTitle = v
|
|
}
|
|
continue
|
|
}
|
|
|
|
if ts := extractTimestamp(line); !ts.IsZero() {
|
|
if startedAt.IsZero() || ts.Before(startedAt) {
|
|
startedAt = ts
|
|
}
|
|
if ts.After(endedAt) {
|
|
endedAt = ts
|
|
}
|
|
}
|
|
|
|
if sessionName == "" {
|
|
if v := strings.TrimSpace(gjson.Get(line, "sessionName").Str); v != "" {
|
|
sessionName = v
|
|
}
|
|
}
|
|
if cwd == "" {
|
|
if v := strings.TrimSpace(gjson.Get(line, "cwd").Str); v != "" {
|
|
cwd = v
|
|
}
|
|
}
|
|
if gitBranch == "" {
|
|
if v := strings.TrimSpace(gjson.Get(line, "gitBranch").Str); v != "" {
|
|
gitBranch = v
|
|
}
|
|
}
|
|
|
|
if isOpenClaudeCompactBoundary(line) {
|
|
content, _, _, _, _, _ := ExtractTextContent(
|
|
gjson.Get(line, "message.content"),
|
|
)
|
|
messages = append(messages, ParsedMessage{
|
|
Ordinal: ordinal,
|
|
Role: RoleAssistant,
|
|
Content: content,
|
|
Timestamp: extractTimestamp(line),
|
|
IsSystem: true,
|
|
ContentLength: len(content),
|
|
SourceType: "system",
|
|
SourceSubtype: "compact_boundary",
|
|
SourceUUID: gjson.Get(line, "uuid").Str,
|
|
SourceParentUUID: gjson.Get(line, "parentUuid").Str,
|
|
IsCompactBoundary: true,
|
|
IsSidechain: gjson.Get(line, "isSidechain").Bool(),
|
|
})
|
|
ordinal++
|
|
continue
|
|
}
|
|
|
|
role := strings.TrimSpace(gjson.Get(line, "message.role").Str)
|
|
if role == "" {
|
|
role = strings.TrimSpace(gjson.Get(line, "type").Str)
|
|
}
|
|
switch role {
|
|
case "user", "assistant", "system":
|
|
default:
|
|
continue
|
|
}
|
|
if role == "user" && gjson.Get(line, "isMeta").Bool() {
|
|
continue
|
|
}
|
|
|
|
content := gjson.Get(line, "message.content")
|
|
text, thinkingText, hasThinking, hasToolUse, toolCalls, toolResults :=
|
|
ExtractTextContent(content)
|
|
if strings.TrimSpace(text) == "" && len(toolResults) == 0 &&
|
|
len(toolCalls) == 0 && role != "system" {
|
|
continue
|
|
}
|
|
|
|
if role == "system" {
|
|
messages = append(messages, ParsedMessage{
|
|
Ordinal: ordinal,
|
|
Role: RoleSystem,
|
|
Content: text,
|
|
ThinkingText: thinkingText,
|
|
Timestamp: extractTimestamp(line),
|
|
HasThinking: hasThinking,
|
|
HasToolUse: hasToolUse,
|
|
IsSystem: true,
|
|
ContentLength: len(text),
|
|
ToolCalls: toolCalls,
|
|
ToolResults: toolResults,
|
|
SourceType: "system",
|
|
SourceSubtype: strings.TrimSpace(gjson.Get(line, "subtype").Str),
|
|
SourceUUID: gjson.Get(line, "uuid").Str,
|
|
SourceParentUUID: gjson.Get(line, "parentUuid").Str,
|
|
IsSidechain: gjson.Get(line, "isSidechain").Bool(),
|
|
})
|
|
ordinal++
|
|
continue
|
|
}
|
|
|
|
if role == "user" && strings.TrimSpace(text) == "" &&
|
|
len(toolResults) == 0 {
|
|
continue
|
|
}
|
|
|
|
msg := ParsedMessage{
|
|
Ordinal: ordinal,
|
|
Role: RoleType(role),
|
|
Content: text,
|
|
ThinkingText: thinkingText,
|
|
Timestamp: extractTimestamp(line),
|
|
HasThinking: hasThinking,
|
|
HasToolUse: hasToolUse,
|
|
ContentLength: len(text),
|
|
ToolCalls: toolCalls,
|
|
ToolResults: toolResults,
|
|
SourceType: role,
|
|
SourceUUID: gjson.Get(line, "uuid").Str,
|
|
SourceParentUUID: gjson.Get(line, "parentUuid").Str,
|
|
IsSidechain: gjson.Get(line, "isSidechain").Bool(),
|
|
tokenPresenceKnown: role == "assistant",
|
|
}
|
|
if role == "assistant" {
|
|
extractOpenClaudeTokenFields(&msg, line)
|
|
msg.StopReason = gjson.Get(line, "message.stop_reason").Str
|
|
}
|
|
messages = append(messages, msg)
|
|
ordinal++
|
|
if role == "user" && strings.TrimSpace(text) != "" {
|
|
userCount++
|
|
if firstUser == "" {
|
|
firstUser = truncate(strings.ReplaceAll(text, "\n", " "), 300)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := lr.Err(); err != nil {
|
|
return nil, nil, fmt.Errorf("reading %s: %w", path, err)
|
|
}
|
|
|
|
isTruncated := lastLine != "" &&
|
|
strings.TrimSpace(lastLine) != "" &&
|
|
!gjson.Valid(lastLine) &&
|
|
!fileEndsWithNewline(f, info.Size())
|
|
|
|
if len(messages) == 0 && len(queuedCommands) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
if len(queuedCommands) > 0 {
|
|
messages = mergeQueuedCommands(messages, queuedCommands, 0)
|
|
firstUser, userCount = firstMessageAndUserCount(messages)
|
|
for _, qc := range queuedCommands {
|
|
if qc.timestamp.After(endedAt) {
|
|
endedAt = qc.timestamp
|
|
}
|
|
if !qc.timestamp.IsZero() &&
|
|
(startedAt.IsZero() || qc.timestamp.Before(startedAt)) {
|
|
startedAt = qc.timestamp
|
|
}
|
|
}
|
|
}
|
|
|
|
if customTitle != "" {
|
|
sessionName = customTitle
|
|
} else if aiTitle != "" {
|
|
sessionName = aiTitle
|
|
}
|
|
|
|
project = firstNonEmptyJSONLString(project, GetProjectName(filepath.Base(filepath.Dir(path))))
|
|
if project == "" {
|
|
project = "unknown"
|
|
}
|
|
|
|
parentSessionID, relationshipType := openClaudeRelationship(path, sessionID)
|
|
sess := &ParsedSession{
|
|
ID: openClaudeSessionID(sessionID),
|
|
Project: project,
|
|
Machine: machine,
|
|
Agent: AgentOpenClaude,
|
|
ParentSessionID: parentSessionID,
|
|
RelationshipType: relationshipType,
|
|
Cwd: cwd,
|
|
GitBranch: gitBranch,
|
|
FirstMessage: firstUser,
|
|
SessionName: sessionName,
|
|
StartedAt: startedAt,
|
|
EndedAt: endedAt,
|
|
MessageCount: len(messages),
|
|
UserMessageCount: userCount,
|
|
MalformedLines: malformedLines,
|
|
IsTruncated: isTruncated,
|
|
File: FileInfo{
|
|
Path: path,
|
|
Size: info.Size(),
|
|
Mtime: info.ModTime().UnixNano(),
|
|
},
|
|
}
|
|
accumulateMessageTokenUsage(sess, messages)
|
|
sess.TerminationStatus = Classify(
|
|
openClaudeSemanticMessages(messages),
|
|
lastAssistantStopReason(openClaudeSemanticMessages(messages)),
|
|
isTruncated,
|
|
)
|
|
return sess, messages, nil
|
|
}
|
|
|
|
func openClaudeRelationship(
|
|
path, sessionID string,
|
|
) (string, RelationshipType) {
|
|
parent := claudeCompanionParentSessionID(path, sessionID)
|
|
if parent == "" {
|
|
return "", RelNone
|
|
}
|
|
return openClaudeSessionID(parent), RelSubagent
|
|
}
|
|
|
|
func openClaudeSemanticMessages(messages []ParsedMessage) []ParsedMessage {
|
|
filtered := make([]ParsedMessage, 0, len(messages))
|
|
for _, msg := range messages {
|
|
if msg.IsSystem {
|
|
continue
|
|
}
|
|
filtered = append(filtered, msg)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func extractOpenClaudeQueuedCommand(line string) (claudeQueuedCommand, bool) {
|
|
attachment := gjson.Get(line, "attachment")
|
|
if attachment.Get("type").Str != "queued_command" {
|
|
return claudeQueuedCommand{}, false
|
|
}
|
|
if attachment.Get("commandMode").Str != "prompt" {
|
|
return claudeQueuedCommand{}, false
|
|
}
|
|
if attachment.Get("isMeta").Bool() || attachment.Get("origin").Exists() {
|
|
return claudeQueuedCommand{}, false
|
|
}
|
|
|
|
prompt, _, _, _, _, _ := ExtractTextContent(attachment.Get("prompt"))
|
|
if strings.TrimSpace(prompt) == "" {
|
|
return claudeQueuedCommand{}, false
|
|
}
|
|
|
|
return claudeQueuedCommand{
|
|
prompt: prompt,
|
|
timestamp: extractTimestamp(line),
|
|
}, true
|
|
}
|
|
|
|
func openClaudeSessionID(id string) string {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" || strings.HasPrefix(id, "openclaude:") {
|
|
return id
|
|
}
|
|
return "openclaude:" + id
|
|
}
|
|
|
|
func isOpenClaudeCompactBoundary(line string) bool {
|
|
if gjson.Get(line, "isCompactSummary").Bool() {
|
|
return true
|
|
}
|
|
if gjson.Get(line, "compact_boundary").Bool() {
|
|
return true
|
|
}
|
|
switch strings.TrimSpace(gjson.Get(line, "subtype").Str) {
|
|
case "compact_boundary", "compact-summary", "compact_summary":
|
|
return true
|
|
}
|
|
return strings.TrimSpace(gjson.Get(line, "type").Str) == "compact_boundary"
|
|
}
|
|
|
|
func extractOpenClaudeTokenFields(msg *ParsedMessage, line string) {
|
|
msg.Model = gjson.Get(line, "message.model").String()
|
|
|
|
usageResult := gjson.Get(line, "message.usage")
|
|
if usageResult.Exists() {
|
|
msg.TokenUsage = json.RawMessage(usageResult.Raw)
|
|
msg.HasOutputTokens = usageResult.Get("output_tokens").Exists()
|
|
msg.HasContextTokens = usageResult.Get("input_tokens").Exists() ||
|
|
usageResult.Get("cache_creation_input_tokens").Exists() ||
|
|
usageResult.Get("cache_read_input_tokens").Exists()
|
|
|
|
input := int(usageResult.Get("input_tokens").Int())
|
|
cacheCreation := int(usageResult.Get("cache_creation_input_tokens").Int())
|
|
cacheRead := int(usageResult.Get("cache_read_input_tokens").Int())
|
|
msg.OutputTokens = int(usageResult.Get("output_tokens").Int())
|
|
msg.ContextTokens = input + cacheCreation + cacheRead
|
|
}
|
|
}
|