inboxctl 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/{chunk-OLL3OA5B.js → chunk-2PN3TSVQ.js} +361 -59
- package/dist/chunk-2PN3TSVQ.js.map +1 -0
- package/dist/cli.js +463 -7
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-OLL3OA5B.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
getLabelDistribution,
|
|
32
32
|
getMessage,
|
|
33
33
|
getNewsletters,
|
|
34
|
+
getNoiseSenders,
|
|
34
35
|
getOAuthReadiness,
|
|
35
36
|
getRecentEmails,
|
|
36
37
|
getRecentRuns,
|
|
@@ -39,7 +40,10 @@ import {
|
|
|
39
40
|
getSenderStats,
|
|
40
41
|
getSqlite,
|
|
41
42
|
getSyncStatus,
|
|
43
|
+
getThread,
|
|
42
44
|
getTopSenders,
|
|
45
|
+
getUncategorizedSenders,
|
|
46
|
+
getUnsubscribeSuggestions,
|
|
43
47
|
getVolumeByPeriod,
|
|
44
48
|
incrementalSync,
|
|
45
49
|
initializeDb,
|
|
@@ -53,7 +57,9 @@ import {
|
|
|
53
57
|
loadTokens,
|
|
54
58
|
markRead,
|
|
55
59
|
markUnread,
|
|
60
|
+
queryEmails,
|
|
56
61
|
reconcileCacheForAuthenticatedAccount,
|
|
62
|
+
reviewCategorized,
|
|
57
63
|
runAllRules,
|
|
58
64
|
runRule,
|
|
59
65
|
saveTokens,
|
|
@@ -61,8 +67,9 @@ import {
|
|
|
61
67
|
startMcpServer,
|
|
62
68
|
startOAuthFlow,
|
|
63
69
|
syncLabels,
|
|
64
|
-
undoRun
|
|
65
|
-
|
|
70
|
+
undoRun,
|
|
71
|
+
unsubscribe
|
|
72
|
+
} from "./chunk-2PN3TSVQ.js";
|
|
66
73
|
|
|
67
74
|
// src/cli.ts
|
|
68
75
|
import { Command } from "commander";
|
|
@@ -254,7 +261,7 @@ function getScreenGuide(screen, focus) {
|
|
|
254
261
|
case "email":
|
|
255
262
|
return "Esc back \u2022 j/k scroll \u2022 a archive \u2022 l label \u2022 r toggle read";
|
|
256
263
|
case "stats":
|
|
257
|
-
return "Esc back \u2022 s senders \u2022 l labels \u2022 n newsletters";
|
|
264
|
+
return "Esc back \u2022 s senders \u2022 l labels \u2022 n newsletters \u2022 o noise \u2022 c uncategorized \u2022 u unsubscribe";
|
|
258
265
|
case "rules":
|
|
259
266
|
return `Esc back \u2022 Tab switch ${focus === "history" ? "history" : "rules"} focus \u2022 d deploy \u2022 e toggle \u2022 r dry-run \u2022 R apply \u2022 u undo`;
|
|
260
267
|
case "search":
|
|
@@ -525,6 +532,9 @@ function App({ initialSync = true }) {
|
|
|
525
532
|
const [statsSenders, setStatsSenders] = useState([]);
|
|
526
533
|
const [statsLabels, setStatsLabels] = useState([]);
|
|
527
534
|
const [statsNewsletters, setStatsNewsletters] = useState([]);
|
|
535
|
+
const [statsNoise, setStatsNoise] = useState([]);
|
|
536
|
+
const [statsUncategorized, setStatsUncategorized] = useState(null);
|
|
537
|
+
const [statsUnsubscribe, setStatsUnsubscribe] = useState([]);
|
|
528
538
|
const [statsVolume, setStatsVolume] = useState([]);
|
|
529
539
|
const [rulesLoading, setRulesLoading] = useState(false);
|
|
530
540
|
const [rulesFocus, setRulesFocus] = useState("rules");
|
|
@@ -582,17 +592,23 @@ function App({ initialSync = true }) {
|
|
|
582
592
|
async function loadStats() {
|
|
583
593
|
setStatsLoading(true);
|
|
584
594
|
try {
|
|
585
|
-
const [overview, senders, labels2, newsletters, volume] = await Promise.all([
|
|
595
|
+
const [overview, senders, labels2, newsletters, noise, uncategorized, unsubscribe2, volume] = await Promise.all([
|
|
586
596
|
getInboxOverview(),
|
|
587
597
|
getTopSenders({ limit: 10 }),
|
|
588
598
|
getLabelDistribution(),
|
|
589
599
|
getNewsletters({ minMessages: 1 }),
|
|
600
|
+
getNoiseSenders({ limit: 10 }),
|
|
601
|
+
getUncategorizedSenders({ limit: 10 }),
|
|
602
|
+
getUnsubscribeSuggestions({ limit: 10 }),
|
|
590
603
|
getVolumeByPeriod("day", { start: Date.now() - 30 * 24 * 60 * 60 * 1e3, end: Date.now() })
|
|
591
604
|
]);
|
|
592
605
|
setStatsOverview(overview);
|
|
593
606
|
setStatsSenders(senders);
|
|
594
607
|
setStatsLabels(labels2.slice(0, 10));
|
|
595
608
|
setStatsNewsletters(newsletters.slice(0, 10));
|
|
609
|
+
setStatsNoise(noise.senders);
|
|
610
|
+
setStatsUncategorized(uncategorized);
|
|
611
|
+
setStatsUnsubscribe(unsubscribe2.suggestions);
|
|
596
612
|
setStatsVolume(volume.slice(-7));
|
|
597
613
|
} catch (error) {
|
|
598
614
|
pushFlash("error", error instanceof Error ? error.message : String(error));
|
|
@@ -1023,6 +1039,18 @@ function App({ initialSync = true }) {
|
|
|
1023
1039
|
}
|
|
1024
1040
|
if (input2 === "n") {
|
|
1025
1041
|
setStatsTab("newsletters");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (input2 === "o") {
|
|
1045
|
+
setStatsTab("noise");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (input2 === "c") {
|
|
1049
|
+
setStatsTab("uncategorized");
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (input2 === "u") {
|
|
1053
|
+
setStatsTab("unsubscribe");
|
|
1026
1054
|
}
|
|
1027
1055
|
return;
|
|
1028
1056
|
}
|
|
@@ -1261,7 +1289,7 @@ function App({ initialSync = true }) {
|
|
|
1261
1289
|
Panel,
|
|
1262
1290
|
{
|
|
1263
1291
|
title: "Stats Dashboard",
|
|
1264
|
-
subtitle: "s senders \u2022 l labels \u2022 n newsletters \u2022 Esc back",
|
|
1292
|
+
subtitle: "s senders \u2022 l labels \u2022 n newsletters \u2022 o noise \u2022 c uncategorized \u2022 u unsubscribe \u2022 Esc back",
|
|
1265
1293
|
accent: "yellow",
|
|
1266
1294
|
children: statsLoading ? /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
1267
1295
|
/* @__PURE__ */ jsx(Spinner, { type: "dots" }),
|
|
@@ -1307,7 +1335,13 @@ function App({ initialSync = true }) {
|
|
|
1307
1335
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
1308
1336
|
/* @__PURE__ */ jsx(Text, { color: statsTab === "labels" ? "cyan" : "gray", children: "[Labels]" }),
|
|
1309
1337
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
1310
|
-
/* @__PURE__ */ jsx(Text, { color: statsTab === "newsletters" ? "cyan" : "gray", children: "[Newsletters]" })
|
|
1338
|
+
/* @__PURE__ */ jsx(Text, { color: statsTab === "newsletters" ? "cyan" : "gray", children: "[Newsletters]" }),
|
|
1339
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
1340
|
+
/* @__PURE__ */ jsx(Text, { color: statsTab === "noise" ? "cyan" : "gray", children: "[Noise]" }),
|
|
1341
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
1342
|
+
/* @__PURE__ */ jsx(Text, { color: statsTab === "uncategorized" ? "cyan" : "gray", children: "[Uncategorized]" }),
|
|
1343
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
1344
|
+
/* @__PURE__ */ jsx(Text, { color: statsTab === "unsubscribe" ? "cyan" : "gray", children: "[Unsubscribe]" })
|
|
1311
1345
|
] }),
|
|
1312
1346
|
statsTab === "senders" ? /* @__PURE__ */ jsx(
|
|
1313
1347
|
Table,
|
|
@@ -1349,6 +1383,70 @@ function App({ initialSync = true }) {
|
|
|
1349
1383
|
emptyMessage: "No newsletters detected."
|
|
1350
1384
|
}
|
|
1351
1385
|
) : null,
|
|
1386
|
+
statsTab === "noise" ? /* @__PURE__ */ jsx(
|
|
1387
|
+
Table,
|
|
1388
|
+
{
|
|
1389
|
+
title: "Noise Senders",
|
|
1390
|
+
headers: ["SENDER", "EMAILS", "UNREAD%", "SCORE", "UNSUB"],
|
|
1391
|
+
rows: statsNoise.map((sender) => [
|
|
1392
|
+
truncate(sender.name || sender.email, 24),
|
|
1393
|
+
String(sender.messageCount),
|
|
1394
|
+
formatPercent(sender.unreadRate),
|
|
1395
|
+
String(sender.noiseScore),
|
|
1396
|
+
sender.hasUnsubscribeLink ? "Yes" : "No"
|
|
1397
|
+
]),
|
|
1398
|
+
emptyMessage: "No noisy senders detected."
|
|
1399
|
+
}
|
|
1400
|
+
) : null,
|
|
1401
|
+
statsTab === "uncategorized" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1402
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1403
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1404
|
+
"Uncategorized: ",
|
|
1405
|
+
statsUncategorized?.totalEmails ?? 0,
|
|
1406
|
+
" emails from ",
|
|
1407
|
+
statsUncategorized?.totalSenders ?? 0,
|
|
1408
|
+
" senders"
|
|
1409
|
+
] }),
|
|
1410
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
1411
|
+
"High ",
|
|
1412
|
+
statsUncategorized?.summary.byConfidence.high.senders ?? 0,
|
|
1413
|
+
" \u2022 Medium ",
|
|
1414
|
+
statsUncategorized?.summary.byConfidence.medium.senders ?? 0,
|
|
1415
|
+
" \u2022 Low ",
|
|
1416
|
+
statsUncategorized?.summary.byConfidence.low.senders ?? 0
|
|
1417
|
+
] })
|
|
1418
|
+
] }),
|
|
1419
|
+
/* @__PURE__ */ jsx(
|
|
1420
|
+
Table,
|
|
1421
|
+
{
|
|
1422
|
+
title: "Uncategorized Senders",
|
|
1423
|
+
headers: ["SENDER", "EMAILS", "UNREAD%", "CONF", "SIGNALS"],
|
|
1424
|
+
rows: (statsUncategorized?.senders || []).map((sender) => [
|
|
1425
|
+
truncate(sender.name || sender.sender, 20),
|
|
1426
|
+
String(sender.emailCount),
|
|
1427
|
+
formatPercent(sender.unreadRate),
|
|
1428
|
+
sender.confidence.toUpperCase(),
|
|
1429
|
+
truncate(sender.signals.join(","), 22)
|
|
1430
|
+
]),
|
|
1431
|
+
emptyMessage: "No uncategorized senders detected."
|
|
1432
|
+
}
|
|
1433
|
+
)
|
|
1434
|
+
] }) : null,
|
|
1435
|
+
statsTab === "unsubscribe" ? /* @__PURE__ */ jsx(
|
|
1436
|
+
Table,
|
|
1437
|
+
{
|
|
1438
|
+
title: "Unsubscribe Candidates",
|
|
1439
|
+
headers: ["SENDER", "EMAILS", "UNREAD%", "IMPACT", "METHOD"],
|
|
1440
|
+
rows: statsUnsubscribe.map((sender) => [
|
|
1441
|
+
truncate(sender.name || sender.email, 24),
|
|
1442
|
+
String(sender.allTimeMessageCount),
|
|
1443
|
+
formatPercent(sender.unreadRate),
|
|
1444
|
+
String(sender.impactScore),
|
|
1445
|
+
sender.unsubscribeMethod
|
|
1446
|
+
]),
|
|
1447
|
+
emptyMessage: "No unsubscribe candidates detected."
|
|
1448
|
+
}
|
|
1449
|
+
) : null,
|
|
1352
1450
|
/* @__PURE__ */ jsx(
|
|
1353
1451
|
Table,
|
|
1354
1452
|
{
|
|
@@ -3383,6 +3481,22 @@ function pad2(value, width) {
|
|
|
3383
3481
|
function printSection(title) {
|
|
3384
3482
|
console.log(ui.bold(title));
|
|
3385
3483
|
}
|
|
3484
|
+
function printSimpleTable(headers, rows) {
|
|
3485
|
+
const widths = headers.map(
|
|
3486
|
+
(header, index) => Math.max(
|
|
3487
|
+
header.length,
|
|
3488
|
+
...rows.map((row) => stripAnsi2(row[index] || "").length)
|
|
3489
|
+
)
|
|
3490
|
+
);
|
|
3491
|
+
console.log(
|
|
3492
|
+
headers.map((header, index) => pad2(ui.dim(header), widths[index] || header.length)).join(" ")
|
|
3493
|
+
);
|
|
3494
|
+
for (const row of rows) {
|
|
3495
|
+
console.log(
|
|
3496
|
+
row.map((cell, index) => pad2(cell, widths[index] || cell.length)).join(" ")
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3386
3500
|
function printKeyValue(label, value) {
|
|
3387
3501
|
console.log(`${ui.dim(`${label}:`)} ${value}`);
|
|
3388
3502
|
}
|
|
@@ -3559,6 +3673,13 @@ function formatPercent2(value) {
|
|
|
3559
3673
|
const normalized = Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, "");
|
|
3560
3674
|
return `${normalized}%`;
|
|
3561
3675
|
}
|
|
3676
|
+
function maybePrintJson(enabled, value) {
|
|
3677
|
+
if (!enabled) {
|
|
3678
|
+
return false;
|
|
3679
|
+
}
|
|
3680
|
+
console.log(JSON.stringify(value, null, 2));
|
|
3681
|
+
return true;
|
|
3682
|
+
}
|
|
3562
3683
|
function formatSenderIdentity(name, email, width) {
|
|
3563
3684
|
const identity = name && name !== email ? `${name} <${email}>` : email;
|
|
3564
3685
|
return pad2(truncate2(identity, width), width);
|
|
@@ -3697,6 +3818,149 @@ function printVolumeTable(label, points) {
|
|
|
3697
3818
|
);
|
|
3698
3819
|
}
|
|
3699
3820
|
}
|
|
3821
|
+
function formatConfidence(confidence) {
|
|
3822
|
+
const upper = confidence.toUpperCase();
|
|
3823
|
+
switch (confidence) {
|
|
3824
|
+
case "high":
|
|
3825
|
+
return ui.green(upper);
|
|
3826
|
+
case "medium":
|
|
3827
|
+
return ui.yellow(upper);
|
|
3828
|
+
case "low":
|
|
3829
|
+
return ui.red(upper);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
function formatSeverity(severity) {
|
|
3833
|
+
return severity === "high" ? ui.red(severity.toUpperCase()) : ui.yellow(severity.toUpperCase());
|
|
3834
|
+
}
|
|
3835
|
+
function formatYesNo(value) {
|
|
3836
|
+
return value ? ui.green("Yes") : ui.dim("No");
|
|
3837
|
+
}
|
|
3838
|
+
function formatImpactLevel(score) {
|
|
3839
|
+
if (score >= 25) {
|
|
3840
|
+
return ui.red("HIGH");
|
|
3841
|
+
}
|
|
3842
|
+
if (score >= 10) {
|
|
3843
|
+
return ui.yellow("MEDIUM");
|
|
3844
|
+
}
|
|
3845
|
+
return ui.green("LOW");
|
|
3846
|
+
}
|
|
3847
|
+
function formatGenericCell(value) {
|
|
3848
|
+
if (value === null || value === void 0) {
|
|
3849
|
+
return "-";
|
|
3850
|
+
}
|
|
3851
|
+
if (typeof value === "boolean") {
|
|
3852
|
+
return value ? "true" : "false";
|
|
3853
|
+
}
|
|
3854
|
+
if (typeof value === "number") {
|
|
3855
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, "");
|
|
3856
|
+
}
|
|
3857
|
+
return String(value);
|
|
3858
|
+
}
|
|
3859
|
+
function printNoiseSendersTable(result) {
|
|
3860
|
+
printSection(`Noise Senders (${result.senders.length})`);
|
|
3861
|
+
printSimpleTable(
|
|
3862
|
+
["SENDER", "EMAILS", "UNREAD", "SCORE", "UNSUB?"],
|
|
3863
|
+
result.senders.map((sender) => [
|
|
3864
|
+
truncate2(sender.name || sender.email, 34),
|
|
3865
|
+
String(sender.messageCount),
|
|
3866
|
+
formatPercent2(sender.unreadRate),
|
|
3867
|
+
String(sender.noiseScore),
|
|
3868
|
+
formatYesNo(sender.hasUnsubscribeLink)
|
|
3869
|
+
])
|
|
3870
|
+
);
|
|
3871
|
+
}
|
|
3872
|
+
function printUncategorizedSendersTable(result) {
|
|
3873
|
+
printSection(`Uncategorized: ${result.totalEmails} emails from ${result.totalSenders} senders`);
|
|
3874
|
+
console.log("");
|
|
3875
|
+
printSimpleTable(
|
|
3876
|
+
["CONFIDENCE", "SENDERS", "EMAILS"],
|
|
3877
|
+
[
|
|
3878
|
+
["HIGH", String(result.summary.byConfidence.high.senders), String(result.summary.byConfidence.high.emails)],
|
|
3879
|
+
["MEDIUM", String(result.summary.byConfidence.medium.senders), String(result.summary.byConfidence.medium.emails)],
|
|
3880
|
+
["LOW", String(result.summary.byConfidence.low.senders), String(result.summary.byConfidence.low.emails)]
|
|
3881
|
+
]
|
|
3882
|
+
);
|
|
3883
|
+
if (result.summary.topDomains.length > 0) {
|
|
3884
|
+
console.log("");
|
|
3885
|
+
printSection("Top Domains");
|
|
3886
|
+
printSimpleTable(
|
|
3887
|
+
["DOMAIN", "EMAILS", "SENDERS"],
|
|
3888
|
+
result.summary.topDomains.map((domain) => [
|
|
3889
|
+
domain.domain,
|
|
3890
|
+
String(domain.emails),
|
|
3891
|
+
String(domain.senders)
|
|
3892
|
+
])
|
|
3893
|
+
);
|
|
3894
|
+
}
|
|
3895
|
+
console.log("");
|
|
3896
|
+
printSection("Top Senders");
|
|
3897
|
+
printSimpleTable(
|
|
3898
|
+
["SENDER", "EMAILS", "UNREAD", "CONFIDENCE", "SIGNALS"],
|
|
3899
|
+
result.senders.map((sender) => [
|
|
3900
|
+
truncate2(sender.name && sender.name !== sender.sender ? `${sender.name} <${sender.sender}>` : sender.sender, 36),
|
|
3901
|
+
String(sender.emailCount),
|
|
3902
|
+
formatPercent2(sender.unreadRate),
|
|
3903
|
+
formatConfidence(sender.confidence),
|
|
3904
|
+
truncate2(sender.signals.join(", "), 40)
|
|
3905
|
+
])
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
function printUnsubscribeSuggestionsTable(result) {
|
|
3909
|
+
printSection(`Unsubscribe Suggestions (${result.suggestions.length} candidates)`);
|
|
3910
|
+
printSimpleTable(
|
|
3911
|
+
["SENDER", "EMAILS", "UNREAD", "IMPACT", "METHOD"],
|
|
3912
|
+
result.suggestions.map((sender) => [
|
|
3913
|
+
truncate2(sender.name || sender.email, 34),
|
|
3914
|
+
String(sender.allTimeMessageCount),
|
|
3915
|
+
formatPercent2(sender.unreadRate),
|
|
3916
|
+
formatImpactLevel(sender.impactScore),
|
|
3917
|
+
sender.unsubscribeMethod
|
|
3918
|
+
])
|
|
3919
|
+
);
|
|
3920
|
+
}
|
|
3921
|
+
function printAnomaliesTable(result) {
|
|
3922
|
+
printSection(result.summary);
|
|
3923
|
+
if (result.anomalies.length === 0) {
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
console.log("");
|
|
3927
|
+
printSimpleTable(
|
|
3928
|
+
["SEVERITY", "SENDER", "SUBJECT", "LABEL", "RULE"],
|
|
3929
|
+
result.anomalies.map((anomaly) => [
|
|
3930
|
+
formatSeverity(anomaly.severity),
|
|
3931
|
+
truncate2(anomaly.from, 24),
|
|
3932
|
+
truncate2(anomaly.subject || "(no subject)", 32),
|
|
3933
|
+
truncate2(anomaly.assignedLabel, 14),
|
|
3934
|
+
anomaly.rule
|
|
3935
|
+
])
|
|
3936
|
+
);
|
|
3937
|
+
}
|
|
3938
|
+
function printQueryResult(result) {
|
|
3939
|
+
printSection(`Query Results (${result.rows.length} of ${result.totalRows})`);
|
|
3940
|
+
if (result.rows.length === 0) {
|
|
3941
|
+
console.log("No rows matched that query.");
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
const headers = Object.keys(result.rows[0] || {});
|
|
3945
|
+
printSimpleTable(
|
|
3946
|
+
headers.map((header) => header.toUpperCase()),
|
|
3947
|
+
result.rows.map((row) => headers.map((header) => formatGenericCell(row[header])))
|
|
3948
|
+
);
|
|
3949
|
+
}
|
|
3950
|
+
function printThreadResult(result) {
|
|
3951
|
+
printSection(`Thread ${ui.dim(result.id)}`);
|
|
3952
|
+
printKeyValue("messages", String(result.messages.length));
|
|
3953
|
+
for (const message of result.messages) {
|
|
3954
|
+
console.log("");
|
|
3955
|
+
printSection(message.subject || "(no subject)");
|
|
3956
|
+
printKeyValue("from", message.fromAddress);
|
|
3957
|
+
printKeyValue("to", message.toAddresses.join(", "));
|
|
3958
|
+
printKeyValue("date", new Date(message.date).toISOString());
|
|
3959
|
+
printKeyValue("labels", message.labelIds.join(", ") || "-");
|
|
3960
|
+
console.log("");
|
|
3961
|
+
console.log(message.textPlain || message.body || message.snippet);
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3700
3964
|
function printSenderDetail(detail) {
|
|
3701
3965
|
printSection(detail.query);
|
|
3702
3966
|
printKeyValue("type", detail.type);
|
|
@@ -4027,7 +4291,7 @@ function printDriftReport(result) {
|
|
|
4027
4291
|
);
|
|
4028
4292
|
}
|
|
4029
4293
|
}
|
|
4030
|
-
program.name("inboxctl").description("CLI email management with MCP server, rules-as-code, and TUI").version("0.
|
|
4294
|
+
program.name("inboxctl").description("CLI email management with MCP server, rules-as-code, and TUI").version("0.4.0").option("--demo", "Launch the seeded demo mailbox").option("--no-sync", "Launch the TUI without running the initial background sync");
|
|
4031
4295
|
program.command("setup").description("Run the interactive Google Cloud and OAuth setup wizard").option("--skip-gcloud", "Skip gcloud detection and API enablement").option("--project <id>", "Pre-set the Google Cloud project ID").action(async (options) => {
|
|
4032
4296
|
try {
|
|
4033
4297
|
await runSetupWizard({
|
|
@@ -4141,6 +4405,20 @@ program.command("email <id>").description("View a single email").action(async (i
|
|
|
4141
4405
|
console.log("");
|
|
4142
4406
|
console.log(email.textPlain || email.body || email.snippet);
|
|
4143
4407
|
});
|
|
4408
|
+
program.command("thread <id>").description("View a full email thread").action(async (id) => {
|
|
4409
|
+
const status = await loadRuntimeStatus();
|
|
4410
|
+
if (!status.gmailReady) {
|
|
4411
|
+
await requireLiveGmailReadiness("thread");
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
try {
|
|
4415
|
+
const thread = await getThread(id);
|
|
4416
|
+
printThreadResult(thread);
|
|
4417
|
+
} catch (error) {
|
|
4418
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4419
|
+
process.exitCode = 1;
|
|
4420
|
+
}
|
|
4421
|
+
});
|
|
4144
4422
|
program.command("archive [id]").description("Archive email(s)").option("-q, --query <query>", "Archive all emails matching query").action(async (id, options) => {
|
|
4145
4423
|
const status = await loadRuntimeStatus();
|
|
4146
4424
|
if (!status.gmailReady) {
|
|
@@ -4270,6 +4548,100 @@ program.command("history").description("Show recent run history").option("-n, --
|
|
|
4270
4548
|
}
|
|
4271
4549
|
printHistoryTable(runs);
|
|
4272
4550
|
});
|
|
4551
|
+
program.command("query").description("Run structured analytics queries over the cached inbox").option("--group-by <dimension>", "Group by sender|domain|label|year_month|year_week|day_of_week|is_read|is_newsletter").option("--aggregate <values...>", "Aggregates to return (count, unread_count, unread_rate, newest, oldest, sender_count)").option("--from <address>", "Exact sender email").option("--from-contains <text>", "Partial sender match").option("--domain <domain>", "Exact sender domain").option("--domain-contains <text>", "Partial sender domain match").option("--subject-contains <text>", "Partial subject match").option("--since <date>", "Filter to emails on or after this ISO date").option("--before <date>", "Filter to emails on or before this ISO date").option("--read", "Only read emails").option("--unread", "Only unread emails").option("--newsletter", "Only newsletter senders").option("--without-newsletter", "Exclude newsletter senders").option("--label <name>", "Only emails with a specific label").option("--has-label", "Only emails with any user label").option("--without-label", "Only emails with no user labels").option("--has-unsubscribe", "Only emails with a List-Unsubscribe header").option("--min-sender-messages <number>", "Only senders with at least this many total emails").option("--having-count-gte <number>", "Require grouped count to be >= this value").option("--having-count-lte <number>", "Require grouped count to be <= this value").option("--having-unread-rate-gte <number>", "Require grouped unread_rate to be >= this value").option("--sort <value>", 'Sort expression, for example: "count desc"').option("--limit <number>", "Maximum rows to return", "50").option("--json", "Output raw JSON").action(async (options) => {
|
|
4552
|
+
try {
|
|
4553
|
+
if (options.read && options.unread) {
|
|
4554
|
+
throw new Error("Use only one of --read or --unread.");
|
|
4555
|
+
}
|
|
4556
|
+
if (options.newsletter && options.withoutNewsletter) {
|
|
4557
|
+
throw new Error("Use only one of --newsletter or --without-newsletter.");
|
|
4558
|
+
}
|
|
4559
|
+
if (options.hasLabel && options.withoutLabel) {
|
|
4560
|
+
throw new Error("Use only one of --has-label or --without-label.");
|
|
4561
|
+
}
|
|
4562
|
+
const result = await queryEmails({
|
|
4563
|
+
filters: {
|
|
4564
|
+
...options.from ? { from: options.from } : {},
|
|
4565
|
+
...options.fromContains ? { from_contains: options.fromContains } : {},
|
|
4566
|
+
...options.domain ? { domain: options.domain } : {},
|
|
4567
|
+
...options.domainContains ? { domain_contains: options.domainContains } : {},
|
|
4568
|
+
...options.subjectContains ? { subject_contains: options.subjectContains } : {},
|
|
4569
|
+
...options.since ? { date_after: options.since } : {},
|
|
4570
|
+
...options.before ? { date_before: options.before } : {},
|
|
4571
|
+
...options.read ? { is_read: true } : {},
|
|
4572
|
+
...options.unread ? { is_read: false } : {},
|
|
4573
|
+
...options.newsletter ? { is_newsletter: true } : {},
|
|
4574
|
+
...options.withoutNewsletter ? { is_newsletter: false } : {},
|
|
4575
|
+
...options.label ? { label: options.label } : {},
|
|
4576
|
+
...options.hasLabel ? { has_label: true } : {},
|
|
4577
|
+
...options.withoutLabel ? { has_label: false } : {},
|
|
4578
|
+
...options.hasUnsubscribe ? { has_unsubscribe: true } : {},
|
|
4579
|
+
...options.minSenderMessages ? { min_sender_messages: parseIntegerOption(options.minSenderMessages, "min-sender-messages") } : {}
|
|
4580
|
+
},
|
|
4581
|
+
...options.groupBy ? { group_by: options.groupBy } : {},
|
|
4582
|
+
...options.aggregate ? { aggregates: options.aggregate } : {},
|
|
4583
|
+
...options.sort ? { order_by: options.sort } : {},
|
|
4584
|
+
...options.havingCountGte || options.havingCountLte || options.havingUnreadRateGte ? {
|
|
4585
|
+
having: {
|
|
4586
|
+
...options.havingCountGte || options.havingCountLte ? {
|
|
4587
|
+
count: {
|
|
4588
|
+
...options.havingCountGte ? { gte: parseIntegerOption(options.havingCountGte, "having-count-gte") } : {},
|
|
4589
|
+
...options.havingCountLte ? { lte: parseIntegerOption(options.havingCountLte, "having-count-lte") } : {}
|
|
4590
|
+
}
|
|
4591
|
+
} : {},
|
|
4592
|
+
...options.havingUnreadRateGte ? {
|
|
4593
|
+
unread_rate: {
|
|
4594
|
+
gte: parsePercentOption(options.havingUnreadRateGte, "having-unread-rate-gte")
|
|
4595
|
+
}
|
|
4596
|
+
} : {}
|
|
4597
|
+
}
|
|
4598
|
+
} : {},
|
|
4599
|
+
limit: parseIntegerOption(options.limit, "limit")
|
|
4600
|
+
});
|
|
4601
|
+
if (maybePrintJson(options.json, result)) {
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
printQueryResult(result);
|
|
4605
|
+
} catch (error) {
|
|
4606
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4607
|
+
process.exitCode = 1;
|
|
4608
|
+
}
|
|
4609
|
+
});
|
|
4610
|
+
program.command("unsubscribe <sender>").description("Return an unsubscribe target for a sender and optionally archive or label existing mail").option("--no-archive", "Only return the unsubscribe link, do not archive existing mail").option("--label <name>", "Label existing emails while unsubscribing").action(async (sender, options) => {
|
|
4611
|
+
if (options.archive || options.label) {
|
|
4612
|
+
const status = await loadRuntimeStatus();
|
|
4613
|
+
if (!status.gmailReady) {
|
|
4614
|
+
await requireLiveGmailReadiness("unsubscribe");
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
try {
|
|
4619
|
+
const result = await unsubscribe({
|
|
4620
|
+
senderEmail: sender,
|
|
4621
|
+
alsoArchive: options.archive,
|
|
4622
|
+
alsoLabel: options.label
|
|
4623
|
+
});
|
|
4624
|
+
printSection(`Unsubscribing from ${result.sender}`);
|
|
4625
|
+
printKeyValue("messages", String(result.messageCount));
|
|
4626
|
+
printKeyValue("archived", String(result.archivedCount));
|
|
4627
|
+
printKeyValue("labeled", String(result.labeledCount));
|
|
4628
|
+
if (result.runId) {
|
|
4629
|
+
printKeyValue("runId", `${ui.dim(result.runId)} (undo with: inboxctl undo ${result.runId})`);
|
|
4630
|
+
}
|
|
4631
|
+
printKeyValue("method", result.unsubscribeMethod);
|
|
4632
|
+
console.log("");
|
|
4633
|
+
console.log(result.instruction);
|
|
4634
|
+
console.log("");
|
|
4635
|
+
console.log(result.unsubscribeLink);
|
|
4636
|
+
if (options.archive) {
|
|
4637
|
+
console.log("");
|
|
4638
|
+
console.log(`Tip: Create a Gmail filter to auto-archive future mail: inboxctl filters create --from ${result.sender} --archive`);
|
|
4639
|
+
}
|
|
4640
|
+
} catch (error) {
|
|
4641
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4642
|
+
process.exitCode = 1;
|
|
4643
|
+
}
|
|
4644
|
+
});
|
|
4273
4645
|
var labels = program.command("labels").description("Manage Gmail labels");
|
|
4274
4646
|
labels.command("list").description("List all Gmail labels").action(async () => {
|
|
4275
4647
|
const status = await loadRuntimeStatus();
|
|
@@ -4348,6 +4720,90 @@ stats.command("senders").description("Top senders by volume").option("--top <num
|
|
|
4348
4720
|
process.exitCode = 1;
|
|
4349
4721
|
}
|
|
4350
4722
|
});
|
|
4723
|
+
stats.command("noise").description("High-noise senders ranked by noise score").option("--top <number>", "Number of senders", "20").option("--sort <mode>", "Sort by noise_score|all_time_noise_score|message_count|unread_rate", "noise_score").option("--min-score <number>", "Minimum noise score", "5").option("--active-days <number>", "Only consider recent activity within this many days", "90").option("--json", "Output raw JSON").action(async (options) => {
|
|
4724
|
+
try {
|
|
4725
|
+
const result = await getNoiseSenders({
|
|
4726
|
+
limit: parseIntegerOption(options.top, "top"),
|
|
4727
|
+
sortBy: options.sort,
|
|
4728
|
+
minNoiseScore: Number(options.minScore),
|
|
4729
|
+
activeDays: parseIntegerOption(options.activeDays, "active-days")
|
|
4730
|
+
});
|
|
4731
|
+
if (maybePrintJson(options.json, result)) {
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
if (result.senders.length === 0) {
|
|
4735
|
+
console.log("No noisy senders matched that filter.");
|
|
4736
|
+
return;
|
|
4737
|
+
}
|
|
4738
|
+
printNoiseSendersTable(result);
|
|
4739
|
+
} catch (error) {
|
|
4740
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4741
|
+
process.exitCode = 1;
|
|
4742
|
+
}
|
|
4743
|
+
});
|
|
4744
|
+
stats.command("uncategorized").description("Summarize uncategorized emails by sender").option("--top <number>", "Number of senders", "20").option("--confidence <level>", "Filter by confidence: high|medium|low").option("--min-emails <number>", "Minimum uncategorized emails per sender", "1").option("--since <date>", "Only include uncategorized emails on or after this ISO date").option("--sort <mode>", "Sort by email_count|newest|unread_rate", "email_count").option("--json", "Output raw JSON").action(async (options) => {
|
|
4745
|
+
try {
|
|
4746
|
+
const result = await getUncategorizedSenders({
|
|
4747
|
+
limit: parseIntegerOption(options.top, "top"),
|
|
4748
|
+
confidence: options.confidence,
|
|
4749
|
+
minEmails: parseIntegerOption(options.minEmails, "min-emails"),
|
|
4750
|
+
since: options.since,
|
|
4751
|
+
sortBy: options.sort
|
|
4752
|
+
});
|
|
4753
|
+
if (maybePrintJson(options.json, result)) {
|
|
4754
|
+
return;
|
|
4755
|
+
}
|
|
4756
|
+
if (result.totalSenders === 0) {
|
|
4757
|
+
console.log("No uncategorized senders matched that filter.");
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
printUncategorizedSendersTable(result);
|
|
4761
|
+
} catch (error) {
|
|
4762
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4763
|
+
process.exitCode = 1;
|
|
4764
|
+
}
|
|
4765
|
+
});
|
|
4766
|
+
stats.command("unsubscribe").description("Rank unsubscribe candidates by impact").option("--top <number>", "Number of senders", "20").option("--min-emails <number>", "Minimum emails from a sender", "5").option("--unread-only-senders", "Only show senders where every email is unread").option("--json", "Output raw JSON").action(async (options) => {
|
|
4767
|
+
try {
|
|
4768
|
+
const result = await getUnsubscribeSuggestions({
|
|
4769
|
+
limit: parseIntegerOption(options.top, "top"),
|
|
4770
|
+
minMessages: parseIntegerOption(options.minEmails, "min-emails"),
|
|
4771
|
+
unreadOnlySenders: options.unreadOnlySenders
|
|
4772
|
+
});
|
|
4773
|
+
if (maybePrintJson(options.json, result)) {
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
if (result.suggestions.length === 0) {
|
|
4777
|
+
console.log("No unsubscribe suggestions matched that filter.");
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
printUnsubscribeSuggestionsTable(result);
|
|
4781
|
+
console.log("");
|
|
4782
|
+
console.log("Run `inboxctl unsubscribe <sender>` to process a specific sender.");
|
|
4783
|
+
} catch (error) {
|
|
4784
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4785
|
+
process.exitCode = 1;
|
|
4786
|
+
}
|
|
4787
|
+
});
|
|
4788
|
+
stats.command("anomalies").description("Review recently categorized emails for potential misclassifications").option("--since <date>", "Only review items on or after this ISO date").option("--limit <number>", "Maximum anomalies to return", "20").option("--json", "Output raw JSON").action(async (options) => {
|
|
4789
|
+
try {
|
|
4790
|
+
const result = await reviewCategorized({
|
|
4791
|
+
...options.since ? { since: options.since } : {},
|
|
4792
|
+
limit: parseIntegerOption(options.limit, "limit")
|
|
4793
|
+
});
|
|
4794
|
+
if (maybePrintJson(options.json, result)) {
|
|
4795
|
+
return;
|
|
4796
|
+
}
|
|
4797
|
+
printAnomaliesTable(result);
|
|
4798
|
+
if (result.anomalies.length > 0) {
|
|
4799
|
+
console.log("");
|
|
4800
|
+
console.log("Undo a run with `inboxctl undo <run-id>`.");
|
|
4801
|
+
}
|
|
4802
|
+
} catch (error) {
|
|
4803
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4804
|
+
process.exitCode = 1;
|
|
4805
|
+
}
|
|
4806
|
+
});
|
|
4351
4807
|
stats.command("newsletters").description("Detected newsletters and mailing lists").option("--min-unread <percent>", "Minimum unread rate filter").action(async (options) => {
|
|
4352
4808
|
try {
|
|
4353
4809
|
const minUnread = parsePercentOption(options.minUnread, "min-unread");
|