README
¶
somniloq
A CLI tool that imports Claude Code, Codex, and Cursor Agent session logs (JSONL) into SQLite for searching and browsing.
It parses JSONL files under ~/.claude/projects/, ~/.codex/sessions/, and ~/.cursor/projects/, enabling cross-session search of past conversations.
Features
- Differential import — Auto-detects Claude Code, Codex, and Cursor Agent JSONL files and imports only what's new
- Cross-session search — Search message bodies and filter by project name and time range
- Long-session navigation — Check session size, skim an outline, then read only the turns you need
- Markdown and JSON output — Export readable Markdown or script-friendly JSON
- Built for Coding Agents — Invoke from skills to use past sessions as context
- Fully local — No external services required. Pure Go + SQLite
Installation
go install github.com/ryotapoi/somniloq/cmd/somniloq@latest
Quick Start
# Import Claude Code, Codex, and Cursor Agent session logs
somniloq import
# List sessions
somniloq sessions
# Sessions from the last 24 hours
somniloq sessions --since 24h
# Sessions saved or updated in the last 24 hours
somniloq sessions --imported-since 24h
# Search message bodies
somniloq search --since 7d "auth bug"
# Skim a long session, then read only relevant turns
somniloq outline <session-id>
somniloq show --turn 40..60 <session-id>
# Show session content
somniloq show <session-id>
# Export the last week as Markdown
somniloq show --since 7d
Commands
| Command | Description |
|---|---|
import |
Import Claude Code, Codex, and Cursor Agent JSONL files into SQLite |
backfill |
Migrate/repair existing DB rows |
sessions |
List sessions |
projects |
List projects with session counts |
show |
Show session content in Markdown |
outline |
List a session's user messages as turn number, time, body size, and first line |
search |
Search message content across sessions with turn numbers |
import
somniloq import # differential import (default)
somniloq import --source claude-code
somniloq import --source codex
somniloq import --source cursor-agent
somniloq import --full # full re-import (with confirmation)
somniloq import --full --yes # skip confirmation
Imports Claude Code JSONL from ~/.claude/projects/, Codex rollout JSONL from ~/.codex/sessions/, and Cursor Agent transcripts from ~/.cursor/projects/. Use --source all|claude-code|codex|cursor-agent to limit the import target. The default is all.
--full always clears the whole somniloq DB before re-importing. If you run somniloq import --source codex --full, Claude Code rows are deleted too, then only Codex logs are imported.
Errors are non-fatal: lines that cannot be parsed (broken JSON, malformed payloads) are skipped and counted in the summary (... N unparsed lines). To make schema changes diagnosable, the first five parse/normalization failures are listed on stderr as file:line: error; they do not change the import exit code. Unreadable directories or files are skipped while the rest is still imported; those errors are listed on stderr and make the exit code 1. A missing source directory is treated as an unused source, not an error.
backfill
somniloq backfill # repair existing rows (with confirmation if rows will be deleted)
somniloq backfill --yes # skip confirmation
Migrates and repairs DB rows produced by older versions. Specifically:
- Migrates v0.3 databases to the v0.4 schema (
sourcecolumns and(source, session_id)session keys). - Resolves
repo_pathfor sessions where it isNULLandcwdis non-empty. - Deletes
sessionsrows that have nomessages(leftover meta-only rows from v0.2.x).
Run backfill once after upgrading to v0.4 before importing. When there are sessions to delete, backfill prompts before proceeding (default No). --yes skips the prompt; in non-interactive environments (pipes, CI), --yes is required if any rows would be deleted. Re-running is safe.
sessions
somniloq sessions # all sessions
somniloq sessions --since 24h # last 24 hours
somniloq sessions --imported-since 24h # sessions saved or updated in the last 24 hours
somniloq sessions --since 7d # last 7 days
somniloq sessions --since 2026-03-28 # after a date (local time)
somniloq sessions --until 2026-03-28 # before a date (local time)
somniloq sessions --since 2026-03-28 --day-boundary 04:00 # after the logical day boundary
somniloq sessions --since 7d --until 2h # 7 days ago to 2 hours ago
somniloq sessions --project myapp # substring match against repo_path
somniloq sessions --short # basename of repo_path for unaliased projects
somniloq sessions --format json # JSON array instead of TSV
Output is TSV: session_id, started_at ~ ended_at, logical_day, project, custom_title, message_count, body_size, non_command_user_turn_count, first_non_command_user_line, source (claude_code, codex, or cursor_agent).
logical_day is derived at query time from ended_at (or started_at when ended_at is empty), using the local dayBoundary. Sessions are not split across days.
Sessions with an unknown timestamp remain visible without a time filter, but do not match --since or --until. When both timestamps are unknown, the TSV time range and Markdown Started value are empty.
sessions --imported-since <time> filters by the inclusive imported_at lower bound. It accepts the same relative, local-date, minute-precision, and RFC3339-instant forms as --since, but a date starts at local midnight and ignores --day-boundary. It combines with --since, --until, and --project using AND. imported_at is the UTC, whole-second start time of the import pass that last saved the session; it is not a message-change timestamp, commit time, or exactly-once consumption watermark. Pass the output source and session_id to somniloq show --source <source> <session-id> to read the session.
When projectAliases matches a repo path or basename, project output uses only the canonical name.
body_size is the total body size in bytes (sidechain excluded), so you can tell whether a session is large before showing it.
non_command_user_turn_count and first_non_command_user_line are skip hints for consumers: they use the same sidechain-excluded user-turn population as outline, then ignore user turns whose trimmed content starts with / or matches a configured commandPatterns regex. The CLI only reports these values; it does not skip sessions.
--format json emits a JSON array with source, sessionId, project, title, startedAt, endedAt, logicalDay, messageCount, bodySize, nonCommandUserTurnCount, firstNonCommandUserLine. JSON timestamps are the stored RFC3339 UTC values (see "JSON output" below).
projects
somniloq projects # all projects
somniloq projects --since 7d # projects active in the last 7 days
somniloq projects --short # basename of repo_path for unaliased projects
somniloq projects --format json
Output is TSV: project, session_count. With --format json: project, sessionCount. Alias groups are displayed and counted under the canonical project name.
Sessions with an unknown repository remain in the empty project group without a filter; they never match --project, including a literal %.
show
somniloq show <session-id> # single session
somniloq show --source codex <session-id> # select a source-qualified session
somniloq show --since 24h # last 24 hours
somniloq show --since 2026-03-28 --until 2026-03-29 # date range
somniloq show --since 7d --project myapp # filter by project
somniloq show --summary 1 --since 24h # first user message per session
somniloq show --summary 3 --since 24h # first 3 user messages per session
somniloq show --short --since 24h # basename of repo_path for unaliased projects
somniloq show --turn 40..60 <session-id> # only turns 40-60
somniloq show --tail 3 <session-id> # only the last 3 turns
somniloq show --format json <session-id> # JSON instead of Markdown
--turn / --tail use the same turn numbering as outline (1-based, incremented on each user message), so you can skim the outline first and read only the range you need. A turn includes the user message and the replies that follow it. --turn and --tail are mutually exclusive, cannot be combined with --summary, and in bulk mode (--since/--until) apply to each listed session independently.
Markdown metadata includes Source. show and outline accept --source claude_code|claude-code|codex|cursor_agent|cursor-agent with a session ID, so use the source value from a search result to select the matching session. If --source is omitted and a session ID exists in multiple sources, they report the source/session candidates as an ambiguity error rather than selecting one. --source cannot be used with show --since/--until; all, an empty value, and unknown sources are rejected.
--format json emits a JSON array of sessions — always an array, even for a single session ID — where each element has source, sessionId, project, title, startedAt, endedAt, and messages (role, content, timestamp). --summary / --turn / --tail filtering applies to messages as-is.
outline
somniloq outline <session-id> # user messages as turn number, time, body size, and first line
somniloq outline --source cursor_agent <session-id> # select a source-qualified session
somniloq outline --format json <session-id> # JSON instead of TSV
Grasp the structure of a long session before showing it in full. Output is TSV: turn, time, body_size, first_line. Turn numbers start at 1 and increment on each user message (sidechain rows are excluded). body_size is the total UTF-8 byte size of all non-sidechain message bodies in that turn, including replies. With --format json: turn, timestamp, bodySize, firstLine.
search
somniloq search "auth bug" # search all message bodies
somniloq search --since 7d "auth" # messages written in the last 7 days
somniloq search --since 2026-03-28 --day-boundary 04:00 "auth" # messages since 04:00 on that local day
somniloq search --since 7d --project myapp "auth" # narrowed by project
somniloq search --limit 50 --offset 50 "auth bug" # next 50 results
somniloq search --format json "auth bug" # JSON search results
Default output is TSV: session_id, turn, time, project, snippet, source (the internal identifier: claude_code, codex, or cursor_agent), newest first. --format json emits an array with source, sessionId, turn, timestamp, project, and snippet; timestamps and snippets retain their stored/raw values. turn uses the same numbering as outline and show --turn, so a hit can feed directly into somniloq show --source <source> --turn <N> <session_id> or somniloq outline --source <source> <session_id>. Matching follows SQLite LIKE: case-insensitive for ASCII only; %, _, and \ in a query are literal text. Unlike sessions/show, --since/--until filter on the message timestamp — the time the content was written, not when the session started. Date-only filters use dayBoundary. Sidechain messages are excluded. --limit N returns at most N results (N >= 1; omitted means unlimited), and --offset M skips M ordered results (M >= 0; default 0) after filtering and sorting. Continue with a fixed query/filter and increasing --offset; pages are stable only while the DB and resolved time conditions are fixed. Database changes and snapshots are unsupported.
JSON output
sessions, projects, outline, search (--format tsv|json) and show (--format markdown|json) support JSON output for scripts. Rules common to all commands:
- Always a JSON array; empty results print
[]. - Timestamps are the stored RFC3339 UTC values, not the local-time display format.
- Strings are raw values (no tab/newline sanitizing; JSON escaping covers it).
titleis the raw custom title with no session-id fallback. projectuses the canonical project alias when configured; otherwise it honors--short, and without--shortyou get the rawrepo_path.
Configuration
Optional config file at ~/.somniloq/config.json (override with the global --config flag). A missing file is fine; broken JSON is an error.
{
"projectAliases": {
"newname": ["oldname"]
},
"commandPatterns": ["^Daily report"],
"dayBoundary": "04:00"
}
projectAliases groups project names that refer to the same project over time (e.g. a renamed repository): current name → old names. When a --project value exactly matches any name in a group, the filter expands to the whole group, so sessions recorded under either name are found. Non-matching values behave as before. Project filtering is a literal substring match, including %, _, and \; empty or unknown repo_path values never match it and remain in the empty project group without a filter. Filtering applies to sessions, show, and search. Project display in sessions, show, projects, and search uses only the canonical name when the stored repo_path or basename matches an alias group; projects also aggregates those rows under the canonical name.
commandPatterns is a list of Go regular expressions used only by sessions skip-hint columns. Each pattern matches against the trimmed full user message. Invalid regular expressions make config loading fail, the same as broken JSON, so typos do not silently disable the setting.
dayBoundary sets the logical day start time as HH:MM local time. It defaults to 00:00 and can be overridden per command with --day-boundary on sessions and search. It only changes date-only --since/--until values and the sessions logical-day column; stored timestamps stay raw, so changing the boundary does not require re-import.
Common Options
| Option | Description | Default |
|---|---|---|
--db <path> |
Path to SQLite database | ~/.somniloq/somniloq.db |
--config <path> |
Path to config file (JSON) | ~/.somniloq/config.json |
--version |
Print version and exit | — |
Requires SQLite 3.35 or later (for
ALTER TABLE ... DROP COLUMN). The bundledmodernc.org/sqlitedriver ships with a recent SQLite, so no separate install is needed.
Time Filters
--since and --until accept the following formats:
| Format | Example | Meaning |
|---|---|---|
| Relative | 30m, 24h, 7d |
That amount of time ago |
| Date | 2026-03-28 |
The configured local dayBoundary on that day for sessions/search; otherwise 00:00 local time |
| Local datetime | 2026-03-28T15:00 |
Exact local time |
| RFC3339 instant | 2026-03-28T15:00:00Z, 2026-03-29T00:00:00+09:00 |
Exact instant using the explicit UTC or numeric offset |
Upgrading to v0.4
v0.4 adds Codex support and changes the session key to include source. Existing databases need a one-time migration/repair through backfill.
- Back up the DB.
backfilldeletes orphan rows (see below), so copy~/.somniloq/somniloq.dbaside first. - Install the v0.4 binary, then run:
This migrates v0.3 rows to the v0.4 schema, resolvessomniloq backfillrepo_pathfor older rows, and removessessionsrows that have nomessages(leftovers from the v0.2.x meta-only INSERT path). - Import current logs.
somniloq import - Optional — refill JSONL you previously archived. If you moved old Claude Code JSONL out of
~/.claude/projects/, copy only the missing files back, then re-import:cp -rn /path/to/old-projects/. ~/.claude/projects/ somniloq import --full --yes
CLI behavior changes
--projectnow matchesrepo_pathonly. The previous fallback to aproject_dircolumn is gone, so older sessions whoserepo_pathis stillNULLwill not match--projectuntil you runsomniloq backfill.sessions/projectsTSV output showsprojectfromrepo_path(noproject_dirfallback column). Configured project aliases display as the canonical name.--shortshowsfilepath.Base(repo_path)for unaliased projects.importnow imports Claude Code, Codex, and Cursor Agent logs by default. Select one source with--source claude-code|codex|cursor-agent.- From v0.12.0,
searchqueries and--projecttreat%,_, and\as literal text rather than wildcards. Results for old wildcard patterns therefore change; there is no wildcard mode. No database migration, backfill, or re-import is required.