chore: import upstream snapshot with attribution
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
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
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
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type commandCodeMeta struct {
|
||||
Title string `json:"title"`
|
||||
UserRenamed bool `json:"userRenamed"`
|
||||
ProjectPath string `json:"projectPath"`
|
||||
Cwd string `json:"cwd"`
|
||||
}
|
||||
|
||||
// parseSession parses a Command Code JSONL transcript.
|
||||
func (p *commandCodeProvider) parseSession(
|
||||
path, 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)
|
||||
var (
|
||||
sessionID string
|
||||
cwd string
|
||||
gitBranch string
|
||||
firstMessage string
|
||||
startedAt time.Time
|
||||
endedAt time.Time
|
||||
ordinal int
|
||||
userCount int
|
||||
malformedLines int
|
||||
messages []ParsedMessage
|
||||
)
|
||||
|
||||
for {
|
||||
line, ok := lr.next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if !gjson.Valid(line) {
|
||||
malformedLines++
|
||||
continue
|
||||
}
|
||||
|
||||
root := gjson.Parse(line)
|
||||
if sessionID == "" {
|
||||
sessionID = root.Get("sessionId").Str
|
||||
}
|
||||
if cwd == "" {
|
||||
cwd = commandCodeCwd(root)
|
||||
}
|
||||
if gitBranch == "" {
|
||||
gitBranch = root.Get("gitBranch").Str
|
||||
}
|
||||
|
||||
ts := parseTimestamp(root.Get("timestamp").Str)
|
||||
if !ts.IsZero() {
|
||||
if startedAt.IsZero() || ts.Before(startedAt) {
|
||||
startedAt = ts
|
||||
}
|
||||
if endedAt.IsZero() || ts.After(endedAt) {
|
||||
endedAt = ts
|
||||
}
|
||||
}
|
||||
|
||||
role := root.Get("role").Str
|
||||
content := root.Get("content")
|
||||
text, thinking, hasThinking, hasToolUse, toolCalls, toolResults :=
|
||||
extractCommandCodeContent(content)
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
switch role {
|
||||
case "user":
|
||||
if text == "" && len(toolResults) == 0 {
|
||||
continue
|
||||
}
|
||||
if firstMessage == "" && text != "" {
|
||||
firstMessage = truncate(
|
||||
strings.ReplaceAll(text, "\n", " "),
|
||||
300,
|
||||
)
|
||||
}
|
||||
messages = append(messages, ParsedMessage{
|
||||
Ordinal: ordinal,
|
||||
Role: RoleUser,
|
||||
Content: text,
|
||||
ThinkingText: thinking,
|
||||
Timestamp: ts,
|
||||
HasThinking: hasThinking,
|
||||
HasToolUse: hasToolUse,
|
||||
ContentLength: len(text),
|
||||
ToolCalls: toolCalls,
|
||||
ToolResults: toolResults,
|
||||
})
|
||||
ordinal++
|
||||
if text != "" {
|
||||
userCount++
|
||||
}
|
||||
|
||||
case "assistant":
|
||||
if text == "" && !hasThinking &&
|
||||
len(toolCalls) == 0 && len(toolResults) == 0 {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, ParsedMessage{
|
||||
Ordinal: ordinal,
|
||||
Role: RoleAssistant,
|
||||
Content: text,
|
||||
ThinkingText: thinking,
|
||||
Timestamp: ts,
|
||||
HasThinking: hasThinking,
|
||||
HasToolUse: hasToolUse,
|
||||
ContentLength: len(text),
|
||||
ToolCalls: toolCalls,
|
||||
ToolResults: toolResults,
|
||||
})
|
||||
ordinal++
|
||||
|
||||
case "tool":
|
||||
if len(toolResults) == 0 {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, ParsedMessage{
|
||||
Ordinal: ordinal,
|
||||
Role: RoleUser,
|
||||
Timestamp: ts,
|
||||
ContentLength: 0,
|
||||
ToolResults: toolResults,
|
||||
})
|
||||
ordinal++
|
||||
}
|
||||
}
|
||||
|
||||
if err := lr.Err(); err != nil {
|
||||
return nil, nil, fmt.Errorf("reading command-code %s: %w", path, err)
|
||||
}
|
||||
if len(messages) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
meta := loadCommandCodeMeta(path)
|
||||
if meta != nil {
|
||||
if cwd == "" {
|
||||
cwd = commandCodeFirstNonEmpty(meta.Cwd, meta.ProjectPath)
|
||||
}
|
||||
if firstMessage == "" {
|
||||
firstMessage = meta.Title
|
||||
}
|
||||
}
|
||||
if sessionID == "" {
|
||||
sessionID = strings.TrimSuffix(filepath.Base(path), ".jsonl")
|
||||
}
|
||||
|
||||
project := ExtractProjectFromCwd(cwd)
|
||||
if project == "" {
|
||||
project = NormalizeName(filepath.Base(filepath.Dir(path)))
|
||||
}
|
||||
|
||||
sessionName := ""
|
||||
if meta != nil {
|
||||
sessionName = meta.Title
|
||||
}
|
||||
|
||||
sess := &ParsedSession{
|
||||
ID: "commandcode:" + sessionID,
|
||||
Project: project,
|
||||
Machine: machine,
|
||||
Agent: AgentCommandCode,
|
||||
Cwd: cwd,
|
||||
GitBranch: gitBranch,
|
||||
SourceVersion: "2",
|
||||
MalformedLines: malformedLines,
|
||||
FirstMessage: firstMessage,
|
||||
SessionName: sessionName,
|
||||
StartedAt: startedAt,
|
||||
EndedAt: endedAt,
|
||||
MessageCount: len(messages),
|
||||
UserMessageCount: userCount,
|
||||
File: FileInfo{
|
||||
Path: path,
|
||||
Size: info.Size(),
|
||||
Mtime: info.ModTime().UnixNano(),
|
||||
},
|
||||
}
|
||||
return sess, messages, nil
|
||||
}
|
||||
|
||||
func loadCommandCodeMeta(path string) *commandCodeMeta {
|
||||
metaPath := strings.TrimSuffix(path, ".jsonl") + ".meta.json"
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var meta commandCodeMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &meta
|
||||
}
|
||||
|
||||
func commandCodeCwd(root gjson.Result) string {
|
||||
for _, key := range []string{
|
||||
"metadata.cwd",
|
||||
"metadata.projectPath",
|
||||
"metadata.context.cwd",
|
||||
"cwd",
|
||||
} {
|
||||
if v := root.Get(key).Str; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractCommandCodeContent(
|
||||
content gjson.Result,
|
||||
) (string, string, bool, bool, []ParsedToolCall, []ParsedToolResult) {
|
||||
if content.Type == gjson.String {
|
||||
return content.Str, "", false, false, nil, nil
|
||||
}
|
||||
if !content.IsArray() {
|
||||
return "", "", false, false, nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
textParts []string
|
||||
thinkingParts []string
|
||||
toolCalls []ParsedToolCall
|
||||
toolResults []ParsedToolResult
|
||||
hasThinking bool
|
||||
hasToolUse bool
|
||||
)
|
||||
|
||||
content.ForEach(func(_, block gjson.Result) bool {
|
||||
switch block.Get("type").Str {
|
||||
case "text":
|
||||
if text := block.Get("text").Str; text != "" {
|
||||
textParts = append(textParts, text)
|
||||
}
|
||||
case "reasoning", "thinking":
|
||||
thinking := commandCodeFirstNonEmpty(
|
||||
block.Get("text").Str,
|
||||
block.Get("thinking").Str,
|
||||
)
|
||||
if thinking != "" {
|
||||
hasThinking = true
|
||||
thinkingParts = append(thinkingParts, thinking)
|
||||
}
|
||||
case "tool-call", "tool_use":
|
||||
toolName := commandCodeFirstNonEmpty(
|
||||
block.Get("toolName").Str,
|
||||
block.Get("name").Str,
|
||||
)
|
||||
if toolName == "" {
|
||||
return true
|
||||
}
|
||||
hasToolUse = true
|
||||
input := block.Get("input")
|
||||
inputJSON := input.Raw
|
||||
if inputJSON == "" {
|
||||
inputJSON = "{}"
|
||||
}
|
||||
toolCalls = append(toolCalls, ParsedToolCall{
|
||||
ToolUseID: blockID(block, "toolCallId", "id"),
|
||||
ToolName: toolName,
|
||||
Category: NormalizeToolCategory(toolName),
|
||||
InputJSON: inputJSON,
|
||||
})
|
||||
case "tool-result", "tool_result":
|
||||
toolUseID := blockID(block, "toolCallId", "tool_use_id")
|
||||
contentRaw, contentLen := commandCodeToolResultContent(block)
|
||||
if toolUseID == "" || contentRaw == "" {
|
||||
return true
|
||||
}
|
||||
toolResults = append(toolResults, ParsedToolResult{
|
||||
ToolUseID: toolUseID,
|
||||
ContentLength: contentLen,
|
||||
ContentRaw: contentRaw,
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return strings.Join(textParts, "\n"),
|
||||
strings.Join(thinkingParts, "\n\n"),
|
||||
hasThinking, hasToolUse, toolCalls, toolResults
|
||||
}
|
||||
|
||||
func commandCodeToolResultContent(block gjson.Result) (string, int) {
|
||||
output := block.Get("output")
|
||||
if !output.Exists() {
|
||||
output = block.Get("content")
|
||||
}
|
||||
if output.Exists() {
|
||||
if output.IsObject() {
|
||||
if value := output.Get("value"); value.Exists() &&
|
||||
value.Type == gjson.String {
|
||||
return strconv.Quote(value.Str), len(value.Str)
|
||||
}
|
||||
}
|
||||
return output.Raw, toolResultContentLength(output)
|
||||
}
|
||||
|
||||
for _, key := range []string{"text", "error", "value"} {
|
||||
if text := block.Get(key).Str; text != "" {
|
||||
return strconv.Quote(text), len(text)
|
||||
}
|
||||
}
|
||||
return "", 0
|
||||
}
|
||||
|
||||
func blockID(block gjson.Result, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := block.Get(key).Str; value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func commandCodeFirstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user