tidyf 1.0.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.
package/src/index.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * tidy - AI-powered file organizer
3
+ *
4
+ * Library exports for programmatic use
5
+ */
6
+
7
+ // Types
8
+ export type {
9
+ FileMetadata,
10
+ FileCategory,
11
+ FileMoveProposal,
12
+ OrganizationProposal,
13
+ OrganizeOptions,
14
+ WatchOptions,
15
+ WatchEvent,
16
+ MoveStatus,
17
+ MoveResult,
18
+ } from "./types/organizer.ts";
19
+
20
+ export type {
21
+ ModelSelection,
22
+ FolderRule,
23
+ CategoryRule,
24
+ TidyConfig,
25
+ ConfigOptions,
26
+ } from "./types/config.ts";
27
+
28
+ // Config
29
+ export {
30
+ resolveConfig,
31
+ getGlobalConfigPath,
32
+ getLocalConfigPath,
33
+ getGlobalRulesPath,
34
+ getLocalRulesPath,
35
+ readConfig,
36
+ writeConfig,
37
+ getRulesPrompt,
38
+ parseModelString,
39
+ expandPath,
40
+ shouldIgnore,
41
+ initGlobalConfig,
42
+ } from "./lib/config.ts";
43
+
44
+ // Scanner
45
+ export {
46
+ scanDirectory,
47
+ getFileMetadata,
48
+ getFileCategory,
49
+ groupFilesByCategory,
50
+ type ScanOptions,
51
+ } from "./lib/scanner.ts";
52
+
53
+ // File utilities
54
+ export {
55
+ moveFile,
56
+ fileExists,
57
+ ensureDirectory,
58
+ generateUniqueName,
59
+ resolveConflict,
60
+ formatFileSize,
61
+ isDirectory,
62
+ isFile,
63
+ type ConflictStrategy,
64
+ } from "./utils/files.ts";
65
+
66
+ // Icons
67
+ export {
68
+ getFileIcon,
69
+ getCategoryIcon,
70
+ getStatusIcon,
71
+ getStatusIndicator,
72
+ } from "./utils/icons.ts";
73
+
74
+ // AI
75
+ export {
76
+ analyzeFiles,
77
+ checkConflicts,
78
+ getAvailableModels,
79
+ cleanup,
80
+ type AnalyzeFilesOptions,
81
+ } from "./lib/opencode.ts";
82
+
83
+ // Watcher
84
+ export {
85
+ FileWatcher,
86
+ createWatcher,
87
+ type WatcherOptions,
88
+ } from "./lib/watcher.ts";
89
+
90
+ // Commands (for programmatic use)
91
+ export { organizeCommand } from "./commands/organize.ts";
92
+ export { watchCommand } from "./commands/watch.ts";
93
+ export { configCommand } from "./commands/config.ts";
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Configuration file management for tidy
3
+ *
4
+ * Manages ~/.tidy/ (global) and .tidy/ (local) configuration
5
+ */
6
+
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { homedir } from "os";
10
+ import type { TidyConfig, ModelSelection } from "../types/config.ts";
11
+
12
+ const CONFIG_DIR = ".tidy";
13
+ const SETTINGS_FILE = "settings.json";
14
+ const RULES_FILE = "rules.md";
15
+
16
+ const DEFAULT_MODEL: ModelSelection = {
17
+ provider: "opencode",
18
+ model: "claude-sonnet-4-5",
19
+ };
20
+
21
+ const DEFAULT_CONFIG: TidyConfig = {
22
+ organizer: DEFAULT_MODEL,
23
+ defaultSource: "~/Downloads",
24
+ defaultTarget: "~/Documents/Organized",
25
+ watchEnabled: false,
26
+ folders: [
27
+ {
28
+ sources: ["~/Downloads"],
29
+ target: "~/Documents/Organized",
30
+ watch: false,
31
+ },
32
+ ],
33
+ ignore: [
34
+ ".DS_Store",
35
+ "*.tmp",
36
+ "*.partial",
37
+ "*.crdownload",
38
+ "*.download",
39
+ "desktop.ini",
40
+ "Thumbs.db",
41
+ ],
42
+ readContent: false,
43
+ maxContentSize: 10240, // 10KB
44
+ };
45
+
46
+ const DEFAULT_RULES = `# File Organization Rules
47
+
48
+ You are an AI assistant that organizes files from a download folder. Analyze each file and categorize it appropriately.
49
+
50
+ ## Categories
51
+
52
+ ### Documents
53
+ - PDFs, Word docs, text files, spreadsheets
54
+ - Subcategorize by: Work, Personal, Receipts, Manuals, Ebooks
55
+
56
+ ### Images
57
+ - Photos, screenshots, graphics, icons
58
+ - Subcategorize by: Photos, Screenshots, Design, Icons
59
+
60
+ ### Videos
61
+ - MP4, MOV, AVI, MKV, WEBM
62
+ - Subcategorize by: Movies, Clips, Tutorials
63
+
64
+ ### Audio
65
+ - MP3, WAV, FLAC, AAC, OGG
66
+ - Subcategorize by: Music, Podcasts, Recordings
67
+
68
+ ### Archives
69
+ - ZIP, RAR, 7Z, TAR, GZ
70
+ - Keep in Archives folder, possibly extract
71
+
72
+ ### Code & Projects
73
+ - Source code files, project archives
74
+ - Keep related files together
75
+ - Detect project names from filenames
76
+
77
+ ### Applications
78
+ - DMG, PKG, EXE, APP files
79
+ - Keep in Installers folder
80
+
81
+ ## Organization Strategy
82
+
83
+ 1. **Primary sort by file type** - Use extension and MIME type
84
+ 2. **Secondary sort by context** - Detect from filename patterns
85
+ 3. **Date-based subfolders** - For large collections (photos, screenshots)
86
+ 4. **Keep related files together** - Same base name, different extensions
87
+
88
+ ## Special Rules
89
+
90
+ 1. Files with dates in name (2024-01-15, Jan2024) group by month
91
+ 2. Receipts/invoices go to Documents/Receipts
92
+ 3. Screenshots go to Images/Screenshots
93
+ 4. Keep files with same base name together (report.pdf, report.docx)
94
+ 5. Installer files go to Applications/Installers
95
+ 6. Compressed files stay as Archives unless clearly part of another category
96
+
97
+ ## Output Format
98
+
99
+ Return JSON with this exact structure:
100
+ \`\`\`json
101
+ {
102
+ "proposals": [
103
+ {
104
+ "file": "original-filename.pdf",
105
+ "destination": "Documents/Work/Reports",
106
+ "category": {
107
+ "name": "Documents",
108
+ "subcategory": "Work/Reports",
109
+ "suggestedPath": "Documents/Work/Reports",
110
+ "confidence": 0.95,
111
+ "reasoning": "PDF file with report-like naming pattern"
112
+ }
113
+ }
114
+ ],
115
+ "strategy": "Brief explanation of overall approach",
116
+ "uncategorized": ["files that couldn't be categorized"]
117
+ }
118
+ \`\`\`
119
+
120
+ ## Important
121
+
122
+ - Every file from the input MUST appear in either proposals or uncategorized
123
+ - Return ONLY the JSON object, no markdown code blocks or explanations
124
+ - Use forward slashes for paths
125
+ - Destination should be relative to the target directory
126
+ `;
127
+
128
+ /**
129
+ * Get the global config directory path
130
+ */
131
+ export function getGlobalConfigDir(): string {
132
+ return join(homedir(), CONFIG_DIR);
133
+ }
134
+
135
+ /**
136
+ * Get the global config file path
137
+ */
138
+ export function getGlobalConfigPath(): string {
139
+ return join(getGlobalConfigDir(), SETTINGS_FILE);
140
+ }
141
+
142
+ /**
143
+ * Get the global rules file path
144
+ */
145
+ export function getGlobalRulesPath(): string {
146
+ return join(getGlobalConfigDir(), RULES_FILE);
147
+ }
148
+
149
+ /**
150
+ * Get the local config directory path
151
+ */
152
+ export function getLocalConfigDir(basePath: string = process.cwd()): string {
153
+ return join(basePath, CONFIG_DIR);
154
+ }
155
+
156
+ /**
157
+ * Get the local config file path
158
+ */
159
+ export function getLocalConfigPath(basePath: string = process.cwd()): string {
160
+ return join(getLocalConfigDir(basePath), SETTINGS_FILE);
161
+ }
162
+
163
+ /**
164
+ * Get the local rules file path
165
+ */
166
+ export function getLocalRulesPath(basePath: string = process.cwd()): string {
167
+ return join(getLocalConfigDir(basePath), RULES_FILE);
168
+ }
169
+
170
+ /**
171
+ * Ensure config directory exists
172
+ */
173
+ export function ensureConfigDir(path: string): void {
174
+ if (!existsSync(path)) {
175
+ mkdirSync(path, { recursive: true });
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Read config from a file
181
+ */
182
+ export function readConfig(path: string): TidyConfig {
183
+ if (!existsSync(path)) {
184
+ return {};
185
+ }
186
+ try {
187
+ const content = readFileSync(path, "utf-8");
188
+ return JSON.parse(content);
189
+ } catch {
190
+ return {};
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Write config to a file
196
+ */
197
+ export function writeConfig(path: string, config: TidyConfig): void {
198
+ const dir = dirname(path);
199
+ ensureConfigDir(dir);
200
+ writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
201
+ }
202
+
203
+ /**
204
+ * Read rules from a file
205
+ */
206
+ export function readRules(path: string): string | null {
207
+ if (!existsSync(path)) {
208
+ return null;
209
+ }
210
+ try {
211
+ return readFileSync(path, "utf-8");
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Write rules to a file
219
+ */
220
+ export function writeRules(path: string, rules: string): void {
221
+ const dir = dirname(path);
222
+ ensureConfigDir(dir);
223
+ writeFileSync(path, rules, "utf-8");
224
+ }
225
+
226
+ /**
227
+ * Initialize global config with defaults if it doesn't exist
228
+ */
229
+ export function initGlobalConfig(): void {
230
+ const configPath = getGlobalConfigPath();
231
+ const rulesPath = getGlobalRulesPath();
232
+
233
+ if (!existsSync(configPath)) {
234
+ writeConfig(configPath, DEFAULT_CONFIG);
235
+ }
236
+
237
+ if (!existsSync(rulesPath)) {
238
+ writeRules(rulesPath, DEFAULT_RULES);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Resolve the effective configuration by merging global and local configs
244
+ */
245
+ export function resolveConfig(basePath: string = process.cwd()): TidyConfig {
246
+ // Start with defaults
247
+ const config: TidyConfig = { ...DEFAULT_CONFIG };
248
+
249
+ // Merge global config
250
+ const globalConfig = readConfig(getGlobalConfigPath());
251
+ Object.assign(config, globalConfig);
252
+
253
+ // Merge local config (takes precedence)
254
+ const localConfig = readConfig(getLocalConfigPath(basePath));
255
+ Object.assign(config, localConfig);
256
+
257
+ return config;
258
+ }
259
+
260
+ /**
261
+ * Get the rules prompt by merging global and local rules
262
+ */
263
+ export function getRulesPrompt(basePath: string = process.cwd()): string {
264
+ // Try local rules first
265
+ const localRules = readRules(getLocalRulesPath(basePath));
266
+ if (localRules) {
267
+ return localRules;
268
+ }
269
+
270
+ // Fall back to global rules
271
+ const globalRules = readRules(getGlobalRulesPath());
272
+ if (globalRules) {
273
+ return globalRules;
274
+ }
275
+
276
+ // Return default rules
277
+ return DEFAULT_RULES;
278
+ }
279
+
280
+ /**
281
+ * Parse a model string "provider/model" into a ModelSelection object
282
+ */
283
+ export function parseModelString(
284
+ modelString?: string
285
+ ): ModelSelection | undefined {
286
+ if (!modelString) return undefined;
287
+ const parts = modelString.split("/");
288
+ if (parts.length < 2) return undefined;
289
+ return {
290
+ provider: parts[0],
291
+ model: parts.slice(1).join("/"),
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Expand ~ to home directory
297
+ */
298
+ export function expandPath(path: string): string {
299
+ if (path.startsWith("~/")) {
300
+ return join(homedir(), path.slice(2));
301
+ }
302
+ return path;
303
+ }
304
+
305
+ /**
306
+ * Check if a file should be ignored based on patterns
307
+ */
308
+ export function shouldIgnore(filename: string, patterns: string[]): boolean {
309
+ for (const pattern of patterns) {
310
+ // Simple glob matching for common patterns
311
+ if (pattern.startsWith("*.")) {
312
+ const ext = pattern.slice(1);
313
+ if (filename.endsWith(ext)) {
314
+ return true;
315
+ }
316
+ } else if (filename === pattern) {
317
+ return true;
318
+ }
319
+ }
320
+ return false;
321
+ }
322
+
323
+ /**
324
+ * Get default config
325
+ */
326
+ export function getDefaultConfig(): TidyConfig {
327
+ return { ...DEFAULT_CONFIG };
328
+ }
329
+
330
+ /**
331
+ * Get default rules
332
+ */
333
+ export function getDefaultRules(): string {
334
+ return DEFAULT_RULES;
335
+ }