一键导入
cc-convo-explorer
Explore and search previous Claude Code conversations stored in ~/.claude/projects/
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Explore and search previous Claude Code conversations stored in ~/.claude/projects/
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Always use this skill when writing or editing python code!
Use this skill when working with software installed or run from local source checkouts that use git worktree hubs.
Always use this skill when writing or editing markdown files!
Always use this skill when writing or editing webscrapers!
Extracting text and tables, merging/splitting documents. Use when needing to convert PDFs while preserving structure.
Always use this skill when writing or editing software tests!
| name | cc-convo-explorer |
| description | Explore and search previous Claude Code conversations stored in ~/.claude/projects/ |
Claude Code stores conversation history as JSONL files in ~/.claude/projects/, one JSON object per line.
~/.claude/projects/{encoded-project-path}/
{session-id}.jsonl - main conversation files (session id is a UUID){session-id}/ - per-session subdirectory holding ancillary data
subagents/agent-{hash}.jsonl - subagent transcripts (records have isSidechain: true)subagents/agent-{hash}.meta.json - sidecar (agentType, description, name, toolUseId)subagents/journal.jsonl, subagents/agent-compact-{hash}.jsonl - workflow/compaction statetool-results/*.txt - raw tool-result payloads (not JSONL)workflows/ - workflow run statesessions-index.json, memory, debug-{digits}.jsonl - other per-project entries~/.claude/history.jsonl - every user-typed message across all projects (see below)The project dir name is the absolute project path with each of /, ., and _ collapsed to a single -.
/home/user/company/proj -> -home-user-company-proj/home/user/.claude -> -home-user--claude/home/user/svc_410439 -> -home-user-svc-410439ls -lah); process line-by-line above ~60 KB.awk 'NR==5 { print length; exit }' file.jsonl checks the length of line 5 before reading it.jq instead of reading whole lines.history*.jsonl and */tool-results/*.txt when scanning a tree for JSONL records - they are not conversation lines and pollute a naive census.Heavyweight records (user, assistant, attachment, system, progress) share a common envelope; the rest carry only type, sessionId, and their own payload.
type - record typeuuid / parentUuid - record id and its parent (parentUuid: null marks a session's initial message)timestamp - ISO-8601 stringsessionId, cwd, gitBranch, version, entrypoint - session context (entrypoint: cli / sdk-cli / sdk-ts)isSidechain - true only in subagent filesuserType - externalversion is 2.1.x. Model ids (message.model) are claude-opus-4-x / claude-sonnet-4-x / claude-fable-x / claude-haiku-4-x, plus <synthetic> for injected or error turns.
Don't hardcode either - they move constantly.
User-side turns: initial prompts, follow-ups, and tool-result carriers.
parentUuid: null): message.content is a plain string (or a short list of {type: "text", text} blocks) holding the clean user prompt.
parentUuid set): message.content is a list of tool_result blocks (tool_use_id, content); structured tool metadata is mirrored in a top-level toolUseResult.
text blocks here are mostly skill expansions or [Request interrupted by user], not user-typed text.promptId, permissionMode, promptSource, origin.{"type": "user", "parentUuid": null, "message": {"role": "user", "content": "the prompt"}}
Model turns. message.content is a list of blocks:
thinking - reasoning (thinking, signature)text - response texttool_use - tool call (id, name, input)message also has model, id, stop_reason, and usage (with input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens).
A turn may be attributed to a skill or MCP tool via top-level attributionSkill / attributionMcpServer / attributionMcpTool.
Message-queue lifecycle. The best source for the full text of follow-up messages, including pasted content.
{"type": "queue-operation", "operation": "enqueue", "sessionId": "...", "timestamp": "...", "content": "full follow-up text"}
operation - enqueue, dequeue, remove, popAllcontent - present on enqueue and popAll; absent on dequeue / remove
<task-notification> XML for agent notifications - filter those out.type, operation, sessionId, timestamp.System/meta events keyed by subtype. Field set varies per subtype.
away_summary - prose recap of what the agent did while you were awaylocal_command - .content holds the <command-name> of a slash-command invocationturn_duration, stop_hook_summary, compact_boundary, api_error (carries error), bridge_status, scheduled_task_fireContext injected into a turn, keyed by attachment.type. Mostly bookkeeping (task_reminder, deferred_tools_delta, skill_listing, edited_text_file, nested_memory, command_permissions, ...).
queued_command is search-relevant: attachment.prompt holds real user follow-up text - a source beyond queue-operation.Streaming status during long tool/subagent operations (data.type: hook_progress, agent_progress, ...).
High volume, low value for search; the underlying subagent output is already in the agent-*.jsonl files.
Tiny records, useful as a cheap per-session index without parsing message bodies:
last-prompt - lastPrompt: full text of the session's most recent user promptai-title - aiTitle: auto-generated topic label for the sessioncustom-title - customTitle: user-set label (overrides ai-title)agent-name - agentName: readable label for a (sub)agent sessionpr-link - prNumber / prUrl / prRepository: the PR a session openedfork-context-ref - parentSessionId / parentLastUuid (in subagent files): links a fork back to its originPure bookkeeping (skip for search): permission-mode, mode, bridge-session, agent-setting, file-history-snapshot, debug_claude.
One record per user-typed message, across all projects. The simplest source for "what did the user type", though pasted content is abbreviated.
{"display": "the typed text", "pastedContents": {}, "project": "/abs/path", "sessionId": "uuid", "timestamp": 1765934747869}
display - the message as typed; pasted blocks show as [Pasted text #1 +N lines]project - absolute project path (matches the conversation file's cwd, not the encoded dir name)sessionId - present on recent records; older records and bare slash-commands (e.g. /mcp status) may omit ittimestamp - epoch millisecondspastedContents - map keyed "1", "2", ... per pasted block; {} when none. Two variants:
{"id": 1, "type": "text", "contentHash": "hex"} (text stored elsewhere by hash){"id": 1, "type": "text", "content": "full pasted text"}id is an integer and may be null.Three sources, by completeness:
history.jsonl - every message with timestamps; pasted content abbreviated.queue-operation content and queued_command attachment.prompt - full follow-up text including pasted content.parentUuid: null user records - the full initial prompt of each session.import json
from pathlib import Path
HISTORY = Path("~/.claude/history.jsonl").expanduser()
def typed_messages(project_path: str) -> list[str]:
"""Every user-typed message for a project, oldest first."""
out: list[tuple[int, str]] = []
for line in HISTORY.read_text().splitlines():
record = json.loads(line)
if record.get("project") != project_path:
continue
display = (record.get("display") or "").strip()
if display and not (display.startswith("/") and " " not in display):
out.append((record.get("timestamp", 0), display))
out.sort()
return [text for _, text in out]
import json
from pathlib import Path
def session_user_messages(jsonl_path: str | Path) -> list[str]:
"""Full user-typed text (initial prompt + follow-ups) for one session, in order."""
out: list[tuple[str, str]] = []
for line in Path(jsonl_path).read_text().splitlines():
if not line.strip():
continue
record = json.loads(line)
rtype = record.get("type")
if rtype == "queue-operation" and record.get("operation") in ("enqueue", "popAll"):
content = (record.get("content") or "").strip()
if content and not content.startswith("<task-notification>"):
out.append((record.get("timestamp", ""), content))
elif rtype == "user" and record.get("parentUuid") is None:
content = record.get("message", {}).get("content")
if isinstance(content, str):
text = content
elif isinstance(content, list):
text = "".join(
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
)
else:
text = ""
if text.strip():
out.append((record.get("timestamp", ""), text.strip()))
out.sort()
return [text for _, text in out]
Useful shell one-liners (records have no spaces after colons, so the compact patterns match):
head -1 file.jsonl | jq -c keys # keys of the first record
grep '"type":"user"' file.jsonl | head -1 | jq # first record of a type
grep '"type":"assistant"' file.jsonl | head -1 | jq 'del(.message.content)' # structure, minus bulk
jq -r .type file.jsonl | sort | uniq -c | sort -rn # count records by type
cat *.jsonl | jq -rR 'fromjson? | .type' | sort -u # all types in a dir (tolerates non-JSON)
grep -h '"type":"queue-operation"' *.jsonl | head -1 | jq # first match across files (-h drops filename)
Keyword search across a project's files:
import json
import sys
from pathlib import Path
def search(base_path: str | Path, keyword: str):
"""Yield (filename, line_no, record_type) for lines containing keyword."""
needle = keyword.lower()
for fpath in Path(base_path).glob("*.jsonl"):
with fpath.open() as f:
for line_no, line in enumerate(f, 1):
if needle not in line.lower():
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
yield fpath.name, line_no, record.get("type")
if __name__ == "__main__":
for name, line_no, rtype in search(sys.argv[1], sys.argv[2]):
print(f"{name}:{line_no} ({rtype})")