Last active
May 5, 2025 06:43
-
-
Save hrishioa/80e5d16884f085c98f91eb592f601e9f to your computer and use it in GitHub Desktop.
Run with `bun run`. Claude Research is really good - but the exported data doesn't have links formatted correctly (often completely missing). This script simply takes the full JSON object from Claude and gives you clean markdown reports with good links.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bun | |
import { existsSync, mkdirSync, writeFileSync } from "fs"; | |
import { resolve } from "path"; | |
// Define types based on the JSON structure | |
type Citation = { | |
url: string; | |
uuid: string; | |
title: string; | |
metadata: { | |
type: string; | |
uuid: string; | |
source: string; | |
icon_url: string; | |
preview_title: string; | |
}; | |
start_index: number; | |
end_index: number; | |
origin_tool_name: null | string; | |
}; | |
type Artifact = { | |
id: string; | |
type: string; | |
title: string; | |
source: string; | |
command: string; | |
content: string; | |
language: null | string; | |
md_citations: Citation[]; | |
}; | |
type ToolUse = { | |
type: string; | |
name: string; | |
input: Artifact; | |
message?: string; | |
}; | |
type Content = { | |
start_timestamp?: string; | |
stop_timestamp?: string; | |
type: string; | |
text?: string; | |
name?: string; | |
input?: any; | |
message?: string; | |
citations?: any[]; | |
}; | |
type Message = { | |
uuid: string; | |
text: string; | |
content: Content[]; | |
sender: string; | |
index: number; | |
created_at: string; | |
updated_at: string; | |
truncated: boolean; | |
stop_reason?: string; | |
attachments: any[]; | |
files: any[]; | |
files_v2: any[]; | |
sync_sources: any[]; | |
parent_message_uuid: string; | |
}; | |
type ChatData = { | |
uuid: string; | |
name: string; | |
summary: string; | |
created_at: string; | |
updated_at: string; | |
settings: { | |
enabled_web_search: boolean; | |
compass_mode: string; | |
paprika_mode: string; | |
preview_feature_uses_artifacts: boolean; | |
}; | |
is_starred: boolean; | |
current_leaf_message_uuid: string; | |
chat_messages: Message[]; | |
}; | |
/** | |
* Extract artifacts from the chat data | |
*/ | |
function extractArtifacts(chatData: ChatData): Artifact[] { | |
const artifacts: Artifact[] = []; | |
for (const message of chatData.chat_messages) { | |
for (const content of message.content) { | |
if ( | |
content.type === "tool_use" && | |
content.name === "artifacts" && | |
content.input | |
) { | |
artifacts.push(content.input as Artifact); | |
} | |
} | |
} | |
return artifacts; | |
} | |
/** | |
* Add citation links to the markdown content | |
*/ | |
function addCitationLinks(content: string, citations: Citation[]): string { | |
let processedContent = content; | |
// Group citations by end_index | |
const citationsByPosition = new Map<number, Citation[]>(); | |
for (const citation of citations) { | |
const endPosition = citation.end_index; | |
if (!citationsByPosition.has(endPosition)) { | |
citationsByPosition.set(endPosition, []); | |
} | |
citationsByPosition.get(endPosition)?.push(citation); | |
} | |
// Sort positions in descending order to avoid index shifting | |
const positions = Array.from(citationsByPosition.keys()).sort( | |
(a, b) => b - a | |
); | |
for (const position of positions) { | |
const citationsAtPosition = citationsByPosition.get(position) || []; | |
// Create comma-separated citation links | |
const citationLinks = citationsAtPosition | |
.map((citation) => `[${citation.metadata.source}](${citation.url})`) | |
.join(", "); | |
// Wrap links in square brackets | |
const formattedCitationLinks = ` [${citationLinks}]`; | |
// Insert at this position | |
if (position <= processedContent.length) { | |
processedContent = | |
processedContent.substring(0, position) + | |
formattedCitationLinks + | |
processedContent.substring(position); | |
} | |
} | |
return processedContent; | |
} | |
/** | |
* Save artifacts as markdown files | |
*/ | |
function saveArtifactsAsMarkdown( | |
artifacts: Artifact[], | |
outputDir: string | |
): void { | |
if (!existsSync(outputDir)) { | |
mkdirSync(outputDir, { recursive: true }); | |
} | |
for (const artifact of artifacts) { | |
if (artifact.type === "text/markdown") { | |
// Process content to add citation links | |
const processedContent = addCitationLinks( | |
artifact.content, | |
artifact.md_citations | |
); | |
// Generate a safe filename from the title | |
const safeTitle = artifact.title | |
.replace(/[^a-z0-9\s-]/gi, "") | |
.replace(/\s+/g, "-") | |
.toLowerCase(); | |
const filePath = resolve(outputDir, `${safeTitle}.md`); | |
// Save to file | |
writeFileSync(filePath, processedContent); | |
console.log(`Saved: ${filePath}`); | |
} | |
} | |
} | |
/** | |
* Main function to process the JSON file | |
*/ | |
async function main() { | |
// Check command line arguments | |
const args = process.argv.slice(2); | |
if (args.length < 2) { | |
console.error( | |
"Usage: bun run script.ts <input-json-file> <output-directory>" | |
); | |
process.exit(1); | |
} | |
const inputFile = args[0]; | |
const outputDir = args[1]; | |
try { | |
// Read and parse the JSON file | |
const jsonContent = await Bun.file(inputFile).text(); | |
const chatData = JSON.parse(jsonContent) as ChatData; | |
// Extract and save artifacts | |
const artifacts = extractArtifacts(chatData); | |
console.log(`Found ${artifacts.length} artifact(s) to process`); | |
saveArtifactsAsMarkdown(artifacts, outputDir); | |
console.log("Processing completed successfully!"); | |
} catch (error) { | |
console.error("Error processing the JSON file:", error); | |
process.exit(1); | |
} | |
} | |
// Run the script | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment