inboxd 1.0.7 → 1.0.9
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/.claude/skills/inbox-assistant/SKILL.md +421 -97
- package/CLAUDE.md +42 -4
- package/README.md +40 -8
- package/package.json +5 -3
- package/scripts/postinstall.js +79 -0
- package/src/cli.js +464 -46
- package/src/gmail-monitor.js +109 -0
- package/src/skill-installer.js +254 -0
- package/tests/filter.test.js +200 -0
- package/tests/group-by-sender.test.js +141 -0
package/src/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
|
-
const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails } = require('./gmail-monitor');
|
|
4
|
+
const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, archiveEmails, groupEmailsBySender } = require('./gmail-monitor');
|
|
5
5
|
const { getState, updateLastCheck, markEmailsSeen, getNewEmailIds, clearOldSeenEmails } = require('./state');
|
|
6
6
|
const { notifyNewEmails } = require('./notifier');
|
|
7
7
|
const { authorize, addAccount, getAccounts, getAccountEmail, removeAccount, removeAllAccounts, renameTokenFile, validateCredentialsFile, hasCredentials, isConfigured, installCredentials } = require('./gmail-auth');
|
|
8
8
|
const { logDeletions, getRecentDeletions, getLogPath, readLog, removeLogEntries } = require('./deletion-log');
|
|
9
|
+
const { getSkillStatus, checkForUpdate, installSkill, SKILL_DEST_DIR, SOURCE_MARKER } = require('./skill-installer');
|
|
9
10
|
const readline = require('readline');
|
|
10
11
|
const path = require('path');
|
|
11
12
|
const os = require('os');
|
|
@@ -33,10 +34,42 @@ function resolvePath(filePath) {
|
|
|
33
34
|
return path.resolve(filePath);
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Parses a duration string like "7d", "24h", "3d" and returns a Date
|
|
39
|
+
* representing that time in the past from now
|
|
40
|
+
* @param {string} duration - Duration string (e.g., "7d", "24h", "1d")
|
|
41
|
+
* @returns {Date|null} Date object or null if invalid format
|
|
42
|
+
*/
|
|
43
|
+
function parseSinceDuration(duration) {
|
|
44
|
+
const match = duration.match(/^(\d+)([dhm])$/i);
|
|
45
|
+
if (!match) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const value = parseInt(match[1], 10);
|
|
50
|
+
const unit = match[2].toLowerCase();
|
|
51
|
+
const now = new Date();
|
|
52
|
+
|
|
53
|
+
switch (unit) {
|
|
54
|
+
case 'd': // days
|
|
55
|
+
return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
|
|
56
|
+
case 'h': // hours
|
|
57
|
+
return new Date(now.getTime() - value * 60 * 60 * 1000);
|
|
58
|
+
case 'm': // minutes
|
|
59
|
+
return new Date(now.getTime() - value * 60 * 1000);
|
|
60
|
+
default:
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
async function main() {
|
|
37
66
|
const chalk = (await import('chalk')).default;
|
|
38
67
|
const boxen = (await import('boxen')).default;
|
|
39
68
|
|
|
69
|
+
// Check for updates (non-blocking, cached)
|
|
70
|
+
const updateNotifier = (await import('update-notifier')).default;
|
|
71
|
+
updateNotifier({ pkg }).notify();
|
|
72
|
+
|
|
40
73
|
program
|
|
41
74
|
.name('inbox')
|
|
42
75
|
.description('Gmail monitoring CLI with multi-account support')
|
|
@@ -207,6 +240,29 @@ async function main() {
|
|
|
207
240
|
console.log(chalk.cyan(' inbox install-service') + chalk.gray(' - Enable background monitoring'));
|
|
208
241
|
console.log('');
|
|
209
242
|
|
|
243
|
+
// Offer to install Claude Code skill
|
|
244
|
+
const rl2 = readline.createInterface({
|
|
245
|
+
input: process.stdin,
|
|
246
|
+
output: process.stdout,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const installSkillAnswer = await prompt(rl2, chalk.cyan(' Install Claude Code skill for AI-powered inbox management? (Y/n): '));
|
|
250
|
+
rl2.close();
|
|
251
|
+
|
|
252
|
+
if (installSkillAnswer.toLowerCase() !== 'n' && installSkillAnswer.toLowerCase() !== 'no') {
|
|
253
|
+
try {
|
|
254
|
+
const result = installSkill();
|
|
255
|
+
console.log(chalk.green('\n ✓ Claude Code skill installed!'));
|
|
256
|
+
console.log(chalk.gray(` Location: ${result.path}`));
|
|
257
|
+
console.log(chalk.gray(' In Claude Code, ask: "check my emails" or "clean up my inbox"\n'));
|
|
258
|
+
} catch (skillError) {
|
|
259
|
+
console.log(chalk.yellow(`\n Could not install skill: ${skillError.message}`));
|
|
260
|
+
console.log(chalk.gray(' You can install it later with: inbox install-skill\n'));
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
console.log(chalk.gray('\n Skipped. Install later with: inbox install-skill\n'));
|
|
264
|
+
}
|
|
265
|
+
|
|
210
266
|
} catch (error) {
|
|
211
267
|
rl.close();
|
|
212
268
|
console.error(chalk.red('\nSetup failed:'), error.message);
|
|
@@ -278,6 +334,8 @@ async function main() {
|
|
|
278
334
|
console.log(` ${chalk.cyan(acc.name)} - ${acc.email || 'unknown email'}`);
|
|
279
335
|
}
|
|
280
336
|
console.log('');
|
|
337
|
+
console.log(chalk.gray('To add another account: inbox auth -a <name>'));
|
|
338
|
+
console.log('');
|
|
281
339
|
});
|
|
282
340
|
|
|
283
341
|
program
|
|
@@ -453,6 +511,8 @@ async function main() {
|
|
|
453
511
|
.option('-a, --account <name>', 'Account to analyze (or "all")', 'all')
|
|
454
512
|
.option('-n, --count <number>', 'Number of emails to analyze per account', '20')
|
|
455
513
|
.option('--all', 'Include read and unread emails (default: unread only)')
|
|
514
|
+
.option('--since <duration>', 'Only include emails from last N days/hours (e.g., "7d", "24h", "3d")')
|
|
515
|
+
.option('--group-by <field>', 'Group emails by field (sender)')
|
|
456
516
|
.action(async (options) => {
|
|
457
517
|
try {
|
|
458
518
|
const accounts = options.account === 'all'
|
|
@@ -465,15 +525,36 @@ async function main() {
|
|
|
465
525
|
|
|
466
526
|
const maxPerAccount = parseInt(options.count, 10);
|
|
467
527
|
const includeRead = !!options.all;
|
|
468
|
-
|
|
528
|
+
let allEmails = [];
|
|
469
529
|
|
|
470
530
|
for (const account of accounts) {
|
|
471
531
|
const emails = await getUnreadEmails(account, maxPerAccount, includeRead);
|
|
472
532
|
allEmails.push(...emails);
|
|
473
533
|
}
|
|
474
534
|
|
|
475
|
-
//
|
|
476
|
-
|
|
535
|
+
// Filter by --since if provided
|
|
536
|
+
if (options.since) {
|
|
537
|
+
const sinceDate = parseSinceDuration(options.since);
|
|
538
|
+
if (sinceDate) {
|
|
539
|
+
allEmails = allEmails.filter(email => {
|
|
540
|
+
const emailDate = new Date(email.date);
|
|
541
|
+
return emailDate >= sinceDate;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Group by sender if requested
|
|
547
|
+
if (options.groupBy) {
|
|
548
|
+
if (options.groupBy !== 'sender') {
|
|
549
|
+
console.error(JSON.stringify({ error: `Unsupported group-by field: ${options.groupBy}. Supported: sender` }));
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
const grouped = groupEmailsBySender(allEmails);
|
|
553
|
+
console.log(JSON.stringify(grouped, null, 2));
|
|
554
|
+
} else {
|
|
555
|
+
// Output pure JSON for AI consumption
|
|
556
|
+
console.log(JSON.stringify(allEmails, null, 2));
|
|
557
|
+
}
|
|
477
558
|
} catch (error) {
|
|
478
559
|
console.error(JSON.stringify({ error: error.message }));
|
|
479
560
|
process.exit(1);
|
|
@@ -483,65 +564,159 @@ async function main() {
|
|
|
483
564
|
program
|
|
484
565
|
.command('delete')
|
|
485
566
|
.description('Move emails to trash')
|
|
486
|
-
.
|
|
487
|
-
.option('
|
|
567
|
+
.option('--ids <ids>', 'Comma-separated message IDs to delete')
|
|
568
|
+
.option('--sender <pattern>', 'Filter by sender (case-insensitive substring)')
|
|
569
|
+
.option('--match <pattern>', 'Filter by subject (case-insensitive substring)')
|
|
570
|
+
.option('-a, --account <name>', 'Account name (or "all" for filter-based deletion)', 'all')
|
|
571
|
+
.option('--limit <number>', 'Max emails when using filters (default: 50)', '50')
|
|
488
572
|
.option('--confirm', 'Skip confirmation prompt')
|
|
489
573
|
.option('--dry-run', 'Show what would be deleted without deleting')
|
|
574
|
+
.option('--force', 'Override safety warnings (required for short patterns or large matches)')
|
|
490
575
|
.action(async (options) => {
|
|
491
576
|
try {
|
|
492
|
-
|
|
577
|
+
let emailsToDelete = [];
|
|
578
|
+
const limit = parseInt(options.limit, 10);
|
|
493
579
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
580
|
+
// Scenario A: IDs provided
|
|
581
|
+
if (options.ids) {
|
|
582
|
+
const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
|
|
498
583
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (!account) {
|
|
502
|
-
const accounts = getAccounts();
|
|
503
|
-
if (accounts.length === 1) {
|
|
504
|
-
account = accounts[0].name;
|
|
505
|
-
} else if (accounts.length > 1) {
|
|
506
|
-
console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
|
|
507
|
-
console.log(chalk.gray('Available accounts:'));
|
|
508
|
-
accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
|
|
584
|
+
if (ids.length === 0) {
|
|
585
|
+
console.log(chalk.yellow('No message IDs provided.'));
|
|
509
586
|
return;
|
|
510
|
-
} else {
|
|
511
|
-
account = 'default';
|
|
512
587
|
}
|
|
513
|
-
}
|
|
514
588
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
589
|
+
// Get account for ID-based deletion
|
|
590
|
+
let account = options.account === 'all' ? null : options.account;
|
|
591
|
+
if (!account) {
|
|
592
|
+
const accounts = getAccounts();
|
|
593
|
+
if (accounts.length === 1) {
|
|
594
|
+
account = accounts[0].name;
|
|
595
|
+
} else if (accounts.length > 1) {
|
|
596
|
+
console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
|
|
597
|
+
console.log(chalk.gray('Available accounts:'));
|
|
598
|
+
accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
|
|
599
|
+
return;
|
|
600
|
+
} else {
|
|
601
|
+
account = 'default';
|
|
602
|
+
}
|
|
603
|
+
}
|
|
518
604
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
605
|
+
// Fetch email details for logging before deletion
|
|
606
|
+
console.log(chalk.cyan(`Fetching ${ids.length} email(s) for deletion...`));
|
|
607
|
+
|
|
608
|
+
for (const id of ids) {
|
|
609
|
+
const email = await getEmailById(account, id);
|
|
610
|
+
if (email) {
|
|
611
|
+
emailsToDelete.push(email);
|
|
612
|
+
} else {
|
|
613
|
+
console.log(chalk.yellow(`Could not find email with ID: ${id}`));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Apply optional filters to ID-based selection
|
|
618
|
+
if (options.sender || options.match) {
|
|
619
|
+
emailsToDelete = emailsToDelete.filter(e => {
|
|
620
|
+
const matchesSender = !options.sender ||
|
|
621
|
+
e.from.toLowerCase().includes(options.sender.toLowerCase());
|
|
622
|
+
const matchesSubject = !options.match ||
|
|
623
|
+
e.subject.toLowerCase().includes(options.match.toLowerCase());
|
|
624
|
+
return matchesSender && matchesSubject;
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Scenario B: No IDs, use filters to find emails
|
|
629
|
+
else if (options.sender || options.match) {
|
|
630
|
+
// Determine accounts
|
|
631
|
+
let accountNames;
|
|
632
|
+
if (options.account === 'all') {
|
|
633
|
+
const accounts = getAccounts();
|
|
634
|
+
accountNames = accounts.length > 0 ? accounts.map(a => a.name) : ['default'];
|
|
523
635
|
} else {
|
|
524
|
-
|
|
636
|
+
accountNames = [options.account];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
console.log(chalk.cyan(`Searching for emails matching filters...`));
|
|
640
|
+
|
|
641
|
+
// Fetch and filter from each account
|
|
642
|
+
for (const accountName of accountNames) {
|
|
643
|
+
const emails = await getUnreadEmails(accountName, limit);
|
|
644
|
+
const filtered = emails.filter(e => {
|
|
645
|
+
const matchesSender = !options.sender ||
|
|
646
|
+
e.from.toLowerCase().includes(options.sender.toLowerCase());
|
|
647
|
+
const matchesSubject = !options.match ||
|
|
648
|
+
e.subject.toLowerCase().includes(options.match.toLowerCase());
|
|
649
|
+
return matchesSender && matchesSubject;
|
|
650
|
+
});
|
|
651
|
+
emailsToDelete.push(...filtered);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Enforce safety limit
|
|
655
|
+
if (emailsToDelete.length > limit) {
|
|
656
|
+
console.log(chalk.yellow(`Found ${emailsToDelete.length} emails. Limiting to ${limit}.`));
|
|
657
|
+
console.log(chalk.gray(`Use --limit N to increase.`));
|
|
658
|
+
emailsToDelete = emailsToDelete.slice(0, limit);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (emailsToDelete.length === 0) {
|
|
662
|
+
console.log(chalk.yellow('No emails found matching filters.'));
|
|
663
|
+
return;
|
|
525
664
|
}
|
|
526
665
|
}
|
|
666
|
+
// Scenario C: Neither IDs nor filters - error
|
|
667
|
+
else {
|
|
668
|
+
console.log(chalk.red('Error: Must specify --ids or filter flags (--sender, --match)'));
|
|
669
|
+
console.log(chalk.gray('Examples:'));
|
|
670
|
+
console.log(chalk.gray(' inbox delete --ids "id1,id2" --confirm'));
|
|
671
|
+
console.log(chalk.gray(' inbox delete --sender "linkedin" --dry-run'));
|
|
672
|
+
console.log(chalk.gray(' inbox delete --sender "newsletter" --match "weekly" --confirm'));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
527
675
|
|
|
528
676
|
if (emailsToDelete.length === 0) {
|
|
529
677
|
console.log(chalk.yellow('No valid emails found to delete.'));
|
|
530
678
|
return;
|
|
531
679
|
}
|
|
532
680
|
|
|
533
|
-
//
|
|
534
|
-
if (!options.
|
|
681
|
+
// Safety warnings for filter-based deletion
|
|
682
|
+
if (!options.ids && (options.sender || options.match)) {
|
|
683
|
+
const warnings = [];
|
|
684
|
+
|
|
685
|
+
if (options.sender && options.sender.length < 3) {
|
|
686
|
+
warnings.push(`Short sender pattern "${options.sender}" may match broadly`);
|
|
687
|
+
}
|
|
688
|
+
if (options.match && options.match.length < 3) {
|
|
689
|
+
warnings.push(`Short subject pattern "${options.match}" may match broadly`);
|
|
690
|
+
}
|
|
691
|
+
if (emailsToDelete.length > 100) {
|
|
692
|
+
warnings.push(`${emailsToDelete.length} emails match - large batch deletion`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// If warnings exist and no --force, block execution
|
|
696
|
+
if (warnings.length > 0 && !options.force) {
|
|
697
|
+
console.log(chalk.yellow('\n⚠️ Safety warnings:'));
|
|
698
|
+
warnings.forEach(w => console.log(chalk.yellow(` - ${w}`)));
|
|
699
|
+
console.log(chalk.gray('\nUse --force to proceed anyway, or narrow your filters.'));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Always show preview for filter-based deletion (even with --confirm)
|
|
705
|
+
const isFilterBased = !options.ids && (options.sender || options.match);
|
|
706
|
+
if (isFilterBased || !options.confirm || options.dryRun) {
|
|
535
707
|
console.log(chalk.bold('\nEmails to be moved to trash:\n'));
|
|
536
708
|
emailsToDelete.forEach(e => {
|
|
537
709
|
const from = e.from.length > 40 ? e.from.substring(0, 37) + '...' : e.from;
|
|
538
710
|
const subject = e.subject.length > 50 ? e.subject.substring(0, 47) + '...' : e.subject;
|
|
539
|
-
|
|
711
|
+
const accountTag = e.account ? chalk.gray(`[${e.account}] `) : '';
|
|
712
|
+
console.log(chalk.white(` ${accountTag}${from}`));
|
|
540
713
|
console.log(chalk.gray(` ${subject}\n`));
|
|
541
714
|
});
|
|
542
715
|
|
|
543
716
|
if (options.dryRun) {
|
|
544
717
|
console.log(chalk.yellow(`\nDry run: ${emailsToDelete.length} email(s) would be deleted.`));
|
|
718
|
+
// Output IDs for programmatic use
|
|
719
|
+
console.log(chalk.gray(`\nIDs: ${emailsToDelete.map(e => e.id).join(',')}`));
|
|
545
720
|
return;
|
|
546
721
|
}
|
|
547
722
|
|
|
@@ -549,24 +724,44 @@ async function main() {
|
|
|
549
724
|
console.log(chalk.gray('Use --confirm to skip this prompt.\n'));
|
|
550
725
|
}
|
|
551
726
|
|
|
727
|
+
// Group emails by account for deletion
|
|
728
|
+
const emailsByAccount = {};
|
|
729
|
+
for (const email of emailsToDelete) {
|
|
730
|
+
const acc = email.account || 'default';
|
|
731
|
+
if (!emailsByAccount[acc]) {
|
|
732
|
+
emailsByAccount[acc] = [];
|
|
733
|
+
}
|
|
734
|
+
emailsByAccount[acc].push(email);
|
|
735
|
+
}
|
|
736
|
+
|
|
552
737
|
// Log deletions BEFORE actually deleting
|
|
553
738
|
logDeletions(emailsToDelete);
|
|
554
739
|
console.log(chalk.gray(`Logged to: ${getLogPath()}`));
|
|
555
740
|
|
|
556
|
-
// Perform the deletion
|
|
557
|
-
|
|
741
|
+
// Perform the deletion for each account
|
|
742
|
+
let totalSucceeded = 0;
|
|
743
|
+
let totalFailed = 0;
|
|
558
744
|
|
|
559
|
-
const
|
|
560
|
-
|
|
745
|
+
for (const [accountName, emails] of Object.entries(emailsByAccount)) {
|
|
746
|
+
const results = await trashEmails(accountName, emails.map(e => e.id));
|
|
747
|
+
const succeeded = results.filter(r => r.success).length;
|
|
748
|
+
const failed = results.filter(r => !r.success).length;
|
|
749
|
+
totalSucceeded += succeeded;
|
|
750
|
+
totalFailed += failed;
|
|
561
751
|
|
|
562
|
-
|
|
563
|
-
|
|
752
|
+
if (failed > 0) {
|
|
753
|
+
results.filter(r => !r.success).forEach(r => {
|
|
754
|
+
console.log(chalk.red(` - ${r.id}: ${r.error}`));
|
|
755
|
+
});
|
|
756
|
+
}
|
|
564
757
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
758
|
+
|
|
759
|
+
if (totalSucceeded > 0) {
|
|
760
|
+
console.log(chalk.green(`\nMoved ${totalSucceeded} email(s) to trash.`));
|
|
761
|
+
console.log(chalk.gray(`Tip: Use 'inbox restore --last ${totalSucceeded}' to undo.`));
|
|
762
|
+
}
|
|
763
|
+
if (totalFailed > 0) {
|
|
764
|
+
console.log(chalk.red(`Failed to delete ${totalFailed} email(s).`));
|
|
570
765
|
}
|
|
571
766
|
|
|
572
767
|
} catch (error) {
|
|
@@ -711,6 +906,153 @@ async function main() {
|
|
|
711
906
|
}
|
|
712
907
|
});
|
|
713
908
|
|
|
909
|
+
program
|
|
910
|
+
.command('mark-read')
|
|
911
|
+
.description('Mark emails as read')
|
|
912
|
+
.requiredOption('--ids <ids>', 'Comma-separated message IDs to mark as read')
|
|
913
|
+
.option('-a, --account <name>', 'Account name')
|
|
914
|
+
.action(async (options) => {
|
|
915
|
+
try {
|
|
916
|
+
const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
|
|
917
|
+
|
|
918
|
+
if (ids.length === 0) {
|
|
919
|
+
console.log(chalk.yellow('No message IDs provided.'));
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Get account - if not specified, try to find from configured accounts
|
|
924
|
+
let account = options.account;
|
|
925
|
+
if (!account) {
|
|
926
|
+
const accounts = getAccounts();
|
|
927
|
+
if (accounts.length === 1) {
|
|
928
|
+
account = accounts[0].name;
|
|
929
|
+
} else if (accounts.length > 1) {
|
|
930
|
+
console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
|
|
931
|
+
console.log(chalk.gray('Available accounts:'));
|
|
932
|
+
accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
|
|
933
|
+
return;
|
|
934
|
+
} else {
|
|
935
|
+
account = 'default';
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
console.log(chalk.cyan(`Marking ${ids.length} email(s) as read...`));
|
|
940
|
+
|
|
941
|
+
const results = await markAsRead(account, ids);
|
|
942
|
+
|
|
943
|
+
const succeeded = results.filter(r => r.success).length;
|
|
944
|
+
const failed = results.filter(r => !r.success).length;
|
|
945
|
+
|
|
946
|
+
if (succeeded > 0) {
|
|
947
|
+
console.log(chalk.green(`\nMarked ${succeeded} email(s) as read.`));
|
|
948
|
+
}
|
|
949
|
+
if (failed > 0) {
|
|
950
|
+
console.log(chalk.red(`Failed to mark ${failed} email(s) as read.`));
|
|
951
|
+
results.filter(r => !r.success).forEach(r => {
|
|
952
|
+
console.log(chalk.red(` - ${r.id}: ${r.error}`));
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (error.message.includes('403') || error.code === 403) {
|
|
958
|
+
console.error(chalk.red('Permission denied. You may need to re-authenticate with updated scopes.'));
|
|
959
|
+
console.error(chalk.yellow('Run: inbox auth -a <account>'));
|
|
960
|
+
} else {
|
|
961
|
+
console.error(chalk.red('Error marking emails as read:'), error.message);
|
|
962
|
+
}
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
program
|
|
968
|
+
.command('archive')
|
|
969
|
+
.description('Archive emails (remove from inbox, keep in All Mail)')
|
|
970
|
+
.requiredOption('--ids <ids>', 'Comma-separated message IDs to archive')
|
|
971
|
+
.option('-a, --account <name>', 'Account name')
|
|
972
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
973
|
+
.action(async (options) => {
|
|
974
|
+
try {
|
|
975
|
+
const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
|
|
976
|
+
|
|
977
|
+
if (ids.length === 0) {
|
|
978
|
+
console.log(chalk.yellow('No message IDs provided.'));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Get account - if not specified, try to find from configured accounts
|
|
983
|
+
let account = options.account;
|
|
984
|
+
if (!account) {
|
|
985
|
+
const accounts = getAccounts();
|
|
986
|
+
if (accounts.length === 1) {
|
|
987
|
+
account = accounts[0].name;
|
|
988
|
+
} else if (accounts.length > 1) {
|
|
989
|
+
console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
|
|
990
|
+
console.log(chalk.gray('Available accounts:'));
|
|
991
|
+
accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
|
|
992
|
+
return;
|
|
993
|
+
} else {
|
|
994
|
+
account = 'default';
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Fetch email details for display
|
|
999
|
+
console.log(chalk.cyan(`Fetching ${ids.length} email(s) for archiving...`));
|
|
1000
|
+
const emailsToArchive = [];
|
|
1001
|
+
|
|
1002
|
+
for (const id of ids) {
|
|
1003
|
+
const email = await getEmailById(account, id);
|
|
1004
|
+
if (email) {
|
|
1005
|
+
emailsToArchive.push(email);
|
|
1006
|
+
} else {
|
|
1007
|
+
console.log(chalk.yellow(`Could not find email with ID: ${id}`));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (emailsToArchive.length === 0) {
|
|
1012
|
+
console.log(chalk.yellow('No valid emails found to archive.'));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Show what will be archived (unless --confirm is passed)
|
|
1017
|
+
if (!options.confirm) {
|
|
1018
|
+
console.log(chalk.bold('\nEmails to be archived:\n'));
|
|
1019
|
+
emailsToArchive.forEach(e => {
|
|
1020
|
+
const from = e.from.length > 40 ? e.from.substring(0, 37) + '...' : e.from;
|
|
1021
|
+
const subject = e.subject.length > 50 ? e.subject.substring(0, 47) + '...' : e.subject;
|
|
1022
|
+
console.log(chalk.white(` ${from}`));
|
|
1023
|
+
console.log(chalk.gray(` ${subject}\n`));
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
console.log(chalk.yellow(`\nThis will archive ${emailsToArchive.length} email(s) (remove from inbox).`));
|
|
1027
|
+
console.log(chalk.gray('Use --confirm to skip this prompt.\n'));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const results = await archiveEmails(account, emailsToArchive.map(e => e.id));
|
|
1031
|
+
|
|
1032
|
+
const succeeded = results.filter(r => r.success).length;
|
|
1033
|
+
const failed = results.filter(r => !r.success).length;
|
|
1034
|
+
|
|
1035
|
+
if (succeeded > 0) {
|
|
1036
|
+
console.log(chalk.green(`\nArchived ${succeeded} email(s).`));
|
|
1037
|
+
}
|
|
1038
|
+
if (failed > 0) {
|
|
1039
|
+
console.log(chalk.red(`Failed to archive ${failed} email(s).`));
|
|
1040
|
+
results.filter(r => !r.success).forEach(r => {
|
|
1041
|
+
console.log(chalk.red(` - ${r.id}: ${r.error}`));
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (error.message.includes('403') || error.code === 403) {
|
|
1047
|
+
console.error(chalk.red('Permission denied. You may need to re-authenticate with updated scopes.'));
|
|
1048
|
+
console.error(chalk.yellow('Run: inbox auth -a <account>'));
|
|
1049
|
+
} else {
|
|
1050
|
+
console.error(chalk.red('Error archiving emails:'), error.message);
|
|
1051
|
+
}
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
714
1056
|
program
|
|
715
1057
|
.command('install-service')
|
|
716
1058
|
.description('Install background service (launchd) for macOS')
|
|
@@ -817,6 +1159,82 @@ async function main() {
|
|
|
817
1159
|
}
|
|
818
1160
|
});
|
|
819
1161
|
|
|
1162
|
+
program
|
|
1163
|
+
.command('install-skill')
|
|
1164
|
+
.description('Install Claude Code skill for AI-powered inbox management')
|
|
1165
|
+
.option('--uninstall', 'Remove the skill instead of installing')
|
|
1166
|
+
.option('--force', 'Force install even if skill exists with different source')
|
|
1167
|
+
.action(async (options) => {
|
|
1168
|
+
if (options.uninstall) {
|
|
1169
|
+
const { uninstallSkill } = require('./skill-installer');
|
|
1170
|
+
const result = uninstallSkill();
|
|
1171
|
+
|
|
1172
|
+
if (result.existed) {
|
|
1173
|
+
console.log(chalk.green('\n✓ Skill uninstalled successfully.'));
|
|
1174
|
+
console.log(chalk.gray(` Removed: ${SKILL_DEST_DIR}\n`));
|
|
1175
|
+
} else {
|
|
1176
|
+
console.log(chalk.gray('\nSkill was not installed.\n'));
|
|
1177
|
+
}
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const status = getSkillStatus();
|
|
1182
|
+
const updateInfo = checkForUpdate();
|
|
1183
|
+
|
|
1184
|
+
// Check ownership conflict
|
|
1185
|
+
if (status.installed && !status.isOurs && !options.force) {
|
|
1186
|
+
console.log(chalk.yellow(`\n⚠️ A skill with the same name already exists but isn't from ${SOURCE_MARKER}.`));
|
|
1187
|
+
console.log(chalk.gray(` Current source: "${status.source || 'none'}"`));
|
|
1188
|
+
console.log(chalk.gray(` Location: ${SKILL_DEST_DIR}\n`));
|
|
1189
|
+
console.log(chalk.white(`To replace it, run: inbox install-skill --force\n`));
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Show current state
|
|
1194
|
+
if (status.installed && status.isOurs) {
|
|
1195
|
+
if (updateInfo.updateAvailable) {
|
|
1196
|
+
console.log(chalk.yellow('\nSkill update available (content changed).'));
|
|
1197
|
+
} else {
|
|
1198
|
+
console.log(chalk.green('\n✓ Skill is already installed and up to date.'));
|
|
1199
|
+
console.log(chalk.gray(` Location: ${SKILL_DEST_DIR}\n`));
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const result = installSkill({ force: options.force });
|
|
1206
|
+
|
|
1207
|
+
if (!result.success) {
|
|
1208
|
+
if (result.reason === 'not_owned') {
|
|
1209
|
+
console.log(chalk.yellow(`\n⚠️ Cannot update: skill exists but isn't from ${SOURCE_MARKER}.`));
|
|
1210
|
+
console.log(chalk.white(`Use --force to replace it.\n`));
|
|
1211
|
+
}
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (result.action === 'installed') {
|
|
1216
|
+
console.log(chalk.green('\n✓ Claude Code skill installed successfully!'));
|
|
1217
|
+
} else if (result.action === 'updated') {
|
|
1218
|
+
if (result.backedUp) {
|
|
1219
|
+
console.log(chalk.green('\n✓ Claude Code skill updated! (previous saved to SKILL.md.backup)'));
|
|
1220
|
+
} else {
|
|
1221
|
+
console.log(chalk.green('\n✓ Claude Code skill updated!'));
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
console.log(chalk.gray(` Location: ${result.path}\n`));
|
|
1226
|
+
|
|
1227
|
+
console.log(chalk.white('What this enables:'));
|
|
1228
|
+
console.log(chalk.gray(' • AI agents can now manage your inbox with expert triage'));
|
|
1229
|
+
console.log(chalk.gray(' • In Claude Code, ask: "check my emails" or "clean up my inbox"'));
|
|
1230
|
+
console.log(chalk.gray(' • The skill provides safe deletion with confirmation + undo\n'));
|
|
1231
|
+
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
console.error(chalk.red('Error installing skill:'), error.message);
|
|
1234
|
+
process.exit(1);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
|
|
820
1238
|
// Handle unknown commands gracefully
|
|
821
1239
|
program.on('command:*', (operands) => {
|
|
822
1240
|
console.error(chalk.red(`\nUnknown command: ${operands[0]}`));
|