inboxd 1.0.8 → 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 +3 -2
- package/scripts/postinstall.js +79 -0
- package/src/cli.js +460 -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,6 +34,34 @@ 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;
|
|
@@ -211,6 +240,29 @@ async function main() {
|
|
|
211
240
|
console.log(chalk.cyan(' inbox install-service') + chalk.gray(' - Enable background monitoring'));
|
|
212
241
|
console.log('');
|
|
213
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
|
+
|
|
214
266
|
} catch (error) {
|
|
215
267
|
rl.close();
|
|
216
268
|
console.error(chalk.red('\nSetup failed:'), error.message);
|
|
@@ -282,6 +334,8 @@ async function main() {
|
|
|
282
334
|
console.log(` ${chalk.cyan(acc.name)} - ${acc.email || 'unknown email'}`);
|
|
283
335
|
}
|
|
284
336
|
console.log('');
|
|
337
|
+
console.log(chalk.gray('To add another account: inbox auth -a <name>'));
|
|
338
|
+
console.log('');
|
|
285
339
|
});
|
|
286
340
|
|
|
287
341
|
program
|
|
@@ -457,6 +511,8 @@ async function main() {
|
|
|
457
511
|
.option('-a, --account <name>', 'Account to analyze (or "all")', 'all')
|
|
458
512
|
.option('-n, --count <number>', 'Number of emails to analyze per account', '20')
|
|
459
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)')
|
|
460
516
|
.action(async (options) => {
|
|
461
517
|
try {
|
|
462
518
|
const accounts = options.account === 'all'
|
|
@@ -469,15 +525,36 @@ async function main() {
|
|
|
469
525
|
|
|
470
526
|
const maxPerAccount = parseInt(options.count, 10);
|
|
471
527
|
const includeRead = !!options.all;
|
|
472
|
-
|
|
528
|
+
let allEmails = [];
|
|
473
529
|
|
|
474
530
|
for (const account of accounts) {
|
|
475
531
|
const emails = await getUnreadEmails(account, maxPerAccount, includeRead);
|
|
476
532
|
allEmails.push(...emails);
|
|
477
533
|
}
|
|
478
534
|
|
|
479
|
-
//
|
|
480
|
-
|
|
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
|
+
}
|
|
481
558
|
} catch (error) {
|
|
482
559
|
console.error(JSON.stringify({ error: error.message }));
|
|
483
560
|
process.exit(1);
|
|
@@ -487,46 +564,113 @@ async function main() {
|
|
|
487
564
|
program
|
|
488
565
|
.command('delete')
|
|
489
566
|
.description('Move emails to trash')
|
|
490
|
-
.
|
|
491
|
-
.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')
|
|
492
572
|
.option('--confirm', 'Skip confirmation prompt')
|
|
493
573
|
.option('--dry-run', 'Show what would be deleted without deleting')
|
|
574
|
+
.option('--force', 'Override safety warnings (required for short patterns or large matches)')
|
|
494
575
|
.action(async (options) => {
|
|
495
576
|
try {
|
|
496
|
-
|
|
577
|
+
let emailsToDelete = [];
|
|
578
|
+
const limit = parseInt(options.limit, 10);
|
|
497
579
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
580
|
+
// Scenario A: IDs provided
|
|
581
|
+
if (options.ids) {
|
|
582
|
+
const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
|
|
502
583
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (!account) {
|
|
506
|
-
const accounts = getAccounts();
|
|
507
|
-
if (accounts.length === 1) {
|
|
508
|
-
account = accounts[0].name;
|
|
509
|
-
} else if (accounts.length > 1) {
|
|
510
|
-
console.log(chalk.yellow('Multiple accounts configured. Please specify --account <name>'));
|
|
511
|
-
console.log(chalk.gray('Available accounts:'));
|
|
512
|
-
accounts.forEach(a => console.log(chalk.gray(` - ${a.name}`)));
|
|
584
|
+
if (ids.length === 0) {
|
|
585
|
+
console.log(chalk.yellow('No message IDs provided.'));
|
|
513
586
|
return;
|
|
514
|
-
} else {
|
|
515
|
-
account = 'default';
|
|
516
587
|
}
|
|
517
|
-
}
|
|
518
588
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
}
|
|
522
604
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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'];
|
|
527
635
|
} else {
|
|
528
|
-
|
|
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);
|
|
529
659
|
}
|
|
660
|
+
|
|
661
|
+
if (emailsToDelete.length === 0) {
|
|
662
|
+
console.log(chalk.yellow('No emails found matching filters.'));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
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;
|
|
530
674
|
}
|
|
531
675
|
|
|
532
676
|
if (emailsToDelete.length === 0) {
|
|
@@ -534,18 +678,45 @@ async function main() {
|
|
|
534
678
|
return;
|
|
535
679
|
}
|
|
536
680
|
|
|
537
|
-
//
|
|
538
|
-
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) {
|
|
539
707
|
console.log(chalk.bold('\nEmails to be moved to trash:\n'));
|
|
540
708
|
emailsToDelete.forEach(e => {
|
|
541
709
|
const from = e.from.length > 40 ? e.from.substring(0, 37) + '...' : e.from;
|
|
542
710
|
const subject = e.subject.length > 50 ? e.subject.substring(0, 47) + '...' : e.subject;
|
|
543
|
-
|
|
711
|
+
const accountTag = e.account ? chalk.gray(`[${e.account}] `) : '';
|
|
712
|
+
console.log(chalk.white(` ${accountTag}${from}`));
|
|
544
713
|
console.log(chalk.gray(` ${subject}\n`));
|
|
545
714
|
});
|
|
546
715
|
|
|
547
716
|
if (options.dryRun) {
|
|
548
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(',')}`));
|
|
549
720
|
return;
|
|
550
721
|
}
|
|
551
722
|
|
|
@@ -553,24 +724,44 @@ async function main() {
|
|
|
553
724
|
console.log(chalk.gray('Use --confirm to skip this prompt.\n'));
|
|
554
725
|
}
|
|
555
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
|
+
|
|
556
737
|
// Log deletions BEFORE actually deleting
|
|
557
738
|
logDeletions(emailsToDelete);
|
|
558
739
|
console.log(chalk.gray(`Logged to: ${getLogPath()}`));
|
|
559
740
|
|
|
560
|
-
// Perform the deletion
|
|
561
|
-
|
|
741
|
+
// Perform the deletion for each account
|
|
742
|
+
let totalSucceeded = 0;
|
|
743
|
+
let totalFailed = 0;
|
|
562
744
|
|
|
563
|
-
const
|
|
564
|
-
|
|
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;
|
|
565
751
|
|
|
566
|
-
|
|
567
|
-
|
|
752
|
+
if (failed > 0) {
|
|
753
|
+
results.filter(r => !r.success).forEach(r => {
|
|
754
|
+
console.log(chalk.red(` - ${r.id}: ${r.error}`));
|
|
755
|
+
});
|
|
756
|
+
}
|
|
568
757
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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).`));
|
|
574
765
|
}
|
|
575
766
|
|
|
576
767
|
} catch (error) {
|
|
@@ -715,6 +906,153 @@ async function main() {
|
|
|
715
906
|
}
|
|
716
907
|
});
|
|
717
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
|
+
|
|
718
1056
|
program
|
|
719
1057
|
.command('install-service')
|
|
720
1058
|
.description('Install background service (launchd) for macOS')
|
|
@@ -821,6 +1159,82 @@ async function main() {
|
|
|
821
1159
|
}
|
|
822
1160
|
});
|
|
823
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
|
+
|
|
824
1238
|
// Handle unknown commands gracefully
|
|
825
1239
|
program.on('command:*', (operands) => {
|
|
826
1240
|
console.error(chalk.red(`\nUnknown command: ${operands[0]}`));
|