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/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
- const allEmails = [];
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
- // Output pure JSON for AI consumption
480
- 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
+ }
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
- .requiredOption('--ids <ids>', 'Comma-separated message IDs to delete')
491
- .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')
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
- const ids = options.ids.split(',').map(id => id.trim()).filter(Boolean);
577
+ let emailsToDelete = [];
578
+ const limit = parseInt(options.limit, 10);
497
579
 
498
- if (ids.length === 0) {
499
- console.log(chalk.yellow('No message IDs provided.'));
500
- return;
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
- // Get account - if not specified, try to find from configured accounts
504
- let account = options.account;
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
- // Fetch email details for logging before deletion
520
- console.log(chalk.cyan(`Fetching ${ids.length} email(s) for deletion...`));
521
- 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
+ }
522
604
 
523
- for (const id of ids) {
524
- const email = await getEmailById(account, id);
525
- if (email) {
526
- 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'];
527
635
  } else {
528
- 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);
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
- // Show what will be deleted
538
- 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) {
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
- console.log(chalk.white(` ${from}`));
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
- 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;
562
744
 
563
- const succeeded = results.filter(r => r.success).length;
564
- 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;
565
751
 
566
- if (succeeded > 0) {
567
- 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
+ }
568
757
  }
569
- if (failed > 0) {
570
- console.log(chalk.red(`Failed to delete ${failed} email(s).`));
571
- results.filter(r => !r.success).forEach(r => {
572
- console.log(chalk.red(` - ${r.id}: ${r.error}`));
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]}`));