transcript.go

  1package hooks
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"fmt"
  7	"os"
  8	"path/filepath"
  9	"time"
 10
 11	"github.com/charmbracelet/crush/internal/message"
 12)
 13
 14// TranscriptMessage represents a message in the exported transcript.
 15type TranscriptMessage struct {
 16	ID          string                 `json:"id"`
 17	Role        string                 `json:"role"`
 18	Content     string                 `json:"content,omitempty"`
 19	ToolCalls   []TranscriptToolCall   `json:"tool_calls,omitempty"`
 20	ToolResults []TranscriptToolResult `json:"tool_results,omitempty"`
 21	Timestamp   string                 `json:"timestamp"`
 22}
 23
 24// TranscriptToolCall represents a tool call in the transcript.
 25type TranscriptToolCall struct {
 26	ID    string `json:"id"`
 27	Name  string `json:"name"`
 28	Input string `json:"input"`
 29}
 30
 31// TranscriptToolResult represents a tool result in the transcript.
 32type TranscriptToolResult struct {
 33	ToolCallID string `json:"tool_call_id"`
 34	Name       string `json:"name"`
 35	Content    string `json:"content"`
 36	IsError    bool   `json:"is_error"`
 37}
 38
 39// Transcript represents the complete transcript structure.
 40type Transcript struct {
 41	SessionID string              `json:"session_id"`
 42	Messages  []TranscriptMessage `json:"messages"`
 43}
 44
 45// exportTranscript exports session messages to a temporary JSON file.
 46func exportTranscript(
 47	ctx context.Context,
 48	messages message.Service,
 49	sessionID string,
 50) (string, error) {
 51	// Get all messages for the session
 52	msgs, err := messages.List(ctx, sessionID)
 53	if err != nil {
 54		return "", fmt.Errorf("failed to list messages: %w", err)
 55	}
 56
 57	// Convert to transcript format
 58	transcript := Transcript{
 59		SessionID: sessionID,
 60		Messages:  make([]TranscriptMessage, 0, len(msgs)),
 61	}
 62
 63	for _, msg := range msgs {
 64		tm := TranscriptMessage{
 65			ID:        msg.ID,
 66			Role:      string(msg.Role),
 67			Timestamp: time.Unix(msg.CreatedAt, 0).Format("2006-01-02T15:04:05Z07:00"),
 68		}
 69
 70		// Extract content
 71		for _, part := range msg.Parts {
 72			if text, ok := part.(message.TextContent); ok {
 73				if tm.Content != "" {
 74					tm.Content += "\n"
 75				}
 76				tm.Content += text.Text
 77			}
 78		}
 79
 80		// Extract tool calls
 81		if msg.Role == message.Assistant {
 82			toolCalls := msg.ToolCalls()
 83			for _, tc := range toolCalls {
 84				tm.ToolCalls = append(tm.ToolCalls, TranscriptToolCall{
 85					ID:    tc.ID,
 86					Name:  tc.Name,
 87					Input: tc.Input,
 88				})
 89			}
 90		}
 91
 92		// Extract tool results
 93		if msg.Role == message.Tool {
 94			toolResults := msg.ToolResults()
 95			for _, tr := range toolResults {
 96				tm.ToolResults = append(tm.ToolResults, TranscriptToolResult{
 97					ToolCallID: tr.ToolCallID,
 98					Name:       tr.Name,
 99					Content:    tr.Content,
100					IsError:    tr.IsError,
101				})
102			}
103		}
104
105		transcript.Messages = append(transcript.Messages, tm)
106	}
107
108	// Marshal to JSON
109	data, err := json.MarshalIndent(transcript, "", "  ")
110	if err != nil {
111		return "", fmt.Errorf("failed to marshal transcript: %w", err)
112	}
113
114	// Write to temporary file
115	tmpDir := os.TempDir()
116	filename := fmt.Sprintf("crush-transcript-%s.json", sessionID)
117	path := filepath.Join(tmpDir, filename)
118
119	// Use restrictive permissions (0600)
120	if err := os.WriteFile(path, data, 0o600); err != nil {
121		return "", fmt.Errorf("failed to write transcript file: %w", err)
122	}
123
124	return path, nil
125}
126
127func cleanupTranscript(path string) {
128	if path != "" {
129		if err := os.Remove(path); err != nil {
130			fmt.Fprintf(os.Stderr, "Warning: failed to cleanup transcript file %s: %v\n", path, err)
131		}
132	}
133}