this repo has no description
at main 3.9 kB view raw
1#!/usr/bin/env bun 2// Interactive CLI tool for labeling exported emails 3 4import { readFileSync, writeFileSync } from "fs"; 5 6interface ExportedEmail { 7 thread_id: string; 8 subject: string; 9 from: string; 10 to?: string; 11 cc?: string; 12 date: string; 13 body: string; 14 labels?: string[]; 15 is_in_inbox?: boolean; 16} 17 18interface LabeledEmail extends ExportedEmail { 19 pertains: boolean; 20 reason: string; 21 labeled_at: string; 22} 23 24interface ExportData { 25 exported_at: string; 26 total_count: number; 27 label: string; 28 emails: ExportedEmail[]; 29} 30 31interface LabeledData { 32 exported_at: string; 33 labeled_at: string; 34 total_count: number; 35 label: string; 36 emails: LabeledEmail[]; 37} 38 39async function prompt(question: string): Promise<string> { 40 process.stdout.write(question); 41 for await (const line of console) { 42 return line.trim(); 43 } 44 return ""; 45} 46 47function truncate(text: string, maxLength: number = 300): string { 48 if (text.length <= maxLength) return text; 49 return text.slice(0, maxLength) + "..."; 50} 51 52async function labelEmail(email: ExportedEmail, index: number, total: number): Promise<LabeledEmail | null> { 53 console.log("\n" + "=".repeat(80)); 54 console.log(`Email ${index + 1} of ${total}`); 55 console.log("=".repeat(80)); 56 console.log(`From: ${email.from}`); 57 console.log(`Subject: ${email.subject}`); 58 console.log(`Date: ${email.date}`); 59 console.log("\nBody preview:"); 60 console.log(truncate(email.body, 500)); 61 console.log("\n" + "-".repeat(80)); 62 63 const pertainsInput = await prompt("Is this relevant? (y/n/s=skip/q=quit): "); 64 65 if (pertainsInput.toLowerCase() === "q") { 66 return null; // Signal to quit 67 } 68 69 if (pertainsInput.toLowerCase() === "s") { 70 console.log("Skipped"); 71 return null; // Signal to skip 72 } 73 74 const pertains = pertainsInput.toLowerCase() === "y"; 75 76 const reason = await prompt(pertains ? "Why is it relevant? " : "Why not relevant? "); 77 78 return { 79 ...email, 80 pertains, 81 reason: reason || (pertains ? "relevant" : "not relevant"), 82 labeled_at: new Date().toISOString(), 83 }; 84} 85 86async function main() { 87 const args = process.argv.slice(2); 88 89 if (args.length === 0) { 90 console.error("Usage: bun label.ts <exported-file.json> [output-file.json]"); 91 process.exit(1); 92 } 93 94 const inputFile = args[0]; 95 const outputFile = args[1] || inputFile.replace(".json", "-labeled.json"); 96 97 console.log(`Loading emails from ${inputFile}...`); 98 99 let data: ExportData; 100 try { 101 data = JSON.parse(readFileSync(inputFile, "utf-8")); 102 } catch (e) { 103 console.error(`Failed to read ${inputFile}:`, e); 104 process.exit(1); 105 } 106 107 console.log(`Loaded ${data.emails.length} emails to label`); 108 109 const labeled: LabeledEmail[] = []; 110 111 for (let i = 0; i < data.emails.length; i++) { 112 const result = await labelEmail(data.emails[i], i, data.emails.length); 113 114 if (result === null && i < data.emails.length - 1) { 115 const continueInput = await prompt("Continue labeling? (y/n): "); 116 if (continueInput.toLowerCase() !== "y") { 117 console.log("\nStopping. Saving labeled emails so far..."); 118 break; 119 } 120 continue; 121 } 122 123 if (result) { 124 labeled.push(result); 125 } 126 } 127 128 if (labeled.length === 0) { 129 console.log("\nNo emails labeled. Exiting without saving."); 130 return; 131 } 132 133 const output: LabeledData = { 134 exported_at: data.exported_at, 135 labeled_at: new Date().toISOString(), 136 total_count: labeled.length, 137 label: data.label, 138 emails: labeled, 139 }; 140 141 writeFileSync(outputFile, JSON.stringify(output, null, 2)); 142 143 console.log("\n" + "=".repeat(80)); 144 console.log(`✓ Labeled ${labeled.length} emails`); 145 console.log(`✓ Saved to ${outputFile}`); 146 console.log("\nNext steps:"); 147 console.log(` 1. Review labels in ${outputFile}`); 148 console.log(` 2. Run: bun import-labeled.ts ${outputFile}`); 149 console.log("=".repeat(80)); 150} 151 152main();