Skip to content

Instantly share code, notes, and snippets.

@fasiha
Last active July 19, 2026 21:12
Show Gist options
  • Select an option

  • Save fasiha/3a54f2e3f9ecec9620dfeceb862d4337 to your computer and use it in GitHub Desktop.

Select an option

Save fasiha/3a54f2e3f9ecec9620dfeceb862d4337 to your computer and use it in GitHub Desktop.
Given a UUID, find the Claude Code session and convert to a Markdown
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const readline = require('readline');
async function findJsonlFile(uuid) {
const projectsDir = path.expandUser('~/.claude/projects');
function searchDir(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
const result = searchDir(fullPath);
if (result) return result;
} else if (file === `${uuid}.jsonl`) {
return fullPath;
}
}
return null;
}
return searchDir(projectsDir);
}
// Mimic path.expandUser for compatibility
path.expandUser = (str) => {
return str.replace(/^~/, process.env.HOME);
};
function escapeMarkdownHeading(text) {
// Escape # at the start of lines
return text.replace(/^(#+\s)/gm, '\\$1');
}
async function parseJsonl(filePath) {
const records = [];
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
if (line.trim()) {
try {
records.push(JSON.parse(line));
} catch (e) {
console.error(`Failed to parse line: ${line.substring(0, 100)}`);
}
}
}
return records;
}
function parseClaudeWebJson(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const data = JSON.parse(content);
if (!data.chat_messages || !Array.isArray(data.chat_messages)) {
throw new Error('Invalid Claude.ai JSON: missing or invalid chat_messages array');
}
const records = [];
for (const message of data.chat_messages) {
if (message.sender === 'human') {
records.push({
type: 'user',
message: {
content: extractClaudeWebContent(message.content)
},
timestamp: message.created_at
});
} else if (message.sender === 'assistant') {
records.push({
type: 'assistant',
message: {
content: extractClaudeWebContent(message.content)
},
timestamp: message.created_at
});
}
}
// Add title from top-level name if available
const data_obj = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
if (data_obj.name) {
records.unshift({
type: 'ai-title',
aiTitle: data_obj.name,
timestamp: data_obj.created_at
});
}
return records;
}
function extractClaudeWebContent(contentArray) {
if (!Array.isArray(contentArray)) {
return '';
}
const blocks = contentArray.map(block => {
if (block.type === 'text') {
return block.text || '';
} else if (block.type === 'thinking') {
const thinking = block.thinking?.trim();
return thinking ? `<details>\n<summary>Thinking</summary>\n\n${thinking}\n\n</details>` : '';
} else if (block.type === 'tool_use') {
return `[Tool use: ${block.name}]`;
} else if (block.type === 'tool_result') {
if (Array.isArray(block.content)) {
return block.content
.map(c => (typeof c === 'string' ? c : c.title || JSON.stringify(c)))
.join('\n');
}
return '[Tool result]';
}
return '';
}).filter(Boolean);
return blocks.join('\n\n');
}
function getTitle(records) {
// Look for ai-title
const titleRecord = records.find(r => r.type === 'ai-title');
if (titleRecord) return titleRecord.aiTitle;
// Look for first user message
const userRecord = records.find(r => r.type === 'user' && r.message);
if (userRecord && userRecord.message.content) {
const content = userRecord.message.content;
const firstLine = content.split('\n')[0];
return firstLine.substring(0, 100);
}
return 'Claude Code Session';
}
function extractContent(message) {
if (typeof message.content === 'string') {
return message.content;
}
if (Array.isArray(message.content)) {
return message.content
.map(block => {
if (block.type === 'text') return block.text;
if (block.type === 'thinking') {
const thinking = block.thinking?.trim();
return thinking ? `<details>\n<summary>Thinking</summary>\n\n${thinking}\n\n</details>` : '';
}
return '';
})
.filter(Boolean)
.join('\n\n');
}
return '';
}
async function generateMarkdown(filePath, records) {
const title = getTitle(records);
let md = `# ${title}\n\n`;
let turnCount = 0;
for (const record of records) {
if (record.type === 'user' && record.message) {
const content = extractContent(record.message);
// Skip empty user messages
if (content.trim()) {
turnCount++;
const timestamp = new Date(record.timestamp).toLocaleString();
md += `## Turn ${turnCount}: User (${timestamp})\n\n`;
md += escapeMarkdownHeading(content) + '\n\n';
}
}
if (record.type === 'assistant' && record.message) {
const content = extractContent(record.message);
// Skip empty responses
if (content.trim()) {
const timestamp = new Date(record.timestamp).toLocaleString();
md += `## Assistant Response\n\n`;
md += escapeMarkdownHeading(content) + '\n\n';
}
}
if (record.type === 'ai-title' && record.aiTitle) {
md += `**Session Title:** ${record.aiTitle}\n\n`;
}
}
return md;
}
async function main() {
const input = process.argv[2];
if (!input || input === '--help' || input === '-h') {
console.log(`Usage: claude-session-to-md.js <file-path-or-uuid>
Arguments:
file-path Path to a JSON file (from Claude.ai) or JSONL file (from ~/.claude)
uuid UUID to search for in ~/.claude/projects
Examples:
claude-session-to-md.js /path/to/7d99661e-873c-411c-bd83-d4ca796e6c7e.json
claude-session-to-md.js ./session.jsonl
claude-session-to-md.js a1b2c3d4-e5f6-7890-1234-567890abcdef
`);
process.exit(input ? 0 : 1);
}
let filePath;
let records;
let outputName;
// Check if input is a file path
if (fs.existsSync(input)) {
console.log(`Loading file: ${input}`);
filePath = input;
// Determine if it's JSON or JSONL
const ext = path.extname(input).toLowerCase();
const isJsonFile = ext === '.json';
if (isJsonFile) {
console.log('Detected Claude.ai JSON format');
records = parseClaudeWebJson(filePath);
outputName = path.basename(input, '.json');
} else {
console.log('Detected JSONL format');
records = await parseJsonl(filePath);
outputName = path.basename(input, path.extname(input));
}
} else {
// Treat as UUID and search in ~/.claude
console.log(`Searching for JSONL file with UUID: ${input}`);
filePath = await findJsonlFile(input);
if (!filePath) {
console.error(`No file found for: ${input} (not a valid path or UUID)`);
process.exit(1);
}
console.log(`Found file: ${filePath}`);
records = await parseJsonl(filePath);
outputName = input;
}
console.log(`Parsed ${records.length} records`);
const markdown = await generateMarkdown(filePath, records);
const outputPath = path.join(process.cwd(), `${outputName}.md`);
fs.writeFileSync(outputPath, markdown);
console.log(`✓ Exported to: ${outputPath}`);
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment