this repo has no description
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();