Documentation
¶
Overview ¶
Package session loads, mutates, and writes coding-agent transcripts.
The package defines a tool-agnostic Turn / Session model plus a single Excise(set<turn_id>) operation that preserves four invariants:
- Removing a turn that owns tool_use blocks removes the paired tool_result turns.
- Removing a tool_result turn requires (or warns about) removing the originating tool_use turn.
- The ordering of surviving turns is preserved; their stable IDs are preserved as well.
- Writes are atomic: snapshot first, write to a tmp file, rename.
Two concrete loaders are provided: claude.go (Claude Code JSONL) and cursor.go (Cursor state.vscdb sqlite).
Index ¶
- func DefaultCursorPath() (string, error)
- func DiscoverNewestClaude() (string, error)
- func SortByTimestamp(turns []Turn)
- type ClaudeLoader
- type ClaudeWriter
- type CursorLoader
- type CursorWriter
- type DependencyGraph
- type Loader
- type Role
- type Session
- type Tool
- type ToolCall
- type ToolResult
- type Turn
- type Warning
- type Writer
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DefaultCursorPath ¶
DefaultCursorPath returns the platform-specific path to state.vscdb.
func DiscoverNewestClaude ¶
DiscoverNewestClaude returns the most recently-modified Claude Code session jsonl under ~/.claude/projects/, searching across every project directory.
It is the zero-arg invocation backbone of `excise`.
func SortByTimestamp ¶
func SortByTimestamp(turns []Turn)
SortByTimestamp ensures a stable display order. We rely on it after loading any format to make the TUI consistent.
Types ¶
type ClaudeLoader ¶
type ClaudeLoader struct{}
ClaudeLoader reads ~/.claude/projects/<dir>/<session>.jsonl files.
Schema (verified against real samples on the target machine, 2026-05-18):
{
"type": "user" | "assistant" | "system" | ...,
"uuid": "...",
"parentUuid": "..." | null,
"timestamp": "2026-05-17T12:32:37.576Z",
"sessionId": "...",
"message": {
"role": "user" | "assistant",
"content": "string" | [{"type": "text" | "thinking" | "tool_use" | "tool_result", ...}, ...]
}
}
We tolerate every Claude-specific extension by capturing the *raw* line bytes and re-emitting them on write, only re-encoding the small set of fields we actually parse for the TUI.
func (*ClaudeLoader) Detect ¶
func (l *ClaudeLoader) Detect(path string) bool
Detect returns true iff path ends in .jsonl AND the first non-empty line parses as a Claude-shaped object. We do not require a Claude path prefix so that the user can `excise testdata/foo.jsonl` outside ~/.claude.
type ClaudeWriter ¶
type ClaudeWriter struct{}
ClaudeWriter implements Writer for Claude JSONL sessions. It re-emits each surviving turn's Raw bytes verbatim, separated by single newlines, and performs an atomic snapshot-then-tmp-then-rename to preserve the invariants documented on the package.
func (*ClaudeWriter) Write ¶
func (w *ClaudeWriter) Write(s *Session) error
Write enforces invariant 4 (atomic) for Claude sessions. The caller is responsible for any snapshot strategy (see internal/safety).
type CursorLoader ¶
type CursorLoader struct{}
CursorLoader reads Cursor's chat data.
Cursor stores composers and individual chat bubbles inside
~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
(linux: ~/.config/Cursor/...; windows: %APPDATA%/Cursor/...).
Two table layouts exist:
The live `state.vscdb` (sqlite) — `cursorDiskKV` table — keys of shape `bubbleId:<composerId>:<bubbleId>` whose value is a JSON blob with fields like `text`, `type` (numeric), `bubbleId`, `toolResults`, `tokenCount`, etc. This is the format verified against the real install on this machine (Cursor build 2025-04).
A fixture-only JSON-lines export, used for unit tests and for users who do not have Cursor installed locally. Each line is one bubble JSON object plus an envelope:
{"composerId": "...", "bubble": { ...the same value as above... }}
Detection: file extension is .jsonl AND first non-empty line has a top-level "bubble" object.
We deliberately do NOT add a CGO sqlite driver because the dependency cost (and Linux/Windows cross-compile friction) outweighs the benefit for v0.1. Instead we shell out to the `sqlite3` CLI for sqlite reads. If sqlite3 is not on PATH, Load returns a clear error pointing the user at the fixture path.
IMPORTANT: this is read-only for sqlite in v0.1. Cursor's state.vscdb is frequently held open by the Cursor process; mutating it from the outside while Cursor is running risks corruption. The writer therefore refuses to write directly back to state.vscdb and instead emits a side-car .excised file the user can manually copy back (or, in v0.2, we will add a Cursor-must-be-closed safety prompt).
func (*CursorLoader) Detect ¶
func (l *CursorLoader) Detect(path string) bool
Detect returns true for either format.
type CursorWriter ¶
type CursorWriter struct{}
CursorWriter writes Cursor sessions. For sqlite sources it emits a side-car `.excised.jsonl` next to the database; for jsonl sources it does an atomic snapshot-then-tmp-then-rename, matching ClaudeWriter.
func (*CursorWriter) Write ¶
func (w *CursorWriter) Write(s *Session) error
type DependencyGraph ¶
type DependencyGraph struct {
// contains filtered or unexported fields
}
DependencyGraph models the tool_use → tool_result edge.
invariant 1 of the Excise primitive — "removing a turn with tool_calls removes its paired tool_result turns" — is enforced here.
func BuildGraph ¶
func BuildGraph(turns []Turn) *DependencyGraph
BuildGraph indexes a session's turns into a DependencyGraph.
func (*DependencyGraph) Closure ¶
Closure computes the full set of turn ids that must be removed when `seeds` are excised, honoring invariants 1 and 2.
- invariant 1: removing a turn with tool_use blocks pulls in the matching tool_result turns.
- invariant 2 (warn-mode, see Verify): removing a turn that contains only a tool_result without its owner is allowed but flagged.
The walk is iterative so it terminates even on pathological self-cycles (which a well-formed transcript should never contain).
type Role ¶
type Role string
Role is the speaker of a single Turn. We collapse Claude's `system` / `tool_result` types and Cursor's numeric types down to a small enum so the TUI does not need to know about format-specific edge cases.
type Session ¶
type Session struct {
Tool Tool
SourcePath string // file path (Claude) or sqlite path (Cursor)
ComposerID string // Cursor only: bubble group id
SessionID string // Claude only: session uuid; Cursor: composer uuid
Turns []Turn
}
Session is an ordered list of Turn plus the metadata needed to write it back to disk in the same format it was loaded from.
type ToolResult ¶
type ToolResult struct {
ToolUseID string `json:"tool_use_id"`
}
ToolResult represents a single tool_result block owned by a Turn.
type Turn ¶
type Turn struct {
ID string // stable id (Claude uuid; Cursor bubbleId)
ParentID string // parentUuid / parentBubbleId, "" if root
Role Role
Timestamp time.Time
Preview string // first ~120 chars of textual content
TokenEst int // very rough token estimate (chars / 4)
ToolCalls []ToolCall // tool_use blocks owned by this turn
ToolResults []ToolResult // tool_result blocks owned by this turn
Raw []byte // verbatim original payload
Meta any // format-specific metadata (loader's own struct)
}
Turn is one logical entry in a transcript. ID is stable across edits.
Raw holds the original on-disk JSON line (Claude) or sqlite value (Cursor) so writer.go can re-emit untouched bytes for surviving turns and avoid lossy round-trips through our model.
func Excise ¶
Excise returns a new []Turn with `toCut` (and any dependents) removed. The original slice is not mutated.
This is the core mutation referenced by the Excise primitive in the MVP plan (section 2). The returned slice preserves ordering (invariant 3) and surviving turns keep their stable IDs.
type Warning ¶
Verify returns warnings about turns in `toCut` that would leave dangling tool refs. invariant 2 says removing a tool_result requires its owner; we surface this as a warning rather than refusing so the user with --force can override.