# `ClaudeWrapper.History`
[🔗](https://github.com/genagent/claude_wrapper_ex/blob/main/lib/claude_wrapper/history.ex#L81)

Read-side access to Claude Code's on-disk session history.

Claude Code stores per-project session logs as line-delimited JSON
under `~/.claude/projects/<slug>/<session_id>.jsonl`, one JSON object
per line. This module gives a typed Elixir API over those logs
without prescribing a representation for the conversation -- callers
render however they want.

Three levels of granularity:

  * `list_projects/2` -- enumerate project directories with summary
    metadata (session count, latest activity).
  * `list_sessions/2` -- enumerate session files (all projects, or one
    via the `:slug` option) with summary metadata.
  * `read_session/2` -- parse a session into typed
    `t:entry/0` values.

## Liberal parsing

Each line is parsed independently; malformed lines are skipped rather
than failing the whole session. Unknown entry types come through as
`{:other, type_tag, raw}` so callers can inspect them. Only `user` and
`assistant` get typed variants; `queue-operation`, `attachment`,
`ai-title`, `last-prompt`, and future types land in `:other`.

## Slug encoding

Project directory names are filesystem-safe encodings of an absolute
path: every character that is not a letter or digit becomes `-` (so
`/Users/josh/Code/foo_bar` becomes `-Users-josh-Code-foo-bar`).
`project_slug/1` is the forward derivation (canonicalize + encode);
`ProjectSummary.decoded_path` is a best-effort inverse that anchors on
the real filesystem to disambiguate literal hyphens in directory names.

The encoding is lossy -- a `-` in a slug may have been a `/`, `.`, `_`,
space, or a literal hyphen -- so the inverse cannot always be exact.
When `decode_slug_anchored` cannot confirm a decoded path against the
filesystem it sets `ProjectSummary.decode_verified?` to `false`. The
forward direction has no such ambiguity and always matches the CLI.

## Example

    {:ok, root} = ClaudeWrapper.History.home()
    {:ok, projects} = ClaudeWrapper.History.list_projects(root)

    for p <- projects do
      {:ok, sessions} = ClaudeWrapper.History.list_sessions(root, slug: p.slug)
      IO.puts("#{p.slug}: #{length(sessions)} sessions")
    end

# `assistant_entry`

```elixir
@type assistant_entry() :: %{
  uuid: String.t() | nil,
  timestamp: String.t() | nil,
  message: term()
}
```

An `assistant` entry's fields.

# `entry`

```elixir
@type entry() ::
  {:user, user_entry()}
  | {:assistant, assistant_entry()}
  | {:other, String.t(), map()}
```

One parsed line from a session `.jsonl`.

`:user` and `:assistant` carry typed fields; every other entry type
is `{:other, type_tag, raw}` with the raw decoded JSON map.

# `list_opt`

```elixir
@type list_opt() ::
  {:limit, non_neg_integer() | nil}
  | {:offset, non_neg_integer()}
  | {:include_empty, boolean()}
  | {:sort, sort()}
  | {:slug, String.t() | nil}
```

Options for the listing functions.

  * `:limit` -- max items after sorting + offset (`nil` = no cap)
  * `:offset` -- skip the first N items (default `0`)
  * `:include_empty` -- keep projects/sessions with no activity
    (default `true`)
  * `:sort` -- `:name_asc` (default) or `:recency_desc`
  * `:slug` -- (sessions only) restrict to one project

# `sort`

```elixir
@type sort() :: :name_asc | :recency_desc
```

Sort order for the listing functions.

# `t`

```elixir
@type t() :: %ClaudeWrapper.History{root: String.t()}
```

# `user_entry`

```elixir
@type user_entry() :: %{
  uuid: String.t() | nil,
  timestamp: String.t() | nil,
  cwd: String.t() | nil,
  git_branch: String.t() | nil,
  message: term()
}
```

A `user` entry's fields.

# `assistant_texts`

```elixir
@spec assistant_texts(ClaudeWrapper.History.SessionLog.t()) :: [String.t()]
```

Every assistant text answer in a session, in order.

Each `:assistant` entry's `message["content"]` is reduced to its text
blocks (string content is taken verbatim; a content list keeps only
`"text"` blocks, joined). Entries with no text (tool-use-only turns,
say) contribute nothing.

    {:ok, log} = ClaudeWrapper.History.read_session(root, id)
    ClaudeWrapper.History.assistant_texts(log)
    #=> ["First answer.", "Second answer."]

# `at`

```elixir
@spec at(String.t()) :: t()
```

Use a specific path as the projects root. Useful for tests (point at a
temp dir) and non-default installs.

# `find_session`

```elixir
@spec find_session(t(), String.t()) ::
  {:ok, {String.t(), String.t()}} | {:error, ClaudeWrapper.Error.t()}
```

Locate the on-disk path and project slug for a session id, searching
every project directory.

Returns `{:ok, {path, slug}}` or `{:error, %ClaudeWrapper.Error{kind:
:not_found}}`.

# `home`

```elixir
@spec home() :: {:ok, t()} | {:error, ClaudeWrapper.Error.t()}
```

Resolve the default history root, `~/.claude/projects`.

Returns `{:error, %ClaudeWrapper.Error{kind: :no_home}}` when the user
home cannot be determined.

# `last_answer`

```elixir
@spec last_answer(ClaudeWrapper.History.SessionLog.t()) ::
  {:ok, String.t()} | {:error, :no_answer}
```

The last assistant text answer in a session.

Returns `{:ok, text}`, or `{:error, :no_answer}` when the session has
no assistant entry carrying text. Pairs with a `show`/`last` style
command reconstructing a stored session's answer from disk.

    {:ok, log} = ClaudeWrapper.History.read_session(root, id)
    ClaudeWrapper.History.last_answer(log)
    #=> {:ok, "The answer."}

# `list_projects`

```elixir
@spec list_projects(t(), [list_opt()]) ::
  {:ok, [ClaudeWrapper.History.ProjectSummary.t()]}
  | {:error, ClaudeWrapper.Error.t()}
```

List project directories with optional filter / sort / pagination.

Returns `{:ok, []}` when the root directory does not exist. See
`t:list_opt/0` for options.

# `list_sessions`

```elixir
@spec list_sessions(t(), [list_opt()]) ::
  {:ok, [ClaudeWrapper.History.SessionSummary.t()]}
  | {:error, ClaudeWrapper.Error.t()}
```

List sessions, optionally restricted to one project via the `:slug`
option, with filter / sort / pagination.

When no `:slug` is given, every project directory is unioned.

# `project_slug`

```elixir
@spec project_slug(String.t()) :: String.t()
```

Derive Claude Code's project-directory slug for a filesystem path,
matching the CLI: the path is canonicalized (best-effort symlink
resolution) and then every character that is not a letter or digit
(`/`, `.`, `_`, space, ...) is encoded as `-`.

This is the reliable way to locate the project directory for a working
directory -- see `sessions_for_path/3`. Falls back to the expanded path
when it cannot be canonicalized.

    iex> ClaudeWrapper.History.project_slug("/Users/josh/Code/foo_bar")
    "-Users-josh-Code-foo-bar"

# `read_session`

```elixir
@spec read_session(t(), String.t()) ::
  {:ok, ClaudeWrapper.History.SessionLog.t()}
  | {:error, ClaudeWrapper.Error.t()}
```

Read one session's full entry log, searching every project directory
for `<session_id>.jsonl`. Malformed lines are skipped.

Returns `{:error, %ClaudeWrapper.Error{kind: :not_found}}` when no
session file matches.

# `root`

```elixir
@spec root(t()) :: String.t()
```

The configured root directory.

# `sessions_for_path`

```elixir
@spec sessions_for_path(t(), String.t(), [list_opt()]) ::
  {:ok, [ClaudeWrapper.History.SessionSummary.t()]}
  | {:error, ClaudeWrapper.Error.t()}
```

List sessions for a working directory, deriving its project slug via
`project_slug/1`. Convenience over `list_sessions(h, slug: ...)`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
