Skip to content

Commit fa9be47

Browse files
committed
feat: add --json, --to, --subject filters to draft list command
Implements GitHub issue #12 by adding missing filtering and output capabilities to `superhuman draft list`: - --json: outputs draft array as JSON for scripting/automation - --to <email>: filters drafts by recipient (case-insensitive substring) - --subject <text>: filters drafts by subject (case-insensitive substring) Also expands test coverage in cli-draft-list.test.ts with tests for each new filter and the JSON output format. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 121b300 commit fa9be47

3 files changed

Lines changed: 197 additions & 2 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ superhuman draft create --to "john" --subject "Hello" --body "Hi there!"
117117
# List drafts (shows both provider and native Superhuman drafts)
118118
superhuman draft list
119119
superhuman draft list --account user@example.com
120+
superhuman draft list --to jon@example.com # Filter by recipient
121+
superhuman draft list --subject "Meeting notes" # Filter by subject
122+
superhuman draft list --json # JSON output for scripting
120123

121124
# Open compose window (keeps it open for editing)
122125
superhuman compose --to user@example.com --subject "Meeting"

src/__tests__/cli-draft-list.test.ts

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/**
22
* CLI Draft List Tests
33
*
4-
* Tests the `superhuman draft list` command output with source column.
4+
* Tests the `superhuman draft list` command output with source column,
5+
* JSON output, and --to / --subject filtering.
56
*/
67

78
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
@@ -110,4 +111,174 @@ describe("superhuman draft list", () => {
110111
expect(exitCode).toBe(0);
111112
expect(stdout).toContain("draft");
112113
});
114+
115+
it("should filter drafts by --to recipient", () => {
116+
const drafts: Draft[] = [
117+
{
118+
id: "draft-1",
119+
subject: "Meeting Follow-up",
120+
from: "user@example.com",
121+
to: ["jon@example.com"],
122+
preview: "Hi Jon...",
123+
timestamp: "2026-02-08T14:30:00Z",
124+
source: "gmail",
125+
},
126+
{
127+
id: "draft-2",
128+
subject: "Project Update",
129+
from: "user@example.com",
130+
to: ["alice@example.com"],
131+
preview: "Hi Alice...",
132+
timestamp: "2026-02-08T15:00:00Z",
133+
source: "gmail",
134+
},
135+
];
136+
137+
const filterTo = "jon@example.com".toLowerCase();
138+
const filtered = drafts.filter((d) =>
139+
d.to.some((recipient) => recipient.toLowerCase().includes(filterTo))
140+
);
141+
142+
expect(filtered).toHaveLength(1);
143+
expect(filtered[0].id).toBe("draft-1");
144+
});
145+
146+
it("should filter drafts by --subject substring", () => {
147+
const drafts: Draft[] = [
148+
{
149+
id: "draft-1",
150+
subject: "Meeting Follow-up",
151+
from: "user@example.com",
152+
to: ["jon@example.com"],
153+
preview: "Hi Jon...",
154+
timestamp: "2026-02-08T14:30:00Z",
155+
source: "gmail",
156+
},
157+
{
158+
id: "draft-2",
159+
subject: "Project Update",
160+
from: "user@example.com",
161+
to: ["alice@example.com"],
162+
preview: "Hi Alice...",
163+
timestamp: "2026-02-08T15:00:00Z",
164+
source: "gmail",
165+
},
166+
];
167+
168+
const filterSubject = "meeting".toLowerCase();
169+
const filtered = drafts.filter((d) =>
170+
(d.subject || "").toLowerCase().includes(filterSubject)
171+
);
172+
173+
expect(filtered).toHaveLength(1);
174+
expect(filtered[0].id).toBe("draft-1");
175+
expect(filtered[0].subject).toBe("Meeting Follow-up");
176+
});
177+
178+
it("should return empty array when no drafts match --to filter", () => {
179+
const drafts: Draft[] = [
180+
{
181+
id: "draft-1",
182+
subject: "Test",
183+
from: "user@example.com",
184+
to: ["jon@example.com"],
185+
preview: "Preview",
186+
timestamp: "2026-02-08T14:30:00Z",
187+
source: "gmail",
188+
},
189+
];
190+
191+
const filterTo = "nobody@example.com".toLowerCase();
192+
const filtered = drafts.filter((d) =>
193+
d.to.some((recipient) => recipient.toLowerCase().includes(filterTo))
194+
);
195+
196+
expect(filtered).toHaveLength(0);
197+
});
198+
199+
it("should return all drafts when no filter is applied", () => {
200+
const drafts: Draft[] = [
201+
{
202+
id: "draft-1",
203+
subject: "First",
204+
from: "user@example.com",
205+
to: ["a@example.com"],
206+
preview: "Preview 1",
207+
timestamp: "2026-02-08T14:30:00Z",
208+
source: "gmail",
209+
},
210+
{
211+
id: "draft-2",
212+
subject: "Second",
213+
from: "user@example.com",
214+
to: ["b@example.com"],
215+
preview: "Preview 2",
216+
timestamp: "2026-02-08T15:00:00Z",
217+
source: "outlook",
218+
},
219+
];
220+
221+
// No filters applied
222+
const filterTo = "";
223+
const filterSubject = "";
224+
let filtered = drafts;
225+
if (filterTo) {
226+
filtered = filtered.filter((d) =>
227+
d.to.some((recipient) => recipient.toLowerCase().includes(filterTo))
228+
);
229+
}
230+
if (filterSubject) {
231+
filtered = filtered.filter((d) =>
232+
(d.subject || "").toLowerCase().includes(filterSubject)
233+
);
234+
}
235+
236+
expect(filtered).toHaveLength(2);
237+
});
238+
239+
it("should produce valid JSON output for drafts", () => {
240+
const drafts: Draft[] = [
241+
{
242+
id: "draft-abc123",
243+
subject: "Meeting Follow-up",
244+
from: "user@example.com",
245+
to: ["jon@example.com"],
246+
preview: "Hi Jon, following up on our conversation...",
247+
timestamp: "2026-02-08T14:30:00Z",
248+
source: "gmail",
249+
},
250+
];
251+
252+
const jsonStr = JSON.stringify(drafts, null, 2);
253+
const parsed = JSON.parse(jsonStr) as typeof drafts;
254+
255+
expect(Array.isArray(parsed)).toBe(true);
256+
expect(parsed).toHaveLength(1);
257+
expect(parsed[0].id).toBe("draft-abc123");
258+
expect(parsed[0].subject).toBe("Meeting Follow-up");
259+
expect(parsed[0].to).toEqual(["jon@example.com"]);
260+
expect(parsed[0].source).toBe("gmail");
261+
});
262+
263+
it("should match --to filter case-insensitively", () => {
264+
const drafts: Draft[] = [
265+
{
266+
id: "draft-1",
267+
subject: "Hello",
268+
from: "user@example.com",
269+
to: ["Jon@Example.COM"],
270+
preview: "Hi",
271+
timestamp: "2026-02-08T14:30:00Z",
272+
source: "gmail",
273+
},
274+
];
275+
276+
// Filter using lowercase
277+
const filterTo = "jon@example.com";
278+
const filtered = drafts.filter((d) =>
279+
d.to.some((recipient) => recipient.toLowerCase().includes(filterTo))
280+
);
281+
282+
expect(filtered).toHaveLength(1);
283+
});
113284
});

src/cli.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,8 @@ async function cmdListDrafts(options: CliOptions) {
13381338
const account = options.account;
13391339
const limit = options.limit || 50;
13401340
const offset = options.offset || 0;
1341+
const filterTo = options.to.length > 0 ? options.to[0].toLowerCase() : "";
1342+
const filterSubject = options.subject ? options.subject.toLowerCase() : "";
13411343

13421344
// Load cached tokens from disk
13431345
await loadTokensFromDisk();
@@ -1374,7 +1376,26 @@ async function cmdListDrafts(options: CliOptions) {
13741376

13751377
// Use DraftService to fetch drafts from all providers
13761378
const service = new DraftService([emailProvider, nativeProvider]);
1377-
const drafts = await service.listDrafts(limit, offset);
1379+
let drafts = await service.listDrafts(limit, offset);
1380+
1381+
// Apply --to filter
1382+
if (filterTo) {
1383+
drafts = drafts.filter((d) =>
1384+
d.to.some((recipient) => recipient.toLowerCase().includes(filterTo))
1385+
);
1386+
}
1387+
1388+
// Apply --subject filter
1389+
if (filterSubject) {
1390+
drafts = drafts.filter((d) =>
1391+
(d.subject || "").toLowerCase().includes(filterSubject)
1392+
);
1393+
}
1394+
1395+
if (options.json) {
1396+
log(JSON.stringify(drafts, null, 2));
1397+
return;
1398+
}
13781399

13791400
if (drafts.length === 0) {
13801401
log(`${colors.dim}No drafts found in ${email}.${colors.reset}`);

0 commit comments

Comments
 (0)