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/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
- const allEmails = [];
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
- // Output pure JSON for AI consumption
476
- console.log(JSON.stringify(allEmails, null, 2));
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
- .requiredOption('--ids <ids>', 'Comma-separated message IDs to delete')
487
- .option('-a, --account <name>', 'Account name (required for single-account delete)')
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
- const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
577
+ let emailsToDelete = [];
578
+ const limit = parseInt(options.limit, 10);
493
579
 
494
- if (ids.length === 0) {
495
- console.log(chalk.yellow('No message IDs provided.'));
496
- return;
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
- // Get account - if not specified, try to find from configured accounts
500
- let account = options.account;
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
- // Fetch email details for logging before deletion
516
- console.log(chalk.cyan(`Fetching ${ids.length} email(s) for deletion...`));
517
- const emailsToDelete = [];
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
- for (const id of ids) {
520
- const email = await getEmailById(account, id);
521
- if (email) {
522
- emailsToDelete.push(email);
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
- console.log(chalk.yellow(`Could not find email with ID: ${id}`));
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
- // Show what will be deleted
534
- if (!options.confirm || options.dryRun) {
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
- console.log(chalk.white(` ${from}`));
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
- const results = await trashEmails(account, emailsToDelete.map(e => e.id));
741
+ // Perform the deletion for each account
742
+ let totalSucceeded = 0;
743
+ let totalFailed = 0;
558
744
 
559
- const succeeded = results.filter(r => r.success).length;
560
- const failed = results.filter(r => !r.success).length;
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
- if (succeeded > 0) {
563
- console.log(chalk.green(`\nMoved ${succeeded} email(s) to trash.`));
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
- if (failed > 0) {
566
- console.log(chalk.red(`Failed to delete ${failed} email(s).`));
567
- results.filter(r => !r.success).forEach(r => {
568
- console.log(chalk.red(` - ${r.id}: ${r.error}`));
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]}`));