diff --git a/README.md b/README.md
index 27a49cc..c84e13c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
> **Warning**: This project is under active development. APIs and output formats may change between versions. Use with caution in production scripts.
-A lightweight headless CLI wrapper for Claude Code that renders structured text output. Perfect for scripts, automation, logging, and headless environments.
+Renders Claude Code's streaming JSON as readable terminal output. A drop-in replacement for headless environments where the interactive TUI isn't available.
diff --git a/history.go b/history.go
new file mode 100644
index 0000000..50561ed
--- /dev/null
+++ b/history.go
@@ -0,0 +1,590 @@
+package main
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+)
+
+// HistoryOptions holds the command line options for history mode
+type HistoryOptions struct {
+ Path string
+ Since string
+ Last int
+ Project string
+}
+
+// HistoryMessage represents a parsed message from a session JSONL
+type HistoryMessage struct {
+ Type string // "user", "assistant", "tool_result"
+ Timestamp time.Time
+ UUID string
+ ParentUUID string
+
+ // For user messages
+ UserText string
+
+ // For assistant messages
+ AssistantText string
+ ToolCalls map[string]int // tool name -> count
+ InputTokens int
+ OutputTokens int
+
+ // For identifying tool results
+ IsToolResult bool
+}
+
+// ConversationTurn represents a single turn in the conversation
+type ConversationTurn struct {
+ Timestamp time.Time
+ UserPrompt string
+ AssistantText string
+ ToolCounts map[string]int
+ InputTokens int
+ OutputTokens int
+}
+
+// HistoryReader reads and processes session JSONL files
+type HistoryReader struct {
+ colors *ColorScheme
+}
+
+// NewHistoryReader creates a new history reader
+func NewHistoryReader() *HistoryReader {
+ return &HistoryReader{
+ colors: GetScheme(),
+ }
+}
+
+// Run executes the history mode with the given options
+func (h *HistoryReader) Run(opts HistoryOptions) error {
+ // Determine what we're reading
+ var sessions []SessionIndexEntry
+ var err error
+
+ if opts.Project != "" {
+ // Find project by name in ~/.claude/projects/
+ sessions, err = h.findProjectSessions(opts.Project)
+ if err != nil {
+ return err
+ }
+ } else if opts.Path != "" {
+ if strings.HasSuffix(opts.Path, ".jsonl") {
+ // Direct session file
+ sessions = []SessionIndexEntry{{FullPath: opts.Path}}
+ } else {
+ // Directory - read sessions-index.json
+ sessions, err = h.readSessionIndex(opts.Path)
+ if err != nil {
+ return err
+ }
+ }
+ } else {
+ return fmt.Errorf("no path or project specified")
+ }
+
+ if len(sessions) == 0 {
+ return fmt.Errorf("no sessions found")
+ }
+
+ // Apply filters
+ sessions = h.filterSessions(sessions, opts)
+
+ if len(sessions) == 0 {
+ return fmt.Errorf("no sessions match the specified filters")
+ }
+
+ // Sort by modified time (most recent first)
+ sort.Slice(sessions, func(i, j int) bool {
+ ti, _ := time.Parse(time.RFC3339, sessions[i].Modified)
+ tj, _ := time.Parse(time.RFC3339, sessions[j].Modified)
+ return ti.After(tj)
+ })
+
+ // Process and display each session
+ for i, session := range sessions {
+ if i > 0 {
+ fmt.Println()
+ fmt.Println(strings.Repeat("=", 60))
+ fmt.Println()
+ }
+ if err := h.processSession(session); err != nil {
+ fmt.Fprintf(os.Stderr, "Error processing session %s: %v\n", session.SessionID, err)
+ }
+ }
+
+ return nil
+}
+
+// findProjectSessions searches ~/.claude/projects/ for a project by name
+func (h *HistoryReader) findProjectSessions(projectName string) ([]SessionIndexEntry, error) {
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get home directory: %w", err)
+ }
+
+ projectsDir := filepath.Join(homeDir, ".claude", "projects")
+ entries, err := os.ReadDir(projectsDir)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read projects directory: %w", err)
+ }
+
+ // Search for matching project directories
+ var matches []string
+ lowerName := strings.ToLower(projectName)
+ for _, entry := range entries {
+ if entry.IsDir() {
+ // Convert directory name to lowercase and check if it contains the project name
+ dirLower := strings.ToLower(entry.Name())
+ if strings.Contains(dirLower, lowerName) {
+ matches = append(matches, filepath.Join(projectsDir, entry.Name()))
+ }
+ }
+ }
+
+ if len(matches) == 0 {
+ return nil, fmt.Errorf("no project found matching '%s'", projectName)
+ }
+
+ // Use the first match (or could prompt user if multiple)
+ if len(matches) > 1 {
+ fmt.Fprintf(os.Stderr, "Found %d matching projects, using: %s\n", len(matches), matches[0])
+ }
+
+ return h.readSessionIndex(matches[0])
+}
+
+// readSessionIndex reads the sessions-index.json file from a directory
+func (h *HistoryReader) readSessionIndex(dirPath string) ([]SessionIndexEntry, error) {
+ indexPath := filepath.Join(dirPath, "sessions-index.json")
+
+ data, err := os.ReadFile(indexPath)
+ if err != nil {
+ // If no index file, try to list .jsonl files directly
+ if os.IsNotExist(err) {
+ return h.listJSONLFiles(dirPath)
+ }
+ return nil, fmt.Errorf("failed to read sessions-index.json: %w", err)
+ }
+
+ var index SessionIndex
+ if err := json.Unmarshal(data, &index); err != nil {
+ return nil, fmt.Errorf("failed to parse sessions-index.json: %w", err)
+ }
+
+ return index.Entries, nil
+}
+
+// listJSONLFiles lists .jsonl files in a directory as fallback
+func (h *HistoryReader) listJSONLFiles(dirPath string) ([]SessionIndexEntry, error) {
+ pattern := filepath.Join(dirPath, "*.jsonl")
+ matches, err := filepath.Glob(pattern)
+ if err != nil {
+ return nil, err
+ }
+
+ var entries []SessionIndexEntry
+ for _, path := range matches {
+ info, err := os.Stat(path)
+ if err != nil {
+ continue
+ }
+
+ entries = append(entries, SessionIndexEntry{
+ SessionID: strings.TrimSuffix(filepath.Base(path), ".jsonl"),
+ FullPath: path,
+ Modified: info.ModTime().Format(time.RFC3339),
+ })
+ }
+
+ return entries, nil
+}
+
+// filterSessions applies --since and --last filters
+func (h *HistoryReader) filterSessions(sessions []SessionIndexEntry, opts HistoryOptions) []SessionIndexEntry {
+ var filtered []SessionIndexEntry
+
+ // Parse --since date if provided
+ var sinceTime time.Time
+ if opts.Since != "" {
+ // Try multiple formats
+ formats := []string{
+ "2006-01-02",
+ "2006-01-02T15:04:05",
+ time.RFC3339,
+ }
+ for _, format := range formats {
+ if t, err := time.Parse(format, opts.Since); err == nil {
+ sinceTime = t
+ break
+ }
+ }
+ }
+
+ for _, session := range sessions {
+ // Apply --since filter
+ if !sinceTime.IsZero() {
+ modified, err := time.Parse(time.RFC3339, session.Modified)
+ if err != nil {
+ continue
+ }
+ if modified.Before(sinceTime) {
+ continue
+ }
+ }
+
+ filtered = append(filtered, session)
+ }
+
+ // Apply --last filter
+ if opts.Last > 0 && len(filtered) > opts.Last {
+ // Sort by modified time first
+ sort.Slice(filtered, func(i, j int) bool {
+ ti, _ := time.Parse(time.RFC3339, filtered[i].Modified)
+ tj, _ := time.Parse(time.RFC3339, filtered[j].Modified)
+ return ti.After(tj)
+ })
+ filtered = filtered[:opts.Last]
+ }
+
+ return filtered
+}
+
+// processSession reads and formats a single session file
+func (h *HistoryReader) processSession(entry SessionIndexEntry) error {
+ file, err := os.Open(entry.FullPath)
+ if err != nil {
+ return fmt.Errorf("failed to open session file: %w", err)
+ }
+ defer file.Close()
+
+ // Parse all messages
+ messages, err := h.parseSessionFile(file)
+ if err != nil {
+ return fmt.Errorf("failed to parse session: %w", err)
+ }
+
+ // Build conversation turns
+ turns := h.buildConversationTurns(messages)
+
+ // Calculate session time range
+ var startTime, endTime time.Time
+ if len(turns) > 0 {
+ startTime = turns[0].Timestamp
+ endTime = turns[len(turns)-1].Timestamp
+ }
+
+ // Print session header
+ h.printSessionHeader(entry, startTime, endTime)
+
+ // Print each turn
+ var totalIn, totalOut int
+ for _, turn := range turns {
+ h.printTurn(turn)
+ totalIn += turn.InputTokens
+ totalOut += turn.OutputTokens
+ }
+
+ // Print session footer with totals
+ h.printSessionFooter(totalIn, totalOut)
+
+ return nil
+}
+
+// parseSessionFile parses a JSONL session file
+func (h *HistoryReader) parseSessionFile(reader io.Reader) ([]HistoryMessage, error) {
+ scanner := bufio.NewScanner(reader)
+ // Increase buffer size for large messages
+ buf := make([]byte, 0, 64*1024)
+ scanner.Buffer(buf, 1024*1024)
+
+ var messages []HistoryMessage
+
+ for scanner.Scan() {
+ line := scanner.Bytes()
+ if len(line) == 0 {
+ continue
+ }
+
+ msg, err := h.parseHistoryLine(line)
+ if err != nil {
+ // Skip lines we can't parse
+ continue
+ }
+
+ if msg != nil {
+ messages = append(messages, *msg)
+ }
+ }
+
+ return messages, scanner.Err()
+}
+
+// parseHistoryLine parses a single line from a session JSONL file
+func (h *HistoryReader) parseHistoryLine(line []byte) (*HistoryMessage, error) {
+ // Parse outer envelope
+ var envelope struct {
+ Type string `json:"type"`
+ Timestamp string `json:"timestamp"`
+ UUID string `json:"uuid"`
+ ParentUUID *string `json:"parentUuid"`
+ Message json.RawMessage `json:"message"`
+ }
+
+ if err := json.Unmarshal(line, &envelope); err != nil {
+ return nil, err
+ }
+
+ // Skip non-message types
+ if envelope.Type != "user" && envelope.Type != "assistant" {
+ return nil, nil
+ }
+
+ timestamp, _ := time.Parse(time.RFC3339, envelope.Timestamp)
+
+ parentUUID := ""
+ if envelope.ParentUUID != nil {
+ parentUUID = *envelope.ParentUUID
+ }
+
+ msg := &HistoryMessage{
+ Type: envelope.Type,
+ Timestamp: timestamp,
+ UUID: envelope.UUID,
+ ParentUUID: parentUUID,
+ }
+
+ if envelope.Type == "user" {
+ return h.parseUserMessage(msg, envelope.Message)
+ }
+
+ if envelope.Type == "assistant" {
+ return h.parseAssistantMessage(msg, envelope.Message)
+ }
+
+ return nil, nil
+}
+
+// parseUserMessage parses a user message
+func (h *HistoryReader) parseUserMessage(msg *HistoryMessage, rawMessage json.RawMessage) (*HistoryMessage, error) {
+ // User message content can be a string or an array (for tool results)
+ var userMsg struct {
+ Role string `json:"role"`
+ Content json.RawMessage `json:"content"`
+ }
+
+ if err := json.Unmarshal(rawMessage, &userMsg); err != nil {
+ return nil, err
+ }
+
+ // Try to parse as string first (actual user prompt)
+ var contentStr string
+ if err := json.Unmarshal(userMsg.Content, &contentStr); err == nil {
+ msg.UserText = contentStr
+ return msg, nil
+ }
+
+ // Try to parse as array (tool_result)
+ var contentArr []struct {
+ Type string `json:"type"`
+ ToolUseID string `json:"tool_use_id,omitempty"`
+ }
+ if err := json.Unmarshal(userMsg.Content, &contentArr); err == nil {
+ if len(contentArr) > 0 && contentArr[0].Type == "tool_result" {
+ msg.IsToolResult = true
+ return msg, nil
+ }
+ }
+
+ return msg, nil
+}
+
+// parseAssistantMessage parses an assistant message
+func (h *HistoryReader) parseAssistantMessage(msg *HistoryMessage, rawMessage json.RawMessage) (*HistoryMessage, error) {
+ var assistantMsg struct {
+ Content []struct {
+ Type string `json:"type"`
+ Text string `json:"text,omitempty"`
+ Thinking string `json:"thinking,omitempty"`
+ Name string `json:"name,omitempty"`
+ Input json.RawMessage `json:"input,omitempty"`
+ } `json:"content"`
+ Usage struct {
+ InputTokens int `json:"input_tokens"`
+ OutputTokens int `json:"output_tokens"`
+ } `json:"usage"`
+ }
+
+ if err := json.Unmarshal(rawMessage, &assistantMsg); err != nil {
+ return nil, err
+ }
+
+ msg.ToolCalls = make(map[string]int)
+ var textParts []string
+
+ for _, block := range assistantMsg.Content {
+ switch block.Type {
+ case "text":
+ if block.Text != "" {
+ textParts = append(textParts, block.Text)
+ }
+ case "tool_use":
+ msg.ToolCalls[block.Name]++
+ // Skip thinking blocks as per plan
+ }
+ }
+
+ msg.AssistantText = strings.Join(textParts, "\n")
+ msg.InputTokens = assistantMsg.Usage.InputTokens
+ msg.OutputTokens = assistantMsg.Usage.OutputTokens
+
+ return msg, nil
+}
+
+// buildConversationTurns groups messages into conversation turns
+func (h *HistoryReader) buildConversationTurns(messages []HistoryMessage) []ConversationTurn {
+ var turns []ConversationTurn
+
+ for i := 0; i < len(messages); i++ {
+ msg := messages[i]
+
+ // Skip tool results - they're not user prompts
+ if msg.IsToolResult {
+ continue
+ }
+
+ // Look for user prompt
+ if msg.Type == "user" && msg.UserText != "" {
+ turn := ConversationTurn{
+ Timestamp: msg.Timestamp,
+ UserPrompt: msg.UserText,
+ ToolCounts: make(map[string]int),
+ }
+
+ // Find all following assistant messages until next user prompt
+ for j := i + 1; j < len(messages); j++ {
+ nextMsg := messages[j]
+
+ // Stop at next user prompt (that's not a tool result)
+ if nextMsg.Type == "user" && !nextMsg.IsToolResult && nextMsg.UserText != "" {
+ break
+ }
+
+ // Accumulate assistant responses
+ if nextMsg.Type == "assistant" {
+ if turn.AssistantText != "" && nextMsg.AssistantText != "" {
+ turn.AssistantText += "\n\n"
+ }
+ turn.AssistantText += nextMsg.AssistantText
+
+ // Accumulate tool counts
+ for tool, count := range nextMsg.ToolCalls {
+ turn.ToolCounts[tool] += count
+ }
+
+ // Accumulate tokens
+ turn.InputTokens += nextMsg.InputTokens
+ turn.OutputTokens += nextMsg.OutputTokens
+ }
+ }
+
+ turns = append(turns, turn)
+ }
+ }
+
+ return turns
+}
+
+// printSessionHeader prints the session header
+func (h *HistoryReader) printSessionHeader(entry SessionIndexEntry, start, end time.Time) {
+ c := h.colors
+
+ // Format session time range
+ var timeRange string
+ if !start.IsZero() && !end.IsZero() {
+ duration := end.Sub(start)
+ startStr := start.Format("2006-01-02 15:04:05")
+ endStr := end.Format("15:04:05")
+
+ if duration >= time.Hour {
+ hours := int(duration.Hours())
+ mins := int(duration.Minutes()) % 60
+ timeRange = fmt.Sprintf("%s - %s (%dh %dm)", startStr, endStr, hours, mins)
+ } else if duration >= time.Minute {
+ mins := int(duration.Minutes())
+ timeRange = fmt.Sprintf("%s - %s (%d min)", startStr, endStr, mins)
+ } else {
+ timeRange = fmt.Sprintf("%s - %s (<1 min)", startStr, endStr)
+ }
+ } else {
+ timeRange = "unknown"
+ }
+
+ fmt.Printf("%sSession:%s %s\n", c.LabelDim, c.Reset, timeRange)
+ if entry.ProjectPath != "" {
+ fmt.Printf("%sProject:%s %s\n", c.LabelDim, c.Reset, entry.ProjectPath)
+ }
+ fmt.Println()
+ fmt.Println("---")
+ fmt.Println()
+}
+
+// printTurn prints a single conversation turn
+func (h *HistoryReader) printTurn(turn ConversationTurn) {
+ c := h.colors
+
+ // Timestamp
+ timestamp := turn.Timestamp.Format("15:04:05")
+ fmt.Printf("%s[%s]%s\n", c.LabelDim, timestamp, c.Reset)
+
+ // User prompt
+ fmt.Printf("%sUser:%s %s\n", c.ValueBright, c.Reset, turn.UserPrompt)
+ fmt.Println()
+
+ // Assistant response
+ if turn.AssistantText != "" {
+ fmt.Printf("%sAssistant:%s %s\n", c.ValueBright, c.Reset, turn.AssistantText)
+ fmt.Println()
+ }
+
+ // Tool summary
+ if len(turn.ToolCounts) > 0 {
+ var toolParts []string
+ for tool, count := range turn.ToolCounts {
+ toolParts = append(toolParts, fmt.Sprintf("%s(%d)", tool, count))
+ }
+ // Sort for consistent output
+ sort.Strings(toolParts)
+ fmt.Printf("%sTools:%s %s\n", c.LabelDim, c.Reset, strings.Join(toolParts, ", "))
+ }
+
+ // Token usage
+ if turn.InputTokens > 0 || turn.OutputTokens > 0 {
+ fmt.Printf("%sTokens:%s %s%d%s in / %s%d%s out\n",
+ c.LabelDim, c.Reset,
+ c.ValueBright, turn.InputTokens, c.Reset,
+ c.ValueBright, turn.OutputTokens, c.Reset)
+ }
+
+ fmt.Println()
+ fmt.Println("---")
+ fmt.Println()
+}
+
+// printSessionFooter prints the session footer with totals
+func (h *HistoryReader) printSessionFooter(totalIn, totalOut int) {
+ c := h.colors
+
+ if totalIn > 0 || totalOut > 0 {
+ fmt.Printf("%sTotal:%s %s%d%s in / %s%d%s out\n",
+ c.LabelDim, c.Reset,
+ c.ValueBright, totalIn, c.Reset,
+ c.ValueBright, totalOut, c.Reset)
+ }
+}
diff --git a/main.go b/main.go
index 8181c4b..b229264 100644
--- a/main.go
+++ b/main.go
@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/signal"
+ "strconv"
"strings"
"syscall"
)
@@ -18,12 +19,18 @@ func printUsage() {
fmt.Fprintf(os.Stderr, "A headless CLI wrapper for Claude Code that outputs structured text.\n\n")
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, " ccv [options] [prompt]\n")
- fmt.Fprintf(os.Stderr, " ccv [options] [claude args...]\n\n")
+ fmt.Fprintf(os.Stderr, " ccv [options] [claude args...]\n")
+ fmt.Fprintf(os.Stderr, " ccv --history View past session transcripts\n\n")
fmt.Fprintf(os.Stderr, "Output Flags:\n")
fmt.Fprintf(os.Stderr, " --verbose Show verbose output including full tool inputs\n")
fmt.Fprintf(os.Stderr, " --quiet Show only assistant text responses\n")
fmt.Fprintf(os.Stderr, " --format Output format: text (default), json\n")
fmt.Fprintf(os.Stderr, " --no-color Disable colored output\n")
+ fmt.Fprintf(os.Stderr, "\nHistory Mode Flags:\n")
+ fmt.Fprintf(os.Stderr, " --history Read past sessions from path (file or directory)\n")
+ fmt.Fprintf(os.Stderr, " --since Filter sessions modified since date (YYYY-MM-DD)\n")
+ fmt.Fprintf(os.Stderr, " --last Show only the last N sessions\n")
+ fmt.Fprintf(os.Stderr, " --project Find project by name in ~/.claude/projects/\n")
fmt.Fprintf(os.Stderr, "\nGeneral Flags:\n")
fmt.Fprintf(os.Stderr, " --help Show help information\n")
fmt.Fprintf(os.Stderr, " --version Show version information\n")
@@ -37,6 +44,11 @@ func printUsage() {
fmt.Fprintf(os.Stderr, " ccv --verbose \"Debug this issue\"\n")
fmt.Fprintf(os.Stderr, " ccv --format json \"List all files\"\n")
fmt.Fprintf(os.Stderr, " ccv -p \"Fix the bug\" --allowedTools Bash,Read\n")
+ fmt.Fprintf(os.Stderr, "\nHistory Examples:\n")
+ fmt.Fprintf(os.Stderr, " ccv --history ~/.claude/projects/-Users-me-myproject/\n")
+ fmt.Fprintf(os.Stderr, " ccv --history --last 5 ~/.claude/projects/-Users-me-myproject/\n")
+ fmt.Fprintf(os.Stderr, " ccv --history --project myproject\n")
+ fmt.Fprintf(os.Stderr, " ccv --history --since 2025-01-20 --project myproject\n")
}
func main() {
@@ -56,6 +68,13 @@ func main() {
format = "text"
}
+ // History mode flags
+ var historyPath string
+ var historySince string
+ var historyLast int
+ var historyProject string
+ historyMode := false
+
// Parse and filter ccv-specific flags, pass remaining args to claude
var claudeArgs []string
for i := 0; i < len(args); i++ {
@@ -99,6 +118,61 @@ func main() {
continue
}
+ // History mode flags
+ if arg == "--history" || arg == "-history" {
+ historyMode = true
+ // Check if next arg is a path (doesn't start with -)
+ if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
+ i++
+ historyPath = args[i]
+ }
+ continue
+ }
+ if strings.HasPrefix(arg, "--history=") {
+ historyMode = true
+ historyPath = strings.TrimPrefix(arg, "--history=")
+ continue
+ }
+ if arg == "--since" || arg == "-since" {
+ if i+1 < len(args) {
+ i++
+ historySince = args[i]
+ }
+ continue
+ }
+ if strings.HasPrefix(arg, "--since=") {
+ historySince = strings.TrimPrefix(arg, "--since=")
+ continue
+ }
+ if arg == "--last" || arg == "-last" {
+ if i+1 < len(args) {
+ i++
+ if n, err := strconv.Atoi(args[i]); err == nil {
+ historyLast = n
+ }
+ }
+ continue
+ }
+ if strings.HasPrefix(arg, "--last=") {
+ if n, err := strconv.Atoi(strings.TrimPrefix(arg, "--last=")); err == nil {
+ historyLast = n
+ }
+ continue
+ }
+ if arg == "--project" || arg == "-project" {
+ historyMode = true
+ if i+1 < len(args) {
+ i++
+ historyProject = args[i]
+ }
+ continue
+ }
+ if strings.HasPrefix(arg, "--project=") {
+ historyMode = true
+ historyProject = strings.TrimPrefix(arg, "--project=")
+ continue
+ }
+
// Pass through to claude
claudeArgs = append(claudeArgs, arg)
}
@@ -108,6 +182,34 @@ func main() {
SetNoColor(true)
}
+ // Handle history mode
+ if historyMode {
+ // If we still don't have a path but have remaining args, use the first one
+ if historyPath == "" && historyProject == "" && len(claudeArgs) > 0 {
+ historyPath = claudeArgs[0]
+ }
+
+ if historyPath == "" && historyProject == "" {
+ fmt.Fprintln(os.Stderr, "Error: --history requires a path or --project name")
+ printUsage()
+ os.Exit(1)
+ }
+
+ reader := NewHistoryReader()
+ opts := HistoryOptions{
+ Path: historyPath,
+ Since: historySince,
+ Last: historyLast,
+ Project: historyProject,
+ }
+
+ if err := reader.Run(opts); err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+ }
+
args = claudeArgs
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "Error: No prompt or arguments provided")
diff --git a/types.go b/types.go
index f49177b..58e5b2a 100644
--- a/types.go
+++ b/types.go
@@ -325,6 +325,26 @@ type UserMessage struct {
ToolUseResult *ToolUseResult `json:"tool_use_result,omitempty"`
}
+// SessionIndex represents the sessions-index.json structure
+type SessionIndex struct {
+ Version int `json:"version"`
+ Entries []SessionIndexEntry `json:"entries"`
+}
+
+// SessionIndexEntry represents an entry in the sessions-index.json
+type SessionIndexEntry struct {
+ SessionID string `json:"sessionId"`
+ FullPath string `json:"fullPath"`
+ FileMtime int64 `json:"fileMtime"`
+ FirstPrompt string `json:"firstPrompt"`
+ MessageCount int `json:"messageCount"`
+ Created string `json:"created"`
+ Modified string `json:"modified"`
+ GitBranch string `json:"gitBranch"`
+ ProjectPath string `json:"projectPath"`
+ IsSidechain bool `json:"isSidechain"`
+}
+
// ToolCall represents a tool invocation with its state
type ToolCall struct {
ID string `json:"id"`