inboxd 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -106,6 +106,69 @@ Unless the user says "inbox zero" or similar:
|
|
|
106
106
|
|
|
107
107
|
---
|
|
108
108
|
|
|
109
|
+
## User Preferences
|
|
110
|
+
|
|
111
|
+
- At the start of every session, read `~/.config/inboxd/user-preferences.md` and apply the rules to all triage/cleanup decisions.
|
|
112
|
+
- The file is natural-language markdown. Keep it under 500 lines so it fits in context.
|
|
113
|
+
- Manage it with `inboxd preferences` (view, init, edit, validate, JSON).
|
|
114
|
+
|
|
115
|
+
### First-Time Onboarding (when file is missing)
|
|
116
|
+
Offer to set up preferences once:
|
|
117
|
+
1) People to **never auto-delete**
|
|
118
|
+
2) Senders to **always clean up** (promotions, alerts)
|
|
119
|
+
3) Specific workflows (e.g., summarize newsletters)
|
|
120
|
+
4) Cleanup aggressiveness (conservative / moderate / aggressive)
|
|
121
|
+
Save answers to `~/.config/inboxd/user-preferences.md`.
|
|
122
|
+
|
|
123
|
+
### Tracking Onboarding
|
|
124
|
+
|
|
125
|
+
After completing onboarding (or if user declines), add this marker to the end of the preferences file:
|
|
126
|
+
|
|
127
|
+
```markdown
|
|
128
|
+
<!-- Internal: Onboarding completed -->
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Before offering onboarding**, check if this marker exists. If it does, do NOT offer onboarding again—even if the file only contains template placeholders. This prevents annoying users who dismissed the initial prompt.
|
|
132
|
+
|
|
133
|
+
### Learning from Feedback
|
|
134
|
+
- **Auto-save explicit requests:** "Always delete LinkedIn alerts", "Never touch mom@family.com", "I prefer brief summaries".
|
|
135
|
+
- **Confirm pattern suggestions:** "You keep deleting promo@site.com. Save a rule to clean these up?" Only suggest if the sender is active.
|
|
136
|
+
- Watch size: if approaching 500 lines, suggest consolidating older entries instead of appending endlessly.
|
|
137
|
+
|
|
138
|
+
### Preference File Format
|
|
139
|
+
- Sections: `## About Me`, `## Important People`, `## Sender Behaviors`, `## Category Rules`, `## Behavioral Preferences`.
|
|
140
|
+
- When updating, **append to existing sections** (bullets), don't overwrite user content. Include brief context ("why") to help future decisions.
|
|
141
|
+
- Never delete the file; it lives outside the skill install path and must survive updates.
|
|
142
|
+
|
|
143
|
+
### Smart Pattern Detection Window
|
|
144
|
+
When suggesting new preferences from behavior:
|
|
145
|
+
1) Only consider deletions from the last 14 days.
|
|
146
|
+
2) Confirm the sender is still active (recent unread emails).
|
|
147
|
+
3) Require 3+ deletions within the window.
|
|
148
|
+
4) Skip if the sender already exists in preferences.
|
|
149
|
+
|
|
150
|
+
### Reading the Deletion Log
|
|
151
|
+
|
|
152
|
+
The deletion log is at `~/.config/inboxd/deletion-log.json`. Each entry:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"deletedAt": "2026-01-08T10:00:00.000Z",
|
|
157
|
+
"account": "personal",
|
|
158
|
+
"id": "abc123",
|
|
159
|
+
"from": "sender@example.com",
|
|
160
|
+
"subject": "Email subject",
|
|
161
|
+
"labelIds": ["UNREAD", "INBOX"]
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Use `inboxd cleanup-suggest --json` for pre-analyzed patterns (recommended), or read the raw log with:
|
|
166
|
+
```bash
|
|
167
|
+
cat ~/.config/inboxd/deletion-log.json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
109
172
|
## Heavy Inbox Strategy
|
|
110
173
|
|
|
111
174
|
When a user has a heavy inbox (>20 unread emails), use this optimized workflow:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
const { getUnreadEmails, getEmailCount, trashEmails, getEmailById, untrashEmails, markAsRead, markAsUnread, archiveEmails, unarchiveEmails, groupEmailsBySender, getEmailContent, searchEmails, searchEmailsCount, searchEmailsPaginated, sendEmail, replyToEmail, extractLinks } = require('./gmail-monitor');
|
|
5
6
|
const { logArchives, getRecentArchives, getArchiveLogPath, removeArchiveLogEntries } = require('./archive-log');
|
|
6
7
|
const { authorize, addAccount, getAccounts, getAccountEmail, removeAccount, removeAllAccounts, renameTokenFile, validateCredentialsFile, hasCredentials, isConfigured, installCredentials } = require('./gmail-auth');
|
|
7
8
|
const { logDeletions, getRecentDeletions, getLogPath, readLog, removeLogEntries, getStats: getDeletionStats, analyzePatterns } = require('./deletion-log');
|
|
8
9
|
const { getSkillStatus, checkForUpdate, installSkill, SKILL_DEST_DIR, SOURCE_MARKER } = require('./skill-installer');
|
|
9
10
|
const { logSentEmail, getSentLogPath, getSentStats } = require('./sent-log');
|
|
11
|
+
const { getPreferencesPath, preferencesExist, readPreferences, writePreferences, validatePreferences, getTemplatePath } = require('./preferences');
|
|
10
12
|
const readline = require('readline');
|
|
11
13
|
const path = require('path');
|
|
12
14
|
const os = require('os');
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
13
16
|
const pkg = require('../package.json');
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -1823,6 +1826,129 @@ async function main() {
|
|
|
1823
1826
|
}
|
|
1824
1827
|
});
|
|
1825
1828
|
|
|
1829
|
+
program
|
|
1830
|
+
.command('preferences')
|
|
1831
|
+
.description('View and manage inbox preferences used by the AI assistant')
|
|
1832
|
+
.option('--init', 'Create preferences file from template if missing')
|
|
1833
|
+
.option('--edit', 'Open preferences file in $EDITOR (creates if missing)')
|
|
1834
|
+
.option('--validate', 'Validate preferences format and line count')
|
|
1835
|
+
.option('--json', 'Output preferences and validation as JSON')
|
|
1836
|
+
.action(async (options) => {
|
|
1837
|
+
try {
|
|
1838
|
+
const prefPath = getPreferencesPath();
|
|
1839
|
+
const templatePath = getTemplatePath();
|
|
1840
|
+
const templateContent = fs.existsSync(templatePath)
|
|
1841
|
+
? fs.readFileSync(templatePath, 'utf8')
|
|
1842
|
+
: '# Inbox Preferences\n';
|
|
1843
|
+
let createdDuringRun = false;
|
|
1844
|
+
|
|
1845
|
+
const ensurePreferencesFile = () => {
|
|
1846
|
+
if (!preferencesExist()) {
|
|
1847
|
+
writePreferences(templateContent);
|
|
1848
|
+
createdDuringRun = true;
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
// Handle init (create if missing)
|
|
1853
|
+
if (options.init) {
|
|
1854
|
+
ensurePreferencesFile();
|
|
1855
|
+
if (!options.json) {
|
|
1856
|
+
if (createdDuringRun) {
|
|
1857
|
+
console.log(chalk.green('\n✓ Preferences file created from template.'));
|
|
1858
|
+
console.log(chalk.gray(` Path: ${prefPath}\n`));
|
|
1859
|
+
} else {
|
|
1860
|
+
console.log(chalk.yellow('\nPreferences file already exists.'));
|
|
1861
|
+
console.log(chalk.gray(` Path: ${prefPath}\n`));
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// Handle edit (open editor)
|
|
1867
|
+
if (options.edit) {
|
|
1868
|
+
ensurePreferencesFile();
|
|
1869
|
+
const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'nano');
|
|
1870
|
+
if (!options.json) {
|
|
1871
|
+
console.log(chalk.gray(`Opening ${prefPath} with ${editor}...\n`));
|
|
1872
|
+
}
|
|
1873
|
+
const result = spawnSync(editor, [prefPath], { stdio: 'inherit' });
|
|
1874
|
+
if (result.error) {
|
|
1875
|
+
console.log(chalk.red(`Failed to open editor "${editor}": ${result.error.message}`));
|
|
1876
|
+
}
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
const exists = preferencesExist();
|
|
1881
|
+
const content = exists ? readPreferences() : null;
|
|
1882
|
+
const validation = validatePreferences(content);
|
|
1883
|
+
|
|
1884
|
+
if (options.json) {
|
|
1885
|
+
console.log(JSON.stringify({
|
|
1886
|
+
path: prefPath,
|
|
1887
|
+
exists,
|
|
1888
|
+
created: createdDuringRun,
|
|
1889
|
+
lineCount: validation.lineCount,
|
|
1890
|
+
valid: validation.valid,
|
|
1891
|
+
errors: validation.errors,
|
|
1892
|
+
warnings: validation.warnings,
|
|
1893
|
+
content: content || '',
|
|
1894
|
+
}, null, 2));
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (options.validate) {
|
|
1899
|
+
if (!exists) {
|
|
1900
|
+
console.log(chalk.red('\nPreferences file not found.'));
|
|
1901
|
+
console.log(chalk.gray('Create one with: inboxd preferences --init\n'));
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
if (validation.valid) {
|
|
1906
|
+
console.log(chalk.green('\n✓ Preferences file is valid.'));
|
|
1907
|
+
} else {
|
|
1908
|
+
console.log(chalk.red('\nPreferences file has issues:'));
|
|
1909
|
+
}
|
|
1910
|
+
console.log(chalk.gray(` Path: ${prefPath}`));
|
|
1911
|
+
console.log(chalk.gray(` Lines: ${validation.lineCount}`));
|
|
1912
|
+
|
|
1913
|
+
if (validation.errors.length > 0) {
|
|
1914
|
+
console.log(chalk.red('\nErrors:'));
|
|
1915
|
+
validation.errors.forEach(err => console.log(chalk.red(` - ${err}`)));
|
|
1916
|
+
}
|
|
1917
|
+
if (validation.warnings.length > 0) {
|
|
1918
|
+
console.log(chalk.yellow('\nWarnings:'));
|
|
1919
|
+
validation.warnings.forEach(warn => console.log(chalk.yellow(` - ${warn}`)));
|
|
1920
|
+
}
|
|
1921
|
+
console.log('');
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// Default: display preferences or hint to init
|
|
1926
|
+
if (!exists) {
|
|
1927
|
+
console.log(chalk.yellow('\nNo preferences file found.'));
|
|
1928
|
+
console.log(chalk.gray('Create one with: inboxd preferences --init'));
|
|
1929
|
+
console.log(chalk.gray('Then edit with: inboxd preferences --edit\n'));
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (validation.lineCount > 500) {
|
|
1934
|
+
console.log(chalk.red(`\nPreferences file is too long (${validation.lineCount} lines). Please shorten below 500 lines.\n`));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
console.log(chalk.bold('\nInbox Preferences\n'));
|
|
1939
|
+
console.log(content);
|
|
1940
|
+
console.log(chalk.gray(`\nPath: ${prefPath}`));
|
|
1941
|
+
console.log(chalk.gray(`Lines: ${validation.lineCount}\n`));
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
if (options.json) {
|
|
1944
|
+
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
1945
|
+
} else {
|
|
1946
|
+
console.error(chalk.red('Error managing preferences:'), error.message);
|
|
1947
|
+
}
|
|
1948
|
+
process.exit(1);
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1826
1952
|
program
|
|
1827
1953
|
.command('install-skill')
|
|
1828
1954
|
.description('Install Claude Code skill for AI-powered inbox management')
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { atomicWriteSync } = require('./utils');
|
|
4
|
+
const { TOKEN_DIR } = require('./gmail-auth');
|
|
5
|
+
|
|
6
|
+
const TEMPLATE_PATH = path.join(__dirname, 'templates', 'user-preferences-template.md');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the path to the user preferences file
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function getPreferencesPath() {
|
|
13
|
+
return path.join(TOKEN_DIR, 'user-preferences.md');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ensures the config directory exists
|
|
18
|
+
*/
|
|
19
|
+
function ensureConfigDir() {
|
|
20
|
+
fs.mkdirSync(TOKEN_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks if the preferences file exists
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
function preferencesExist() {
|
|
28
|
+
return fs.existsSync(getPreferencesPath());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reads preferences file contents
|
|
33
|
+
* @returns {string|null} File content or null if missing
|
|
34
|
+
*/
|
|
35
|
+
function readPreferences() {
|
|
36
|
+
if (!preferencesExist()) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return fs.readFileSync(getPreferencesPath(), 'utf8');
|
|
41
|
+
} catch (_err) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the line count of the current preferences file (or provided content)
|
|
48
|
+
* @param {string|null} content - Optional content to count lines from
|
|
49
|
+
* @returns {number}
|
|
50
|
+
*/
|
|
51
|
+
function getLineCount(content = null) {
|
|
52
|
+
const text = content !== null ? content : readPreferences();
|
|
53
|
+
if (!text) return 0;
|
|
54
|
+
return text.split(/\r?\n/).length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Writes preferences to disk, creating a backup if the file already exists
|
|
59
|
+
* @param {string} content - Content to write
|
|
60
|
+
* @returns {{ path: string, backupPath: string|null }}
|
|
61
|
+
*/
|
|
62
|
+
function writePreferences(content) {
|
|
63
|
+
ensureConfigDir();
|
|
64
|
+
const prefPath = getPreferencesPath();
|
|
65
|
+
let backupPath = null;
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(prefPath)) {
|
|
68
|
+
backupPath = `${prefPath}.backup`;
|
|
69
|
+
try {
|
|
70
|
+
fs.copyFileSync(prefPath, backupPath);
|
|
71
|
+
} catch (_err) {
|
|
72
|
+
// If backup fails, better to abort than risk data loss
|
|
73
|
+
throw new Error(`Failed to create backup at ${backupPath}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
atomicWriteSync(prefPath, content);
|
|
78
|
+
return { path: prefPath, backupPath };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validates preferences content for basic safety rules
|
|
83
|
+
* @param {string|null} content - Content to validate (reads current file if null)
|
|
84
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[], lineCount: number }}
|
|
85
|
+
*/
|
|
86
|
+
function validatePreferences(content = null) {
|
|
87
|
+
const text = content !== null ? content : readPreferences();
|
|
88
|
+
|
|
89
|
+
if (!text) {
|
|
90
|
+
return {
|
|
91
|
+
valid: false,
|
|
92
|
+
errors: ['Preferences file not found or empty'],
|
|
93
|
+
warnings: [],
|
|
94
|
+
lineCount: 0,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const errors = [];
|
|
99
|
+
const warnings = [];
|
|
100
|
+
const lineCount = getLineCount(text);
|
|
101
|
+
|
|
102
|
+
if (lineCount > 500) {
|
|
103
|
+
errors.push(`Preferences exceed 500 lines (currently ${lineCount}). Consider consolidating rules.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const requiredSections = [
|
|
107
|
+
'## About Me',
|
|
108
|
+
'## Important People',
|
|
109
|
+
'## Sender Behaviors',
|
|
110
|
+
'## Category Rules',
|
|
111
|
+
'## Behavioral Preferences',
|
|
112
|
+
];
|
|
113
|
+
const missingSections = requiredSections.filter(section => !text.toLowerCase().includes(section.toLowerCase()));
|
|
114
|
+
if (missingSections.length > 0) {
|
|
115
|
+
warnings.push(`Missing sections: ${missingSections.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!text.trimStart().toLowerCase().startsWith('# inbox preferences')) {
|
|
119
|
+
warnings.push('Missing top-level heading "# Inbox Preferences"');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
valid: errors.length === 0,
|
|
124
|
+
errors,
|
|
125
|
+
warnings,
|
|
126
|
+
lineCount,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Appends content to a specific section, creating it if missing
|
|
132
|
+
* @param {string} sectionTitle - Section heading without hashes (e.g., "Important People (Never Auto-Delete)")
|
|
133
|
+
* @param {string} entry - Entry to append (bullet text)
|
|
134
|
+
* @returns {{ path: string, backupPath: string|null, createdSection: boolean }}
|
|
135
|
+
*/
|
|
136
|
+
function appendToSection(sectionTitle, entry) {
|
|
137
|
+
const prefPath = getPreferencesPath();
|
|
138
|
+
ensureConfigDir();
|
|
139
|
+
|
|
140
|
+
let content = readPreferences();
|
|
141
|
+
if (!content) {
|
|
142
|
+
content = fs.existsSync(TEMPLATE_PATH)
|
|
143
|
+
? fs.readFileSync(TEMPLATE_PATH, 'utf8')
|
|
144
|
+
: `# Inbox Preferences\n\n## ${sectionTitle}\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const lines = content.split(/\r?\n/);
|
|
148
|
+
const normalized = sectionTitle.trim().toLowerCase();
|
|
149
|
+
const sectionHeader = `## ${sectionTitle}`;
|
|
150
|
+
|
|
151
|
+
let sectionIndex = lines.findIndex(line => line.trim().toLowerCase() === `## ${normalized}`);
|
|
152
|
+
let createdSection = false;
|
|
153
|
+
|
|
154
|
+
if (sectionIndex === -1) {
|
|
155
|
+
// Append new section at end
|
|
156
|
+
if (lines[lines.length - 1].trim() !== '') {
|
|
157
|
+
lines.push('');
|
|
158
|
+
}
|
|
159
|
+
lines.push(sectionHeader, `- ${entry}`);
|
|
160
|
+
createdSection = true;
|
|
161
|
+
} else {
|
|
162
|
+
// Find insertion point (before next header or end of file)
|
|
163
|
+
let insertIndex = lines.length;
|
|
164
|
+
for (let i = sectionIndex + 1; i < lines.length; i++) {
|
|
165
|
+
if (lines[i].startsWith('#')) {
|
|
166
|
+
insertIndex = i;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Ensure a blank line before the append if needed
|
|
172
|
+
if (lines[insertIndex - 1] && lines[insertIndex - 1].trim() !== '') {
|
|
173
|
+
lines.splice(insertIndex, 0, '');
|
|
174
|
+
insertIndex++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const bullet = entry.trim().startsWith('-') ? entry.trim() : `- ${entry.trim()}`;
|
|
178
|
+
lines.splice(insertIndex, 0, bullet);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { backupPath } = writePreferences(lines.join('\n'));
|
|
182
|
+
return { path: prefPath, backupPath, createdSection };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the template path (useful for CLI to bootstrap)
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
function getTemplatePath() {
|
|
190
|
+
return TEMPLATE_PATH;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
getPreferencesPath,
|
|
195
|
+
preferencesExist,
|
|
196
|
+
readPreferences,
|
|
197
|
+
writePreferences,
|
|
198
|
+
validatePreferences,
|
|
199
|
+
appendToSection,
|
|
200
|
+
getLineCount,
|
|
201
|
+
getTemplatePath,
|
|
202
|
+
};
|
package/src/skill-installer.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Source marker: Only manages skills with `source: inboxd` in front matter
|
|
7
7
|
* - Content hash: Detects changes without version numbers
|
|
8
8
|
* - Backup: Creates .backup before replacing modified files
|
|
9
|
+
* - Never touches user config files (tokens, preferences) under ~/.config/inboxd
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const fs = require('fs');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Inbox Preferences
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
This file stores your inbox preferences. The AI assistant reads this
|
|
5
|
+
at the start of each session to personalize how it manages your email.
|
|
6
|
+
Keep it under 500 lines. Edit with: inboxd preferences --edit
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
## About Me
|
|
10
|
+
<!-- Describe your context: job role, interests, what emails matter to you -->
|
|
11
|
+
<!-- Example entries (delete these and add your own): -->
|
|
12
|
+
- Software engineer interested in AI/ML opportunities
|
|
13
|
+
- Based in London, prefer remote-friendly roles
|
|
14
|
+
- Active on GitHub, receive many notifications
|
|
15
|
+
|
|
16
|
+
## Important People (Never Auto-Delete)
|
|
17
|
+
<!-- Senders whose emails should NEVER be suggested for deletion -->
|
|
18
|
+
<!-- Example: -->
|
|
19
|
+
- partner@gmail.com - spouse, always important
|
|
20
|
+
- hr@company.com - work-related
|
|
21
|
+
|
|
22
|
+
## Sender Behaviors
|
|
23
|
+
<!-- Rules for specific senders or domains -->
|
|
24
|
+
<!-- Examples: -->
|
|
25
|
+
- **LinkedIn job alerts** - When there are multiple, summarize the best matches for my profile and suggest deleting the rest
|
|
26
|
+
- **GitHub notifications** - Keep PRs I'm tagged on, archive others after reading
|
|
27
|
+
|
|
28
|
+
## Category Rules
|
|
29
|
+
<!-- Rules for types of emails -->
|
|
30
|
+
<!-- Examples: -->
|
|
31
|
+
- Bank/Financial emails - Archive after viewing, never delete
|
|
32
|
+
- Promotional emails older than 14 days - Can be auto-suggested for cleanup
|
|
33
|
+
- Newsletters I haven't read in 30 days - Suggest unsubscribing
|
|
34
|
+
|
|
35
|
+
## Behavioral Preferences
|
|
36
|
+
<!-- How you want the AI to behave -->
|
|
37
|
+
<!-- Examples: -->
|
|
38
|
+
- I prefer brief summaries (2-3 sentences)
|
|
39
|
+
- Always ask before deleting more than 10 emails at once
|
|
40
|
+
- On busy days (>50 unread), prioritize action-required emails
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterAll, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
describe('preferences module', () => {
|
|
7
|
+
const tempDir = path.join(os.tmpdir(), 'inboxd-preferences-test');
|
|
8
|
+
const originalTokenDir = process.env.INBOXD_TOKEN_DIR;
|
|
9
|
+
let getPreferencesPath;
|
|
10
|
+
let preferencesExist;
|
|
11
|
+
let readPreferences;
|
|
12
|
+
let writePreferences;
|
|
13
|
+
let validatePreferences;
|
|
14
|
+
let appendToSection;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
vi.resetModules();
|
|
21
|
+
process.env.INBOXD_TOKEN_DIR = tempDir;
|
|
22
|
+
|
|
23
|
+
const module = await import('../src/preferences');
|
|
24
|
+
getPreferencesPath = module.getPreferencesPath;
|
|
25
|
+
preferencesExist = module.preferencesExist;
|
|
26
|
+
readPreferences = module.readPreferences;
|
|
27
|
+
writePreferences = module.writePreferences;
|
|
28
|
+
validatePreferences = module.validatePreferences;
|
|
29
|
+
appendToSection = module.appendToSection;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
34
|
+
if (originalTokenDir === undefined) {
|
|
35
|
+
delete process.env.INBOXD_TOKEN_DIR;
|
|
36
|
+
} else {
|
|
37
|
+
process.env.INBOXD_TOKEN_DIR = originalTokenDir;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('uses INBOXD_TOKEN_DIR for preference path', () => {
|
|
42
|
+
expect(getPreferencesPath()).toBe(path.join(tempDir, 'user-preferences.md'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('writes preferences and creates a backup on overwrite', () => {
|
|
46
|
+
const initial = '# Inbox Preferences\nInitial content\n';
|
|
47
|
+
writePreferences(initial);
|
|
48
|
+
expect(preferencesExist()).toBe(true);
|
|
49
|
+
|
|
50
|
+
const updated = '# Inbox Preferences\nUpdated content\n';
|
|
51
|
+
writePreferences(updated);
|
|
52
|
+
|
|
53
|
+
const backupPath = `${getPreferencesPath()}.backup`;
|
|
54
|
+
expect(fs.existsSync(backupPath)).toBe(true);
|
|
55
|
+
const backupContent = fs.readFileSync(backupPath, 'utf8');
|
|
56
|
+
expect(backupContent).toContain('Initial content');
|
|
57
|
+
|
|
58
|
+
const current = readPreferences();
|
|
59
|
+
expect(current).toContain('Updated content');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('flags files over 500 lines', () => {
|
|
63
|
+
const oversized = '# Inbox Preferences\n' + 'line\n'.repeat(501);
|
|
64
|
+
const result = validatePreferences(oversized);
|
|
65
|
+
|
|
66
|
+
expect(result.valid).toBe(false);
|
|
67
|
+
expect(result.errors.some(e => e.includes('500'))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('appends to existing sections using the template when missing', () => {
|
|
71
|
+
const result = appendToSection('Important People (Never Auto-Delete)', 'test@example.com - never delete');
|
|
72
|
+
const content = readPreferences();
|
|
73
|
+
|
|
74
|
+
expect(result.createdSection).toBe(false);
|
|
75
|
+
expect(content).toContain('## Important People');
|
|
76
|
+
expect(content).toContain('test@example.com');
|
|
77
|
+
});
|
|
78
|
+
});
|