filemayor 4.0.7 → 4.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/core/ai/planner.js +13 -2
- package/core/ai/strategist.js +15 -1
- package/core/ai/validator.js +9 -3
- package/core/cleaner.js +19 -14
- package/core/config.js +63 -2
- package/core/engine/dedupe-engine.js +111 -25
- package/core/engine/para-engine.js +370 -0
- package/core/fs-abstraction.js +147 -54
- package/core/index.js +2 -0
- package/core/watcher.js +85 -7
- package/index.js +152 -22
- package/package.json +1 -1
package/core/index.js
CHANGED
|
@@ -27,6 +27,7 @@ const CureEngine = require('./engine/cure-engine');
|
|
|
27
27
|
const ApplyEngine = require('./engine/apply-engine');
|
|
28
28
|
const PreviewEngine = require('./engine/preview-engine');
|
|
29
29
|
const DedupeEngine = require('./engine/dedupe-engine');
|
|
30
|
+
const PARAEngine = require('./engine/para-engine');
|
|
30
31
|
const MetadataStore = require('./metadata-store');
|
|
31
32
|
const strategist = require('./ai/strategist');
|
|
32
33
|
const sentry = require('./ai/sentry');
|
|
@@ -119,6 +120,7 @@ module.exports = {
|
|
|
119
120
|
ApplyEngine,
|
|
120
121
|
PreviewEngine,
|
|
121
122
|
DedupeEngine,
|
|
123
|
+
PARAEngine,
|
|
122
124
|
MetadataStore,
|
|
123
125
|
|
|
124
126
|
// Hardened Runtime
|
package/core/watcher.js
CHANGED
|
@@ -80,8 +80,15 @@ class FileWatcher extends EventEmitter {
|
|
|
80
80
|
maxLogEntries: options.maxLogEntries || 10000,
|
|
81
81
|
autoOrganize: options.autoOrganize || false,
|
|
82
82
|
organizeOptions: options.organizeOptions || {},
|
|
83
|
+
// PARA mode (v4.0.9): route each new file into Projects/Areas/
|
|
84
|
+
// Resources/Archives by actionability as it lands. Journaled.
|
|
85
|
+
paraMode: options.paraMode || false,
|
|
86
|
+
paraConfig: options.paraConfig || {},
|
|
83
87
|
};
|
|
84
88
|
|
|
89
|
+
this._fmfs = null; // lazy — shared journal session for this watcher
|
|
90
|
+
this._paraEngine = null; // lazy — built on first PARA routing
|
|
91
|
+
|
|
85
92
|
this._watchers = new Map();
|
|
86
93
|
this._debounceTimers = new Map();
|
|
87
94
|
this._running = false;
|
|
@@ -338,6 +345,11 @@ class FileWatcher extends EventEmitter {
|
|
|
338
345
|
break; // First match wins
|
|
339
346
|
}
|
|
340
347
|
|
|
348
|
+
// PARA mode — route the new file into its actionability bucket
|
|
349
|
+
if (this.options.paraMode && eventType === 'rename') {
|
|
350
|
+
this._routePara(fullPath, filename, watchDir, stats);
|
|
351
|
+
}
|
|
352
|
+
|
|
341
353
|
// Auto-organize if enabled
|
|
342
354
|
if (this.options.autoOrganize && eventType === 'rename') {
|
|
343
355
|
// A new file appeared — organize the directory
|
|
@@ -353,6 +365,65 @@ class FileWatcher extends EventEmitter {
|
|
|
353
365
|
}
|
|
354
366
|
}
|
|
355
367
|
|
|
368
|
+
/** Lazy shared FileMayorFS — one journal session per watcher run. */
|
|
369
|
+
_getFmfs() {
|
|
370
|
+
if (!this._fmfs) {
|
|
371
|
+
const FileMayorFS = require('./fs-abstraction');
|
|
372
|
+
this._fmfs = new FileMayorFS();
|
|
373
|
+
}
|
|
374
|
+
return this._fmfs;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* PARA mode: classify one new file by actionability and move it into
|
|
379
|
+
* Projects/Areas/Resources/Archives. Skips files already inside a
|
|
380
|
+
* bucket (no loops — the move itself fires another watch event).
|
|
381
|
+
*/
|
|
382
|
+
_routePara(fullPath, filename, watchDir, stats) {
|
|
383
|
+
try {
|
|
384
|
+
if (!this._paraEngine) {
|
|
385
|
+
const PARAEngine = require('./engine/para-engine');
|
|
386
|
+
this._paraEngine = new PARAEngine(this.options.paraConfig);
|
|
387
|
+
}
|
|
388
|
+
const engine = this._paraEngine;
|
|
389
|
+
const folders = engine.config.folders;
|
|
390
|
+
|
|
391
|
+
const rel = path.relative(watchDir, fullPath);
|
|
392
|
+
const firstSegment = rel.split(path.sep)[0].toLowerCase();
|
|
393
|
+
const bucketNames = new Set(Object.values(folders).map(f => f.toLowerCase()));
|
|
394
|
+
if (bucketNames.has(firstSegment)) return; // already sorted
|
|
395
|
+
|
|
396
|
+
const fileInfo = {
|
|
397
|
+
name: filename,
|
|
398
|
+
path: fullPath,
|
|
399
|
+
category: categorize(path.extname(filename).toLowerCase()),
|
|
400
|
+
size: stats.size,
|
|
401
|
+
modified: stats.mtime,
|
|
402
|
+
};
|
|
403
|
+
const { bucket, reason } = engine._classify(fileInfo, rel, Date.now());
|
|
404
|
+
const destination = path.join(watchDir, folders[bucket], filename);
|
|
405
|
+
|
|
406
|
+
this._getFmfs().move(fullPath, destination)
|
|
407
|
+
.then(() => {
|
|
408
|
+
this._stats.filesActioned++;
|
|
409
|
+
this.emit('action', {
|
|
410
|
+
action: 'para-route',
|
|
411
|
+
source: fullPath,
|
|
412
|
+
destination,
|
|
413
|
+
bucket,
|
|
414
|
+
reason,
|
|
415
|
+
});
|
|
416
|
+
})
|
|
417
|
+
.catch((err) => {
|
|
418
|
+
this._stats.errors++;
|
|
419
|
+
this.emit('error', { action: 'para-route', path: fullPath, error: err.message });
|
|
420
|
+
});
|
|
421
|
+
} catch (err) {
|
|
422
|
+
this._stats.errors++;
|
|
423
|
+
this.emit('error', { action: 'para-route', path: fullPath, error: err.message });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
356
427
|
/**
|
|
357
428
|
* Execute a matched rule
|
|
358
429
|
*/
|
|
@@ -400,13 +471,20 @@ class FileWatcher extends EventEmitter {
|
|
|
400
471
|
break;
|
|
401
472
|
|
|
402
473
|
case 'delete':
|
|
403
|
-
|
|
404
|
-
this.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
474
|
+
// Trash-based (v4.0.9): journaled and reversible with undo.
|
|
475
|
+
this._getFmfs().delete(event.path)
|
|
476
|
+
.then(() => {
|
|
477
|
+
this._stats.filesActioned++;
|
|
478
|
+
this.emit('action', {
|
|
479
|
+
action: 'delete',
|
|
480
|
+
source: event.path,
|
|
481
|
+
rule
|
|
482
|
+
});
|
|
483
|
+
})
|
|
484
|
+
.catch((err) => {
|
|
485
|
+
this._stats.errors++;
|
|
486
|
+
this.emit('error', { action: 'delete', path: event.path, error: err.message, rule });
|
|
487
|
+
});
|
|
410
488
|
break;
|
|
411
489
|
|
|
412
490
|
case 'log':
|
package/index.js
CHANGED
|
@@ -51,13 +51,14 @@ const { formatBytes } = require('./core/scanner');
|
|
|
51
51
|
const { getCategories } = require('./core/categories');
|
|
52
52
|
const { checkPermissions } = require('./core/security');
|
|
53
53
|
const { activateLicense, deactivateLicense, getLicenseInfo, checkProFeature, checkBulkLimit } = require('./core/license');
|
|
54
|
-
const { ExplainEngine, CureEngine, ApplyEngine, DedupeEngine } = require('./core/index');
|
|
54
|
+
const { ExplainEngine, CureEngine, ApplyEngine, DedupeEngine, PARAEngine } = require('./core/index');
|
|
55
55
|
const { ping: telemetryPing } = require('./core/telemetry');
|
|
56
56
|
|
|
57
57
|
const { c, banner, Spinner, success, error, warn, info } = reporter;
|
|
58
58
|
|
|
59
59
|
// ─── Version ──────────────────────────────────────────────────────
|
|
60
|
-
|
|
60
|
+
// Derived from package.json so it can never drift out of sync.
|
|
61
|
+
const VERSION = require('./package.json').version;
|
|
61
62
|
|
|
62
63
|
// ─── Path Helpers ─────────────────────────────────────────────────
|
|
63
64
|
|
|
@@ -117,6 +118,10 @@ function parseArgs(argv) {
|
|
|
117
118
|
args.flags.depth = parseInt(raw[++i], 10);
|
|
118
119
|
} else if (arg === '--naming' && raw[i + 1]) {
|
|
119
120
|
args.flags.naming = raw[++i];
|
|
121
|
+
} else if (arg === '--archive-after' && raw[i + 1]) {
|
|
122
|
+
args.flags.archiveAfter = parseInt(raw[++i], 10);
|
|
123
|
+
} else if (arg === '--para') {
|
|
124
|
+
args.flags.para = true;
|
|
120
125
|
} else if (arg === '--output' && raw[i + 1]) {
|
|
121
126
|
args.flags.outputDir = raw[++i];
|
|
122
127
|
} else if (arg === '--config' && raw[i + 1]) {
|
|
@@ -173,19 +178,20 @@ ${banner()}
|
|
|
173
178
|
${c('cyan', 'filemayor')} ${c('yellow', '<command>')} ${c('dim', '[path]')} ${c('dim', '[options]')}
|
|
174
179
|
|
|
175
180
|
${c('yellow', 'explain')} ${c('dim', '<path>')} Diagnose folder health
|
|
176
|
-
${c('yellow', 'cure')} ${c('dim', '<path>')} Plan treatment using AI
|
|
181
|
+
${c('yellow', 'cure')} ${c('dim', '<path>')} Plan treatment using AI (--para for PARA buckets)
|
|
177
182
|
${c('yellow', 'apply')} Execute the cure plan
|
|
178
|
-
${c('yellow', 'duplicates')} ${c('dim', '<path>')} Find
|
|
179
|
-
${c('yellow', 'dedupe')} ${c('dim', '<path>')}
|
|
183
|
+
${c('yellow', 'duplicates')} ${c('dim', '<path>')} Find duplicates (content-hash)
|
|
184
|
+
${c('yellow', 'dedupe')} ${c('dim', '<path>')} Trash duplicates (reversible with undo)
|
|
180
185
|
|
|
181
186
|
${c('bold', 'COMMANDS')}
|
|
182
187
|
${c('yellow', 'scan')} ${c('dim', '<path>')} Scan directory and report contents
|
|
183
188
|
${c('yellow', 'analyze')} ${c('dim', '<path>')} Deep analysis (duplicates, bloat, savings)
|
|
184
189
|
${c('yellow', 'organize')} ${c('dim', '<path>')} Organize files into categories
|
|
190
|
+
${c('yellow', 'para')} ${c('dim', '<path>')} Sort by actionability — PARA method (then apply)
|
|
185
191
|
${c('yellow', 'clean')} ${c('dim', '<path>')} Find and remove junk files
|
|
186
|
-
${c('yellow', 'watch')} ${c('dim', '<path>')} Watch directory
|
|
187
|
-
${c('yellow', 'init')}
|
|
188
|
-
${c('yellow', 'undo')}
|
|
192
|
+
${c('yellow', 'watch')} ${c('dim', '<path>')} Watch directory (--para: auto-route new files)
|
|
193
|
+
${c('yellow', 'init')} ${c('dim', '[--para]')} Create .filemayor.yml (--para scaffolds PARA folders)
|
|
194
|
+
${c('yellow', 'undo')} Undo last session (--list · --session <id> · --all)
|
|
189
195
|
${c('yellow', 'info')} System info and version
|
|
190
196
|
${c('yellow', 'license')} ${c('dim', '<action>')} Manage license (activate|status|deactivate)
|
|
191
197
|
|
|
@@ -201,6 +207,8 @@ ${banner()}
|
|
|
201
207
|
${c('dim', '--config <path>')} Custom config file
|
|
202
208
|
${c('dim', '--depth <n>')} Max recursion depth
|
|
203
209
|
${c('dim', '--naming <type>')} Naming: original|category_prefix|date_prefix|clean
|
|
210
|
+
${c('dim', '--archive-after <n>')} PARA: months of inactivity before a file → Archives
|
|
211
|
+
${c('dim', '--para')} init: scaffold PARA folders + preset config
|
|
204
212
|
${c('dim', '--duplicates <s>')} Duplicates: rename|skip|overwrite
|
|
205
213
|
${c('dim', '--extensions <e>')} Filter by extensions (comma-separated)
|
|
206
214
|
${c('dim', '--output <path>')} Output directory for organized files
|
|
@@ -214,6 +222,8 @@ ${banner()}
|
|
|
214
222
|
${c('dim', '$')} filemayor scan ~/Downloads
|
|
215
223
|
${c('dim', '$')} filemayor organize ~/Downloads --dry-run
|
|
216
224
|
${c('dim', '$')} filemayor organize ~/Downloads --naming category_prefix
|
|
225
|
+
${c('dim', '$')} filemayor init --para ~/Documents
|
|
226
|
+
${c('dim', '$')} filemayor para ~/Documents --archive-after 18
|
|
217
227
|
${c('dim', '$')} filemayor clean /var/tmp --yes
|
|
218
228
|
${c('dim', '$')} filemayor scan . --json | jq '.files | length'
|
|
219
229
|
${c('dim', '$')} filemayor watch ~/Downloads
|
|
@@ -372,7 +382,7 @@ async function cmdClean(target, flags, config) {
|
|
|
372
382
|
if (format === 'table' && !flags.quiet) spinner.start();
|
|
373
383
|
|
|
374
384
|
try {
|
|
375
|
-
const result = clean(targetPath, {
|
|
385
|
+
const result = await clean(targetPath, {
|
|
376
386
|
dryRun,
|
|
377
387
|
categories: flags.category || config.clean.categories,
|
|
378
388
|
maxDepth: flags.depth || config.clean.maxDepth,
|
|
@@ -414,8 +424,14 @@ async function cmdWatch(target, flags, config) {
|
|
|
414
424
|
naming: config.organize.naming,
|
|
415
425
|
duplicateStrategy: config.organize.duplicates,
|
|
416
426
|
},
|
|
427
|
+
paraMode: !!flags.para,
|
|
428
|
+
paraConfig: config.para || {},
|
|
417
429
|
});
|
|
418
430
|
|
|
431
|
+
if (flags.para) {
|
|
432
|
+
console.log(info('PARA mode: new files route to Projects / Areas / Resources / Archives (journaled — undo works).'));
|
|
433
|
+
}
|
|
434
|
+
|
|
419
435
|
watcher.on('watching', (data) => {
|
|
420
436
|
console.log(success(`Watching: ${data.directory}`));
|
|
421
437
|
});
|
|
@@ -454,6 +470,23 @@ async function cmdWatch(target, flags, config) {
|
|
|
454
470
|
|
|
455
471
|
async function cmdInit(flags) {
|
|
456
472
|
try {
|
|
473
|
+
if (flags.para) {
|
|
474
|
+
// PARA preset: write a tuned config AND scaffold the four folders
|
|
475
|
+
// with teaching READMEs so a first-timer understands the method.
|
|
476
|
+
const configPath = createConfigFile(process.cwd(), '.filemayor.yml', 'para');
|
|
477
|
+
console.log(success(`Created ${c('cyan', configPath)}`));
|
|
478
|
+
|
|
479
|
+
const engine = new PARAEngine();
|
|
480
|
+
const { created, folders } = engine.scaffold(process.cwd());
|
|
481
|
+
console.log(success(`Scaffolded PARA folders: ${c('cyan', Object.values(folders).join(', '))}`));
|
|
482
|
+
if (created.length) {
|
|
483
|
+
console.log(c('dim', ` ${created.length} item(s) created (folders + READMEs)`));
|
|
484
|
+
}
|
|
485
|
+
console.log('');
|
|
486
|
+
console.log(c('dim', ' Next: ') + c('cyan', 'filemayor para .') + c('dim', ' then ') + c('cyan', 'filemayor apply'));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
457
490
|
const configPath = createConfigFile(process.cwd());
|
|
458
491
|
console.log(success(`Created ${c('cyan', configPath)}`));
|
|
459
492
|
console.log(c('dim', ' Edit this file to customize FileMayor settings'));
|
|
@@ -463,24 +496,100 @@ async function cmdInit(flags) {
|
|
|
463
496
|
}
|
|
464
497
|
}
|
|
465
498
|
|
|
499
|
+
// ─── PARA: sort by actionability (Projects/Areas/Resources/Archives) ──
|
|
500
|
+
async function cmdPara(target, flags, config) {
|
|
501
|
+
const targetPath = path.resolve(target || '.');
|
|
502
|
+
|
|
503
|
+
// Per-run override for the archive threshold.
|
|
504
|
+
const paraConfig = { ...(config.para || {}) };
|
|
505
|
+
if (Number.isFinite(flags.archiveAfter)) {
|
|
506
|
+
paraConfig.archiveAfterMonths = flags.archiveAfter;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const spinner = new Spinner(`Sorting ${c('cyan', targetPath)} by actionability (PARA)...`);
|
|
510
|
+
if (!flags.quiet) spinner.start();
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const engine = new PARAEngine(paraConfig);
|
|
514
|
+
const result = engine.plan(targetPath);
|
|
515
|
+
spinner.stop();
|
|
516
|
+
|
|
517
|
+
if (flags.format === 'json') {
|
|
518
|
+
console.log(JSON.stringify(result, null, 2));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Cache the plan so `filemayor apply` can execute it (same path as cure).
|
|
523
|
+
const cachePath = path.join(os.tmpdir(), 'filemayor-plan.json');
|
|
524
|
+
fs.writeFileSync(cachePath, JSON.stringify(result), 'utf8');
|
|
525
|
+
|
|
526
|
+
console.log(reporter.formatCureReport(result));
|
|
527
|
+
|
|
528
|
+
// PARA-specific breakdown — the bucket is the whole point.
|
|
529
|
+
const b = result.summary.buckets;
|
|
530
|
+
console.log(c('bold', ' Into PARA buckets:'));
|
|
531
|
+
console.log(` ${c('cyan', 'Projects')} ${b.projects} ${c('cyan', 'Areas')} ${b.areas} ${c('cyan', 'Resources')} ${b.resources} ${c('cyan', 'Archives')} ${b.archives}`);
|
|
532
|
+
for (const wn of (result.warnings || [])) {
|
|
533
|
+
console.log(c('dim', ` ⚠ ${wn}`));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (result.plan.length === 0) {
|
|
537
|
+
console.log(info('Nothing to reorganise — run with a messier folder, or it is already PARA-sorted.'));
|
|
538
|
+
} else {
|
|
539
|
+
console.log('');
|
|
540
|
+
console.log(c('dim', ' Review the plan above, then run ') + c('cyan', 'filemayor apply') + c('dim', ' to execute (reversible with ') + c('cyan', 'filemayor undo') + c('dim', ').'));
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
spinner.fail(err.message);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
466
548
|
async function cmdUndo(target, flags) {
|
|
467
|
-
const { JOURNAL_PATH, rollback: fsRollback } = require('./core/fs-abstraction');
|
|
549
|
+
const { JOURNAL_PATH, rollback: fsRollback, listSessions } = require('./core/fs-abstraction');
|
|
468
550
|
|
|
469
551
|
if (!fs.existsSync(JOURNAL_PATH)) {
|
|
470
552
|
console.log(info('No operations to undo (journal is empty)'));
|
|
471
553
|
return;
|
|
472
554
|
}
|
|
473
555
|
|
|
474
|
-
|
|
556
|
+
// undo --list: show the undoable session history, newest first.
|
|
557
|
+
if (flags.list) {
|
|
558
|
+
const sessions = listSessions(JOURNAL_PATH);
|
|
559
|
+
if (sessions.length === 0) {
|
|
560
|
+
console.log(info('No undoable sessions — the journal has nothing left to reverse.'));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
console.log(c('bold', '\n UNDO HISTORY') + c('dim', ` — ${sessions.length} session${sessions.length === 1 ? '' : 's'}\n`));
|
|
564
|
+
for (const s of sessions) {
|
|
565
|
+
const when = s.end.replace('T', ' ').slice(0, 19);
|
|
566
|
+
const parts = [];
|
|
567
|
+
if (s.moves) parts.push(`${s.moves} move${s.moves === 1 ? '' : 's'}`);
|
|
568
|
+
if (s.deletes) parts.push(`${s.deletes} delete${s.deletes === 1 ? '' : 's'}`);
|
|
569
|
+
console.log(` ${c('cyan', s.sessionId.padEnd(10))} ${c('dim', when)} ${parts.join(', ')}`);
|
|
570
|
+
}
|
|
571
|
+
console.log(c('dim', "\n Reverse one: filemayor undo --session <id>"));
|
|
572
|
+
console.log(c('dim', ' Reverse all: filemayor undo --all\n'));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const scope = flags.session
|
|
577
|
+
? `session ${flags.session}`
|
|
578
|
+
: flags.all ? 'all sessions' : 'the most recent session';
|
|
579
|
+
const spinner = new Spinner(`Undoing ${scope}...`);
|
|
475
580
|
spinner.start();
|
|
476
581
|
|
|
477
582
|
try {
|
|
478
|
-
const result = await fsRollback(JOURNAL_PATH
|
|
583
|
+
const result = await fsRollback(JOURNAL_PATH, {
|
|
584
|
+
all: !!flags.all,
|
|
585
|
+
sessionId: typeof flags.session === 'string' ? flags.session : undefined,
|
|
586
|
+
});
|
|
479
587
|
spinner.stop();
|
|
480
588
|
if (result.count === 0) {
|
|
481
589
|
console.log(info('Nothing to undo — no completed operations found in the journal'));
|
|
482
590
|
} else {
|
|
483
|
-
|
|
591
|
+
const which = result.sessionId && !flags.all ? c('dim', ` (session ${result.sessionId})`) : '';
|
|
592
|
+
console.log(success(`Undone ${result.count} operation${result.count === 1 ? '' : 's'}`) + which);
|
|
484
593
|
}
|
|
485
594
|
} catch (err) {
|
|
486
595
|
spinner.fail(err.message);
|
|
@@ -510,18 +619,33 @@ async function cmdExplain(target, flags) {
|
|
|
510
619
|
|
|
511
620
|
async function cmdCure(target, flags) {
|
|
512
621
|
const targetPath = path.resolve(target || '.');
|
|
513
|
-
const prompt = flags.prompt ||
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
622
|
+
const prompt = flags.prompt || (flags.para
|
|
623
|
+
? 'Sort this folder by actionability using the PARA method (Projects, Areas, Resources, Archives).'
|
|
624
|
+
: 'Clean up this folder and group similar files.');
|
|
625
|
+
|
|
626
|
+
// Multi-provider (v4.0.9): first key present wins — Anthropic, Gemini,
|
|
627
|
+
// then OpenAI. With no key at all, the core planner falls back to the
|
|
628
|
+
// free FileMayor proxy, so there is nothing to hard-fail on here.
|
|
629
|
+
const providers = [
|
|
630
|
+
['ANTHROPIC_API_KEY', 'anthropic'],
|
|
631
|
+
['GEMINI_API_KEY', 'gemini'],
|
|
632
|
+
['OPENAI_API_KEY', 'openai'],
|
|
633
|
+
];
|
|
634
|
+
let apiKey = '';
|
|
635
|
+
for (const [envVar, provider] of providers) {
|
|
636
|
+
if (process.env[envVar]) {
|
|
637
|
+
process.env.FM_AI_PROVIDER = provider;
|
|
638
|
+
process.env.FM_AI_API_KEY = process.env[envVar];
|
|
639
|
+
apiKey = process.env[envVar];
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
518
642
|
}
|
|
519
643
|
|
|
520
644
|
const spinner = new Spinner(`Consulting AI for ${c('cyan', targetPath)}...`);
|
|
521
645
|
spinner.start();
|
|
522
646
|
|
|
523
647
|
try {
|
|
524
|
-
const engine = new CureEngine(targetPath,
|
|
648
|
+
const engine = new CureEngine(targetPath, apiKey);
|
|
525
649
|
activePlanState = await engine.generatePlan(prompt);
|
|
526
650
|
spinner.stop();
|
|
527
651
|
|
|
@@ -603,7 +727,7 @@ async function cmdDuplicates(target, flags) {
|
|
|
603
727
|
|
|
604
728
|
try {
|
|
605
729
|
const engine = new DedupeEngine();
|
|
606
|
-
const result = engine.find(targetPath);
|
|
730
|
+
const result = await engine.find(targetPath);
|
|
607
731
|
spinner.stop();
|
|
608
732
|
console.log(reporter.formatDedupeReport(result));
|
|
609
733
|
} catch (err) {
|
|
@@ -619,14 +743,15 @@ async function cmdDedupe(target, flags) {
|
|
|
619
743
|
|
|
620
744
|
try {
|
|
621
745
|
const engine = new DedupeEngine();
|
|
622
|
-
const report = engine.find(targetPath);
|
|
746
|
+
const report = await engine.find(targetPath);
|
|
623
747
|
if (report.sets === 0) {
|
|
624
748
|
spinner.succeed('Total order restored: No duplicates found.');
|
|
625
749
|
return;
|
|
626
750
|
}
|
|
627
751
|
|
|
628
752
|
const result = await engine.clean(report);
|
|
629
|
-
spinner.succeed(`
|
|
753
|
+
spinner.succeed(`Trashed ${result.deleted} duplicates. Reclaimed ${result.freedHuman}.`);
|
|
754
|
+
console.log(c('dim', ` Recoverable: files moved to FileMayor trash — 'filemayor undo' restores them.`));
|
|
630
755
|
} catch (err) {
|
|
631
756
|
spinner.fail(err.message);
|
|
632
757
|
process.exit(1);
|
|
@@ -837,6 +962,11 @@ async function main() {
|
|
|
837
962
|
await cmdOrganize(args.target, args.flags, config);
|
|
838
963
|
break;
|
|
839
964
|
|
|
965
|
+
case 'para':
|
|
966
|
+
case 'pm':
|
|
967
|
+
await cmdPara(args.target, args.flags, config);
|
|
968
|
+
break;
|
|
969
|
+
|
|
840
970
|
case 'clean':
|
|
841
971
|
case 'cl':
|
|
842
972
|
case 'c':
|