Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 127 additions & 144 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,188 +12,171 @@

<p align="center">
<a href="https://github.com/devcodes9/agsearch/actions/workflows/ci.yml"><img src="https://github.com/devcodes9/agsearch/actions/workflows/ci.yml/badge.svg" alt="ci"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license: MIT"></a>
<a href="https://github.com/devcodes9/agsearch/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license: MIT"></a>
</p>

agsearch indexes the local transcripts your coding agents already write. Search them in one
ranked list, preview the matching lines, and resume the original Claude Code or Codex session.
Everything stays on your machine.

<p align="center">
<img src="https://raw.githubusercontent.com/devcodes9/agsearch/main/docs/demo.gif"
alt="Typing a misspelled half-remembered phrase; the session you meant is row one"
width="100%">
</p>

## Why not just `/resume`?
## Quick start

Claude Code's `/resume` picker and `codex resume` both got good recently — `/resume` searches
across every project on the machine, filters by branch and worktree, and previews. If you
remember roughly what a session was *called*, use them. They're free and already installed.
Try a search without installing anything:

Read what they match on, though. Claude matches "the session name if you set one, otherwise the
AI-generated title, conversation summary, or first prompt." Codex matches name, preview, thread
id, branch, cwd. **Both search metadata about the session. Neither searches inside the
conversation.**
```sh
uvx agsearch -n "stripe tax id"
```

> Native search finds sessions by what they were **called**.
> agsearch finds them by what was **said** in them — ranked, across Claude and Codex.
For the interactive interface:

Three other differences worth knowing:
```sh
brew install devcodes9/tap/agsearch
agsearch
```

- **Cross-tool.** One ranked list over Claude *and* Codex. Rows tagged `cc` / `cx`.
- **It actually resumes.** Picking a session from an unrelated project in the native picker
copies a `cd`+resume command to your clipboard. agsearch resumes it.
- **It sees `-p` / SDK sessions.** Those never appear in the native picker at all. agsearch
indexes them, dimmed and tagged `auto`.
Type anything you remember from a past conversation. Select a result to resume it.

## Try it without installing anything
## Features

```sh
uvx agsearch -n "stripe tax id"
```
- **Full-conversation search.** Search user prompts and assistant replies, not only titles and
session metadata.
- **One list for both tools.** Claude Code and Codex sessions appear together, labelled `cc`
and `cx`.
- **Ranked results.** BM25 ranking favors focused sessions and shows matching lines in context.
- **Preview, read, or resume.** Inspect a match, open the transcript in a pager, or return to the
original session.
- **Fully local.** No uploads, API keys, hosted index, or network calls.
- **Fast warm searches.** A per-file cache reparses only transcripts that changed.

## Installation

That searches your real sessions and prints ranked matches. Nothing is installed, nothing is
left behind. agsearch is stdlib-only, so there is nothing to compile either.
### Homebrew

## Install
Recommended because it installs both agsearch and the `fzf` dependency:

```sh
brew install devcodes9/tap/agsearch
```

Brew is the recommended path because it resolves `fzf` and your PATH in the same step, and
`fzf` is what the interactive TUI needs. `uvx` cannot install it, because fzf is not a Python
package — that is the one thing the zero-install route above cannot give you.
### Python tool installers

```sh
uv tool install agsearch
# or
pipx install agsearch
```

Install [`fzf`](https://github.com/junegunn/fzf) separately for the interactive interface.
Without it, `agsearch -n "query"` still works.

Otherwise:
### Install script

```sh
curl -fsSL https://raw.githubusercontent.com/devcodes9/agsearch/main/install.sh | sh
```

Installs the latest release to `~/.local/bin`. Override with `PREFIX=` or pin with
`AGSEARCH_VERSION=v0.1.0`.
This installs the latest release to `~/.local/bin`. Set `PREFIX` to change the destination or
`AGSEARCH_VERSION` to pin a release.

agsearch requires Python 3.9 or newer and has no Python package dependencies.

## Usage

```sh
agsearch # browse all sessions in the interactive interface
agsearch "stripe tax id" # open with an initial query
agsearch -n "stripe tax id" # print ranked matches without fzf
agsearch --here "webhook" # search only the current project
agsearch -p myapp "migration" # search projects whose path contains "myapp"
agsearch --thinking "query" # include assistant thinking blocks
agsearch --no-resume "query" # print the selected resume command
agsearch --reindex # rebuild the transcript cache
agsearch --version # print the installed version
```

### Interactive keys

| Key | Action |
| --- | --- |
| <kbd>Enter</kbd> | Resume the selected session |
| <kbd>Ctrl-O</kbd> | Read the full conversation in your pager |
| <kbd>Ctrl-Y</kbd> | Copy the resume command |
| <kbd>Ctrl-/</kbd> | Toggle the preview pane |

Selecting a result starts `claude --resume` or `codex resume` from the session's project
directory. The current query is copied to the clipboard so you can find the same text after
resuming.

For a global shortcut, see the
[hotkey guide](https://github.com/devcodes9/agsearch/blob/main/docs/hotkey.md).

## Why not just `/resume`?

Claude Code's `/resume` picker and `codex resume` are good when you remember a session's title,
branch, directory, or first prompt. They search metadata about the session.

agsearch searches the conversation itself. It also combines both tools in one list and includes
Claude Code SDK and `-p` sessions that do not appear in the native picker.

Use the native picker when you remember what the session was called. Use agsearch when you
remember what was said.

Requirements: **Python 3**, no packages. **[`fzf`](https://github.com/junegunn/fzf)** for the
interactive TUI — `agsearch -n "query"` works without it.
## Search and ranking

agsearch drops common stopwords, applies conservative stemming, and ranks matching sessions with
BM25 across three weighted fields: title and project, first prompt, and full transcript. Sessions
covering more query terms rank first; relevance, recency, and previous resumes break close ties.
Rare long typos fall back to subsequence matching, so `conection pool` still finds the
session about connection pools.

Search is lexical, not semantic. It will not match concepts expressed with completely different
words, and the first result is not guaranteed to be the session you intended.

## Privacy and storage

agsearch reads:

- `~/.claude/projects/**/*.jsonl`
- `~/.codex/sessions/**/*.jsonl`

Its cache lives under `~/.cache/agsearch/`. Transcript parsing and ranking happen locally, and
only changed files are reparsed.

> [!IMPORTANT]
> **Claude Code deletes transcripts after 30 days by default.** agsearch can only find what is
> still on disk, and the default `cleanupPeriodDays: 30` means your history is quietly
> evaporating right now. If you want a searchable archive, raise it in `~/.claude/settings.json`:
> Claude Code deletes transcripts after 30 days by default. To keep a longer searchable history,
> set `cleanupPeriodDays` in `~/.claude/settings.json`:
>
> ```json
> { "cleanupPeriodDays": 365 }
> ```
>
> This is your setting to change — agsearch never writes to it.
> agsearch never changes this setting.

## Session handling

- Claude Code subagent transcripts are folded into their resumable parent session.
- SDK and other automated sessions remain searchable but rank below user-started sessions.
- Sessions from deleted worktrees resume from the nearest existing parent directory.
- Recently active sessions are marked `●` and require confirmation before reattaching.

## Use
## Development

```sh
agsearch # interactive TUI: one row per session, over all sessions
agsearch "stripe tax id" # TUI pre-filtered to a query
agsearch --fuzzy "..." # fuzzy matching instead of the default exact substring
agsearch -n "stripe tax id" # non-interactive: print ranked matches (no fzf)
agsearch --here "..." # only sessions from the current directory's project
agsearch --project myapp # only sessions whose path contains 'myapp'
agsearch --thinking # also search assistant thinking blocks
agsearch --reindex # force a full cache rebuild
agsearch --version # print the installed version
git clone https://github.com/devcodes9/agsearch.git
cd agsearch
python3 -m unittest discover -s tests
```

**Enter** resumes the session (cd's to its project dir, runs `claude --resume`), **Ctrl-/**
toggles the preview. **Ctrl-o** opens the whole conversation in your pager — no resume, no CLI
start, no tokens spent — which is usually all you needed to check you had the right session.
**Ctrl-y** copies the reattach command. Add `--no-resume` to print it instead.

Search is **exact substring, AND-of-terms** by default (`stripe webhook` = both words). Operators
work in either mode: `'word` force-exact, `^prefix`, `suffix$`, `!exclude`, `a | b` for OR. Pass
`--fuzzy` for typo-tolerant matching.

[Optional: bind it to a global hotkey](docs/hotkey.md) — Raycast, Hammerspoon or skhd. Not
required; if you're running `claude` you're already at a prompt.

## Runs fully local

Your sessions are indexed and searched on your machine — nothing is uploaded, and agsearch makes
no network calls at all. It reads the JSONL your agent CLIs already wrote, caches the index under
`~/.cache/agsearch/`, and the only programs it ever shells out to are `fzf`, a clipboard tool
(`pbcopy`, `wl-copy`, `xclip` or `xsel`, whichever you have), and the `claude`/`codex` CLI you
asked it to resume.

No Elasticsearch needed: it's ripgrep-speed over local JSONL with a per-file cache. Cold build
over ~1,200 sessions is a few seconds (it tells you how far along it is); every warm search after
that is ~0.1s.

## What the results look like

The TUI lists **one clean row per session**: `date · project · N/T · title`, where `N/T` is how many
of your query terms the session matched. Search runs over the full conversation text (agsearch does
the matching itself and feeds fzf only matching sessions, so the list stays clean while every word is
searchable). The right pane is a **compact preview card**, not a transcript dump: with a query it
shows only the matched lines (highlighted); with no query, the bookends (first prompt + last message)
so you know what the session was about. Full reading is what **resume** is for.

**Snippets are cleaned, not raw.** Transcripts are full of things nobody wants to read in a result
row, so every snippet strips markdown noise (fences, headings, bullets, emphasis, link syntax) and
condenses payloads — JSON dumps, `cat -n` file reads, pasted images, long opaque tokens — into short
`[json]` / `[file]` / `[image]` placeholders, on one fixed-width line. Search still runs over the
raw text, and if your term only exists inside a condensed payload the raw line is shown instead:
a snippet always contains the word you searched for, highlighted.

**Sessions are categorised, not filtered.** Claude Code records how each session started
(`entrypoint`): `cli` = you typed it, `sdk-py`/`sdk-ts` = spawned by a plugin or SDK process
(e.g. the security-guidance plugin's Stop hook, which opens a fresh session per git diff). Your
own sessions always rank **above** automation; automated runs still appear, dimmed and tagged
`auto`, so nothing is hidden. Row markers: `cc` Claude, `cx` Codex, `auto` plugin/SDK run.

**Subagents fold into their parent.** `agent-*` subagent transcripts (Explore/Task/guide agents)
can't be resumed on their own, so their content is searched as part of the **parent conversation
that spawned them** (linked via the subagent's `sessionId` field). A hit in subagent text lists
and resumes the parent; the preview shows subagent lines tagged `⤷`. So the list stays at ~325
real sessions while nothing is lost from search.

Resume is **id-based**, so a session whose project dir has since been deleted (a removed worktree)
still resumes — agsearch cd's to the nearest surviving ancestor of the dead path (or `$HOME`) and
prints a one-line notice. Those rows carry a dim `orig dir gone` tag in the list, and the preview
card says so too; it's a footnote, not a gate — the session opens exactly like any other. Sessions
written to in the last few minutes are probably still running: they're marked **●**, and Enter
warns (and asks, when there's a tty) before attaching.

Because Claude Code renders its own TUI, an outside tool can't highlight a line *inside* the
resumed session. So the highlighted-in-context view lives in the preview pane (before you spend
any tokens), and as a bonus, Enter copies your query to the clipboard: once the session reopens,
press **⌘F → ⌘V → Enter** to jump to the match in whatever iTerm holds in scrollback.

## How ranking works

Search is **BM25 ranked**, not just substring. It drops stopwords, applies a conservative stem so
`migration`≈`migrate`≈`migrating`, and scores each session over three weighted fields — title +
project, the session's **first prompt**, and the full transcript. The first prompt is what you
opened the session *asking for*, so it's weighted hardest; **length normalization** means a
sprawling transcript that mentions your term once no longer outranks a short, on-point session.
Near-ties are then nudged by **recency** and by how often you've actually resumed that session
(read off agsearch's own resume log). Badge `N/T` = query terms matched. A word that barely exists
anywhere (a typo like `conection`) falls back to subsequence matching. No modes or flags.

**Honest limit:** this is lexical, not semantic. Ranking beats an unordered `rg` dump, but it
is not a guarantee that row one is the session you meant. Because transcripts are large,
lower-ranked results can share a badge while being only loosely related, and it won't match by
*meaning* (e.g. `migration` won't find "porting the database"). That needs embeddings, a
deliberate future step, not more matching heuristics.

## How it works

- Walks `~/.claude/projects/**/*.jsonl` and `~/.codex/sessions/**/*.jsonl`, extracting user
prompts and assistant text (one row per message) plus each session's AI-generated title.
A source-adapter layer means new agents (Gemini CLI, Cursor, …) slot in without touching
search or ranking.
- Caches parsed output per file, keyed by mtime, under `~/.cache/agsearch/`. Only changed
sessions are re-parsed on later runs.
- The JSONL schema is Claude Code's internal format and can change between releases; if a
future version breaks parsing, run `agsearch --reindex` after updating.
- Ranking is covered by tests over a small fixture corpus (no dependencies, no fixtures on
disk): `python3 -m unittest discover -s tests`.

## Contributing

MIT licensed. Changes to ranking should come with a case in `tests/` — the gold-set scorer is
how we keep recall@1 from regressing. See [CHANGELOG.md](CHANGELOG.md) for what shipped when.
Changes to ranking should include a regression case in `tests/`. See the
[changelog](https://github.com/devcodes9/agsearch/blob/main/CHANGELOG.md) and
[open issues](https://github.com/devcodes9/agsearch/issues).

## License

[MIT](https://github.com/devcodes9/agsearch/blob/main/LICENSE)