jlreyes/agent-history-skills · Archived

exploring-cursor-history

Finds and explores Cursor IDE conversation history stored locally in SQLite databases and plaintext agent transcripts. Use when the user asks to find, search, read, or export a Cursor chat session, agent conversation, composer thread, or transcript.

First seen Jun 11, 2026

Installation

$ npx skills add jlreyes/agent-history-skills --skill exploring-cursor-history

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from jlreyes/agent-history-skills.

npx skills add jlreyes/agent-history-skills

Browse all from jlreyes/agent-history-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 3
License LICENSE
Default branch main
Open issues 0
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

CompatibilityRequires sqlite3 CLI and jq. IDE paths below are macOS; on Linux the IDE data root is ~/.config/Cursor, on Windows %APPDATA%\Cursor. The CLI root defaults to ~/.cursor (Linux/BSD honours $XDG_CONFIG_HOME/cursor; $CURSOR_CONFIG_DIR overrides everywhere).
Allowed toolsBash(sqlite3 *) Bash(jq *) Bash(find *) Bash(sort *) Bash(head *) Bash(sed *) Bash(dirname *)
Declared agents cursor
More metadata
author
jlreyes

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,279 B
  • docs SUMMARY.md 281 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 7 installs

SKILL.md

Exploring Cursor History

Cursor stores IDE conversations in SQLite (state.vscdb), with the global DB as the single source of truth — workspace DBs only hold legacy metadata (pre-3.0). The CLI (agent) stores its own sessions under a root that defaults to ~/.cursor. Current official releases are Desktop 3.19 / CLI 2026.09.02-c22c1a3; local schema evidence is Desktop 3.15.6 and CLI through 2026.08.04 because current live generation did not complete.

Storage locations

IDE paths are shown for macOS (~/Library/Application Support/Cursor → ~/.config/Cursor on Linux, %APPDATA%\Cursor on Windows). The CLI root defaults to ~/.cursor; substitute the XDG or CURSORCONFIGDIR override when configured.

Path What it holds
…/Cursor/User/globalStorage/state.vscdb (IDE) All conversation content + metadata — cursorDiskKV key/value table plus a composerHeaders index table. Can be ~1 GB
…/Cursor/User/globalStorage/conversation-search.db (IDE) FTS5 full-text index over conversation titles + bodies, added in Cursor 3.11. Fastest search surface; best-effort, not exhaustive
…/Cursor/User/workspaceStorage/<hash>/state.vscdb (IDE) Legacy per-workspace conversation metadata (stopped updating at Cursor 3.0); workspace.json sibling maps hash → folder (folder or workspace key)
~/.cursor/projects/<path-slug>/agent-transcripts/<id>/<id>.jsonl Plaintext JSONL transcript of agent conversations — IDE agent threads and CLI agent sessions. Only exists for recent conversations (from ~Cursor 3.0); sub-agents live in a subagents/<subagentId>.jsonl sibling dir
~/.cursor/chats/<md5>/<sessionId>/store.db CLI (agent) session store + meta.json. <md5> = md5 of the workspace absolute path
~/.cursor/plans/*.plan.md Plan-mode artifacts (markdown)
~/.cursor/prompt_history.json Rolling array of recent typed prompts (small; IDE only)

Schema (quick reference)

Global DB, cursorDiskKV table (key/value):

  • composerData:<composerId> — one row per conversation: name, subtitle, unifiedMode (agent/chat/plan/edit), createdAt/lastUpdatedAt (epoch ms). The last bubble's createdAt is exact only when present; it was absent for 447/990 nonempty conversations. Fall back to lastUpdatedAt/createdAt as approximate. fullConversationHeadersOnly[] is the ordered message list: {bubbleId, type} (1=user, 2=assistant). workspaceIdentifier is almost never present (4 of 2473 rows here) — attribute projects via the composerHeaders table, the legacy workspace lookup, or the ~/.cursor/projects slug.
  • bubbleId:<composerId>:<bubbleId> — one row per message: text (only on text turns), type, createdAt (ISO, often absent), toolFormerData ({name, params, result, status} — tool calls), thinking.text (reasoning), codeBlocks, context (file selections etc.)

Global DB, composerHeaders table (added ~Cursor 3.9): (composerId, workspaceId, createdAt, lastUpdatedAt, isArchived, isSubagent, recency, checkpointAt, value) — a forward-only index with a precomputed recency and real workspace attribution, but only for conversations created after the migration (26 rows vs 990 conversations with content here). Use it to filter/attribute recent chats; use composerData:% for the full history.

Iterate via headers, not raw bubble rows — orphan non-null bubbles from regenerated/deleted turns were observed; headers remain authoritative. Always open DBs read-only (file:...?mode=ro). In sqlite3 shell arguments, escape JSON paths as '\$.field' so the shell doesn't mangle them.

All queries below assume:

GLOBAL_DB="$HOME/Library/Application Support/Cursor/User/globalStorage/state.vscdb"
SEARCH_DB="$HOME/Library/Application Support/Cursor/User/globalStorage/conversation-search.db"

Resolve a partial composer ID

sqlite3 "file:$GLOBAL_DB?mode=ro" \
  "SELECT substr(key,14) FROM cursorDiskKV WHERE key LIKE 'composerData:1de03385%' LIMIT 1;"

List recent conversations

sqlite3 -separator ' | ' "file:$GLOBAL_DB?mode=ro" "
  SELECT datetime(json_extract(c.value,'\$.createdAt')/1000,'unixepoch','localtime'),
         substr(c.key,14,12),
         json_extract(c.value,'\$.unifiedMode'),
         coalesce(nullif(json_extract(c.value,'\$.name'),''), json_extract(c.value,'\$.subtitle'), '?')
  FROM cursorDiskKV c
  WHERE c.key LIKE 'composerData:%'
    AND json_array_length(c.value,'\$.fullConversationHeadersOnly') > 0
  ORDER BY 1 DESC LIMIT 30;"

To sort by best-available last activity, order by the last bubble's timestamp ([#-1] = last array element), then fall back to composer timestamps:

coalesce(
  (SELECT unixepoch(json_extract(b.value,'$.createdAt')) * 1000 FROM cursorDiskKV b
   WHERE b.key = 'bubbleId:' || substr(c.key,14) || ':' ||
         json_extract(c.value,'$.fullConversationHeadersOnly[#-1].bubbleId')),
  json_extract(c.value,'$.lastUpdatedAt'),
  json_extract(c.value,'$.createdAt'))

For conversations created since the composerHeaders migration, that work is already done — and this is the only place with reliable workspace attribution and a subagent flag:

sqlite3 -separator ' | ' "file:$GLOBAL_DB?mode=ro" "
  SELECT datetime(recency/1000,'unixepoch','localtime'), substr(composerId,1,12), workspaceId,
         json_extract(value,'\$.unifiedMode'), json_extract(value,'\$.workspaceIdentifier.uri.fsPath')
  FROM composerHeaders WHERE isSubagent=0 ORDER BY recency DESC LIMIT 20;"

Read a conversation transcript

One query — json_each walks the ordered header list and joins each bubble:

CID="<full composerId>"
sqlite3 "file:$GLOBAL_DB?mode=ro" "
  SELECT CASE json_extract(j.value,'\$.type') WHEN 1 THEN '## USER' ELSE '### ASSISTANT' END
         || coalesce('  ['||json_extract(b.value,'\$.toolFormerData.name')
                     ||' '||coalesce(json_extract(b.value,'\$.toolFormerData.status'),'')||']','')
         || char(10) || coalesce(nullif(json_extract(b.value,'\$.text'),''),'') || char(10)
  FROM cursorDiskKV c, json_each(c.value,'\$.fullConversationHeadersOnly') j
  LEFT JOIN cursorDiskKV b ON b.key = 'bubbleId:' || substr(c.key,14) || ':' || json_extract(j.value,'\$.bubbleId')
  WHERE c.key = 'composerData:$CID'
  ORDER BY j.key;"

Add json_extract(b.value,'$.thinking.text') for reasoning or '$.createdAt' for timestamps. Tool results can be large and may contain sensitive material; do not export them by default.

Search conversations

Fastest path — the FTS5 index (conversationfts(title, body), joined to conversations on ftsrowid; conversations.id is the composerId):

sqlite3 -separator ' | ' "file:$SEARCH_DB?mode=ro" "
  SELECT datetime(c.updated_at/1000,'unixepoch','localtime'), substr(c.id,1,12), c.title,
         replace(snippet(conversation_fts,1,'«','»','…',12), char(10),' ')
  FROM conversation_fts f JOIN conversations c ON c.fts_rowid = f.rowid
  WHERE conversation_fts MATCH 'SEARCH_TERM'
  ORDER BY c.updated_at DESC LIMIT 20;"

Supports FTS5 syntax (title:foo, a OR b, "exact phrase", pref*). It is not exhaustive: it indexes conversations up to a cap (conversationsearchsettings.effectiveconversationcap) and ~1/3 of indexed rows here have an empty body. Fall back to a raw scan of the global DB when a known string isn't found (~0.9 s over a 932 MB DB):

sqlite3 "file:$GLOBAL_DB?mode=ro" "
  SELECT key, json_extract(value,'\$.type'), substr(json_extract(value,'\$.text'),1,120)
  FROM cursorDiskKV
  WHERE key LIKE 'bubbleId:%' AND value LIKE '%SEARCH_TERM%'
  LIMIT 20;"

The LIKE on raw value also matches thinking text and tool results; pull specific fields from the hits afterwards. The key embeds the composerId: bubbleId:<composerId>:<bubbleId>.

List legacy (pre-3.0) conversations with workspace attribution

Conversations from before Cursor 3.0 (April 2026) have no workspaceIdentifier; their metadata lives in workspace DBs:

cd ~/Library/Application\ Support/Cursor/User/workspaceStorage
for d in */; do
  ws=$(jq -r '.folder // .workspace // empty' "$d/workspace.json" 2>/dev/null | sed 's|^file://||')
  sqlite3 "file:$d/state.vscdb?mode=ro" "
    SELECT json_extract(j.value,'\$.lastUpdatedAt') || '|' || substr(json_extract(j.value,'\$.composerId'),1,12)
           || '|' || coalesce(nullif(json_extract(j.value,'\$.name'),''), json_extract(j.value,'\$.subtitle'), '?')
           || '|' || '${ws##*/}'
    FROM ItemTable t, json_each(t.value,'\$.allComposers') j
    WHERE t.key='composer.composerData';" 2>/dev/null
done | sort -t'|' -rn | head -30

(Qualify t.key — json_each emits its own key column. Nothing newer than the 3.0 cutover appears here; always check the global DB first.)

Safe text export (no SQLite at all)

Each line is one JSON record: role messages are {role:"user"|"assistant", message:{content:[…]}}; controls are {type:"turnended",status:"success"} or add error with status:"aborted". This safe text export emits tool names only: inputs and results can expose commands, paths, queries, edits, or secrets. It is intentionally not byte-complete; inspect selected input fields only when explicitly needed. No toolresult part was observed in the 46-file corpus.

jq -r 'select(.role) | .role + ": " +
  ([.message.content[]
    | if .type=="text" then .text
      elif .type=="tool_use" then "[tool_use "+.name+"]"
      else "["+.type+"]" end] | join("\n"))' \
  ~/.cursor/projects/<slug>/agent-transcripts/<id>/<id>.jsonl

<slug> = workspace absolute path with the leading / dropped and remaining / → -. In the observed corpus, 20/100 user records wrapped the prompt as <timestamp>…</timestamp>\n<userquery>\n…\n</userquery>. No toolresult part was observed; the JSONLs did contain tooluse calls.

Cursor CLI (agent) sessions

A headless run writes both the JSONL transcript above and ~/.cursor/chats/<md5(workspacePath)>/<sessionId>/store.db + meta.json. <sessionId> is the session_id from --output-format json. --continue/--resume rewrite the JSONL with the full conversation. List sessions without the TUI:

find ~/.cursor/chats -type f -name meta.json -print0 2>/dev/null |
  while IFS= read -r -d '' m; do
    jq -r --arg d "$(dirname "$m")" '"\(.updatedAtMs)  \($d|split("/")|last)  \(.cwd // "?")"' "$m"
  done | sort -rn

store.db is a content-addressed blob store (blobs, meta); the JSONL is the readable surface — see [data-model.md](data-model.md#cli-agent-store-storedb) before touching it.

Tips

  • Tool-call turns have empty text — render toolFormerData.name instead. The legacy toolResults / suggestedCodeBlocks / assistantSuggestedDiffs fields still exist on the 56,085 versioned bubble objects but are always empty arrays; don't read them.
  • Tool names differ per surface: the DB uses internal names (readfilev2, ripgreprawsearch, runterminalcommand_v2); observed JSONL display names are AwaitShell, Delete, GetMcpTools, Glob, Grep, Read, ReadLints, Shell, StrReplace, TodoWrite, WebFetch, WebSearch, and Write.
  • The JSONL mirror is not a complete history — only 33 of 990 conversations with content had one here (it starts around Cursor 3.0). Use the DB when a conversation is missing.
  • Sub-agent threads are full composerData rows too (listed in the parent's subagentComposerIds), so they show up in "list recent conversations" — filter with composerHeaders.isSubagent when you only want top-level chats.
  • Map a composerId to its project via composerHeaders.workspaceId / value.workspaceIdentifier.uri.fsPath, the legacy workspace lookup, or by which ~/.cursor/projects/<slug>/agent-transcripts/ directory contains it.
  • Current help visibly lists agent create-chat, agent ls, agent resume, and agent persist list|attach|stop; persist manages detached processes, not export. On the older 2026.08.04 CLI, ls/resume were Ink TUIs that could hang under piped stdin. There is no export/history/sessions subcommand.
  • Cloud/background agents (bc-* IDs) keep almost nothing locally — only the title is cached, in conversation-search.db as source='cloud-cache'; transcripts are server-side.
  • A conversation that shows "Chat Too Old" in the UI is still fully readable from the DB; only its server conversationState token is lost.