Skip to content

Instantly share code, notes, and snippets.

@hrishioa
Last active May 5, 2025 06:43
Show Gist options
  • Save hrishioa/80e5d16884f085c98f91eb592f601e9f to your computer and use it in GitHub Desktop.
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.
#!/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