npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@threadbase-sh/scanner

v0.21.1

Published

Unified Claude Code conversation history scanner

Readme

@threadbase/scanner

Unified Claude Code conversation history scanner.

License: MIT Node.js

Combines the best parts of four independent scanner implementations (VS Code, Electron, IntelliJ, CLI) into a single TypeScript package.

Features

  • Persistent SQLite index (default) — durable metadata/search index with incremental byte-offset updates: after the first scan, a grown conversation file is re-read for only its appended bytes. Opt out with persistent: false for a pure in-memory scan.
  • Deep discovery — **/*.jsonl glob finds all conversations including subagents (1,472 conversations vs 351-497 from individual scanners)
  • Full metadata extraction — session ID, project, git branch, model, tool names, teammate/subagent detection
  • Full-text search — SQLite FTS5 (persistent) or FlexSearch (in-memory) across content and metadata
  • File watching — optional chokidar watcher with a periodic-rescan correctness backstop, emitting change events
  • Bounded conversation paging — read a message window without parsing the whole file, via byte-offset checkpoints
  • Configurable content tiers — standard (200/5K) and full (1,200/50K) preview/snippet limits, extensible
  • Multiple views — flat, tree (parent + subagents), grouped (by team)
  • Filtering — by project, account, time range, conversation type (conversations/subagents/teammates)
  • 5 sort modes — recent, oldest, messages-desc, messages-asc, alphabetical
  • Pagination — limit/offset on all operations
  • Multi-provider — index Threadbase/Claude history, local OpenAI Codex CLI sessions, Cursor agent-transcripts, and GitHub Copilot CLI sessions through one normalized pipeline (all but Claude are opt-in)
  • Multi-profile — scan multiple Claude config directories
  • LRU caching — metadata and conversation caches for fast repeated access
  • Git branch detection — reads .git/HEAD with parent directory walking

Installation

npm install @threadbase-sh/scanner

Requires Node.js 22 or later. The package uses better-sqlite3 (a native module) for its persistent index. Prebuilt binaries ship inside the package for common platforms, so a normal npm install needs no compiler and no install script. If the binary is somehow unavailable, openDatabase() says so explicitly and points at npm rebuild better-sqlite3 — or you can skip SQLite entirely with persistent: false.

Persistent vs. in-memory

By default the scanner maintains a durable SQLite index at ~/.config/threadbase-scanner/index.db, so repeated scans only re-read files that changed and search/list queries are indexed. To opt out of the native dependency and use the legacy in-memory path, construct with persistent: false (or pass --no-persist to the CLI):

const scanner = new ConversationScanner({ persistent: false }) // in-memory, no DB
const scanner2 = new ConversationScanner({ persistent: { dbPath: '/tmp/tb.db' } }) // custom DB

Library Usage

import { scan, search, getConversation, ConversationScanner } from '@threadbase/scanner'

// Quick scan with defaults
const result = await scan()
console.log(`Found ${result.total} conversations`)

// Scan with options
const filtered = await scan({
  sort: 'recent',
  since: '7d',
  project: 'my-app',
  include: 'conversations', // exclude subagents/teammates
  tier: 'full',             // larger previews
  limit: 20,
  offset: 0,
})

// Full-text search
const results = await search('authentication bug', {
  limit: 10,
  project: 'backend',
})

for (const r of results) {
  console.log(r.meta.projectName, r.matches[0]?.snippet)
}

// Load full conversation
const conv = await getConversation(results[0].meta.id)
for (const msg of conv.messages) {
  console.log(`[${msg.role}] ${msg.text.slice(0, 100)}`)
}

Using the class directly

import { ConversationScanner } from '@threadbase/scanner'

const scanner = new ConversationScanner({ conversationCacheSize: 10 })

// Scan with progress and batch callbacks
const result = await scanner.scan({
  onProgress: (scanned, total) => console.log(`${scanned}/${total}`),
  onBatch: (metas) => {
    // Incrementally update UI as batches complete
    for (const meta of metas) {
      addToList(meta)
    }
  },
})

// Reuse the scanner instance for cached lookups
const conv = await scanner.getConversation(someId)

// Bounded page — reads only the requested window (persistent mode seeks from a
// checkpoint instead of parsing the whole file)
const page = await scanner.getConversationPage(someId, { limit: 50 })

// Collision-safe sessionId lookup (session ids are not unique)
const all = scanner.getConversationsBySessionId('sess-123')

// Release the SQLite connection when done
scanner.close()

Scanning Codex CLI history (providers)

The scanner can index local OpenAI Codex CLI rollout sessions and Cursor agent-transcripts (~/.cursor/projects/<slug>/agent-transcripts/) alongside the default Threadbase/Claude history, normalizing all of them into the same ConversationMeta model. Extra providers are opt-in: pass providers and the matching *Roots (no home directory is scanned by default). Cursor Composer (state.vscdb) is a different store and is not indexed.

import { homedir } from 'node:os'
import { join } from 'node:path'

const scanner = new ConversationScanner()

const result = await scanner.scan({
  providers: ['claude-code', 'codex-cli', 'cursor', 'copilot'],
  codexRoots: [join(homedir(), '.codex/sessions')],
  cursorRoots: [join(homedir(), '.cursor/projects')],
  copilotRoots: [join(homedir(), '.copilot/session-state')],
})

// Each meta carries its source provider
for (const c of result.conversations) {
  console.log(c.provider) // 'claude-code' | 'codex-cli' | 'cursor' | 'copilot'
}

// Search across all, or filter to one provider
const cursorHits = await scanner.search('refactor', { provider: 'cursor' })

codexRoots / cursorRoots entries must be absolute paths — expand ~ before passing them (e.g. join(homedir(), '.cursor/projects')). Codex and Cursor metas also set kind ('conversation' | 'task') and externalSessionId when available.

Codex and Cursor rows are indexed into the same SQLite database as Claude history. A Codex sessionId is the rollout's session_meta id (the uuid at the end of rollout-<ts>-<uuid>.jsonl). A Cursor sessionId is the transcript filename stem (<runId>.jsonl). The wire name is cursor. The live PTY name cursor-cli is still accepted as an alias. Not cursor-agent. Cursor copies of another provider set isImportedFromClaude or isImportedFromCodex; isImportedFromCursor is reserved for Claude/Codex providers.

GitHub Copilot CLI (wire name copilot) keeps one directory per session, <root>/<sessionId>/events.jsonl. Pass the session-state directory (or the Copilot home above it; COPILOT_HOME moves it) as copilotRoots. The prompt is indexed as typed (user.message), with the assistant's replies and tool calls (assistant.message). A Copilot sessionId is the id in session.start, which is also the directory name. Tool results, reasoning and the session-store.db SQLite index are not read, and Copilot Chat in VS Code is a different store that is not indexed.

Resolving an id to its transcript file

Don't rebuild a provider's directory layout to find a file. Ask the index. This works on a fresh scanner over an existing index, with no scan() in the current process:

const scanner = new ConversationScanner() // opens the existing index.db
const [meta] = scanner.getConversationsBySessionId(uuid) // newest first; [] if unknown
meta?.filePath // absolute path to the .jsonl (Claude, Codex, and Cursor)
const page = await scanner.getConversationPage(uuid, { limit: 50 })

Watching for changes (persistent mode)

const scanner = new ConversationScanner() // persistent by default

scanner.on('change', ({ filePath, meta }) => {
  // meta is the fresh ConversationMeta, or null if the file was removed
  refreshUI(meta)
})

await scanner.watch() // filesystem watcher + periodic rescan backstop
// ... later
await scanner.unwatch()

View modes

// Flat (default) — all conversations in a single list
await scan({ view: 'flat' })

// Tree — parent conversations with nested subagents
await scan({ view: 'tree' })
// Returns TreeConversation[] with .subagents array

// Grouped — conversations grouped by team name
await scan({ view: 'grouped' })
// Returns { [teamName: string]: ConversationMeta[] }

Custom content tiers

await scan({
  tier: 'compact',
  tiers: {
    compact: { name: 'compact', previewMax: 50, snippetMax: 500 },
  },
})

Shared default scanner

The convenience functions scan, search, and getConversation share a lazy module-level ConversationScanner so the FlexSearch index and conversation LRU survive across calls. A first scan() warms state; a subsequent search() reuses the already-built index instead of re-walking the filesystem.

import { scan, search, getConversation, resetDefaultScanner } from '@threadbase/scanner'

await scan({ profiles })          // warms the shared scanner
await search('auth', { profiles }) // hits the in-memory index — no re-scan
await getConversation(id)         // LRU hit on subsequent calls for the same id

// Drop shared state (e.g. between tests, or to force a fresh scan)
resetDefaultScanner()

To run isolated state (parallel scans with different options, multi-tenant hosts, etc.) pass an explicit scanner as the optional third parameter:

import { ConversationScanner, scan, search } from '@threadbase/scanner'

const work = new ConversationScanner()
const personal = new ConversationScanner()

await scan({ profiles: workProfiles }, work)
await scan({ profiles: personalProfiles }, personal)

const results = await search('query', { limit: 20 }, work)

The shared scanner does not auto-refresh: it reflects the filesystem at the time of the first scan. Call resetDefaultScanner() (or scan() again) when you need to pick up newly-created .jsonl files.

Logging

The library uses pino internally and ships with a default silent logger, so embedding it produces no console output unless you opt in.

import pino from 'pino'
import { setLogger, createLogger } from '@threadbase/scanner'

// Use your own pino instance
setLogger(pino({ level: 'info' }))

// Or build one from options
setLogger(createLogger({ level: 'debug' }))

The CLI installs a pino-pretty transport on stderr at level info by default. Override with the LOG_LEVEL env var:

LOG_LEVEL=debug threadbase-scanner scan
LOG_LEVEL=silent threadbase-scanner list --json

Log events the scanner emits include scan: start / scan: complete (with timings + counts), search: start / search: complete, batched discovery summaries, parse-failure warnings, and getConversation cache-hit traces. Previously-swallowed errors (broken JSONL, inaccessible files, missing config dirs) now surface as warn-level events with structured context — useful for diagnosing why a particular conversation didn't show up.

Profiles

import { loadProfiles, saveProfiles } from '@threadbase/scanner'

// Load from ~/.config/threadbase-scanner/profiles.json
const profiles = await loadProfiles('~/.config/threadbase-scanner')

// Scan specific profiles
await scan({
  profiles: [
    { id: 'work', label: 'Work', configDir: '~/.claude-work', enabled: true },
    { id: 'personal', label: 'Personal', configDir: '~/.claude', enabled: true },
  ],
})

CLI Usage

# Install globally
npm install -g @threadbase/scanner

# Scan all conversations
threadbase-scanner scan

# List recent conversations
threadbase-scanner list --limit 20 --sort recent

# List with filters
threadbase-scanner list --since 7d --project my-app --include conversations

# Full-text search
threadbase-scanner search "fix bug" --limit 10

# Show a full conversation (prefix match on session ID)
threadbase-scanner show 879dd66c

# JSON output (for piping)
threadbase-scanner list --json | jq '.conversations[].projectName'

# Profile management
threadbase-scanner profiles list
threadbase-scanner profiles add work ~/.claude-work
threadbase-scanner profiles remove work

CLI Flags

| Flag | Commands | Description | |---|---|---| | --limit, -l | list, search | Max results (default: 20) | | --offset | list, search | Skip N results (default: 0) | | --sort, -s | list, search | recent\|oldest\|messages-desc\|messages-asc\|alpha | | --since | list, search | Time filter: 7d, 2w, 24h, 2024-01-15 | | --project, -p | list, search | Filter by project name/path | | --account, -a | list, search | Filter by profile account | | --include | list | all\|conversations\|subagents\|teammates | | --tier | list, scan | Content tier: standard\|full | | --json | all | JSON output |

ConversationMeta Fields

Every scanned conversation produces a ConversationMeta with the full superset of fields from all four original scanners:

| Field | Type | Origin | |---|---|---| | id | string | All | | filePath | string | All | | sessionId | string | All | | sessionName | string | All | | projectPath | string | All | | projectName | string | All | | account | string | All | | timestamp | string (ISO-8601) | All | | messageCount | number | All | | lastMessageSender | 'user' \| 'assistant' | Electron/VS Code/IntelliJ | | preview | string | All (tier-dependent) | | contentSnippet | string | Electron/VS Code/IntelliJ (tier-dependent) | | gitBranch | string | null | IntelliJ/CLI | | model | string | null | IntelliJ | | isSubagent | boolean | VS Code | | parentSessionId | string | null | VS Code | | isTeammate | boolean | VS Code | | teamName | string | null | VS Code | | toolNames | string[] | CLI | | provider | 'claude-code' \| 'codex-cli' \| 'cursor' \| 'copilot' | Provider that produced the meta | | kind | 'conversation' \| 'task' | Codex / Cursor (optional) | | externalSessionId | string | Provider-native session id (optional) | | isImportedFromClaude | boolean | Cursor copy of a Claude session (optional; omit when false) | | isImportedFromCodex | boolean | Cursor copy of a Codex session (optional; omit when false) | | isImportedFromCursor | boolean | Reserved for Claude/Codex copies of Cursor (optional; omit when false) |

Development

npm install
npm test          # run tests
npm run build     # build ESM + CJS + types
npm run lint      # type check

Contributing

Small bugfixes and parser improvements are welcome. For design changes, please open an issue first to discuss the shape before opening a PR.

  • Use conventional commits (feat:, fix:, chore:, etc.) — see CLAUDE.md for project conventions.
  • Run npm run lint && npm test before opening a PR.
  • New features need an integration or e2e test in __tests__/; new parser cases need a fixture in __fixtures__/.

Architecture

src/
  index.ts        Public API exports + standalone functions
  types.ts        All interfaces (ConversationMeta, ScanOptions, etc.)
  scanner.ts      ConversationScanner class (main orchestrator)
  discovery.ts    File discovery (fast-glob + exclusions)
  parser.ts       JSONL parsing (meta + full conversation)
  indexer.ts      FlexSearch-based search indexing
  filters.ts      Sort, since-filter, include, pagination
  cache.ts        LRU cache
  git.ts          Git branch detection
  profiles.ts     Profile management
  tags.ts         System tag cleaning
  tiers.ts        Content tier definitions
  logger.ts       Pino-based logger seam (silent by default)
cli/
  index.ts        CLI entry point (commander)
  commands/       list, search, show, scan, profiles