filemayor 2.0.5 → 3.6.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/README.md +48 -43
- package/core/ai/planner.js +42 -0
- package/core/ai/sentry.js +92 -0
- package/core/ai/strategist.js +186 -0
- package/core/ai/validator.js +116 -0
- package/core/analyzer.js +163 -0
- package/core/emergency-halt.js +104 -0
- package/core/engine/apply-engine.js +69 -0
- package/core/engine/cure-engine.js +70 -0
- package/core/engine/dedupe-engine.js +77 -0
- package/core/engine/explain-engine.js +114 -0
- package/core/engine/preview-engine.js +49 -0
- package/core/fs-abstraction.js +199 -0
- package/core/guardrail.js +115 -0
- package/core/index.js +56 -0
- package/core/intent-interpreter.js +158 -0
- package/core/jailer.js +151 -0
- package/core/license.js +12 -13
- package/core/metadata-store.js +104 -0
- package/core/organizer.js +28 -142
- package/core/reporter.js +212 -2
- package/core/scanner.js +92 -62
- package/core/security.js +65 -12
- package/core/sop-parser.js +1 -2
- package/core/vault.js +165 -0
- package/index.js +230 -2
- package/package.json +55 -55
package/README.md
CHANGED
|
@@ -1,84 +1,89 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Hrypopo/filemayor-landing/main/logo.png" width="128" height="128" alt="FileMayor logo" />
|
|
2
3
|
|
|
3
4
|
# FileMayor
|
|
5
|
+
### The intelligent, local-first engine for filesystem order.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
`v3.0.0` · Windows · macOS · Linux · Node ≥18 · **Zero Dependencies**
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
[Website](https://filemayor.com) · [npm](https://www.npmjs.com/package/filemayor) · [Safety Report](#security)
|
|
8
10
|
|
|
9
11
|
</div>
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
15
|
+
## Why FileMayor?
|
|
16
|
+
|
|
17
|
+
Hacker News skeptics, we hear you. Why not `du`, `find`, or `ncdu`?
|
|
18
|
+
|
|
19
|
+
Unix tools are powerful but dangerous and manual. **FileMayor** provides a middle ground: **Recursive context-aware intelligence** paired with **Psychological Safety**.
|
|
20
|
+
|
|
21
|
+
- **Intelligence**: Don't just list files; analyze bloat, find duplicates across names, and auto-categorize into 12 smart categories.
|
|
22
|
+
- **Safety**: Every move is journaled. Every destructive action is dry-run by default. Every mistake is reversible.
|
|
23
|
+
|
|
13
24
|
## Install
|
|
14
25
|
|
|
15
26
|
```bash
|
|
16
27
|
npm install -g filemayor
|
|
17
28
|
```
|
|
18
29
|
|
|
19
|
-
##
|
|
30
|
+
## Quick Start
|
|
20
31
|
|
|
21
32
|
```bash
|
|
22
|
-
|
|
23
|
-
filemayor
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
filemayor
|
|
27
|
-
filemayor undo ~/Downloads # Undo last organization
|
|
28
|
-
filemayor init # Create config file
|
|
29
|
-
filemayor info # System info + version
|
|
30
|
-
```
|
|
33
|
+
# Get deep insights into your disk bloat and duplicate waste
|
|
34
|
+
filemayor analyze ~/Downloads
|
|
35
|
+
|
|
36
|
+
# Preview a massive organization change without touching a bit
|
|
37
|
+
filemayor organize ~/Downloads --dry-run
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
# Reclaim space by nuking system junk and temporary rot
|
|
40
|
+
filemayor clean /var/tmp --yes
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| `organize` | Auto-sort files into 12 categories with undo journal |
|
|
38
|
-
| `clean` | Detect and remove temp, cache, logs, system junk |
|
|
39
|
-
| `watch` | Monitor folders in real-time with rules engine |
|
|
40
|
-
| `undo` | Rollback any organization safely |
|
|
42
|
+
# Restore order if you change your mind
|
|
43
|
+
filemayor undo ~/Downloads
|
|
44
|
+
```
|
|
41
45
|
|
|
42
|
-
##
|
|
46
|
+
## Features
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
- **`analyze` (New)**: Deep intelligence. Fhash-based duplicate detection, top directory bloat mapping, and potential savings calculation.
|
|
49
|
+
- **`organize`**: Deterministic sorting into Documents, Images, Media, etc.
|
|
50
|
+
- **`clean`**: Multi-category junk removal (temp, cache, logs, system, dependencies).
|
|
51
|
+
- **`watch` (Pro)**: Real-time background organization using a high-performance rules engine.
|
|
52
|
+
- **`undo`**: 100% cryptographic rollback of any organizational operation.
|
|
45
53
|
|
|
46
|
-
##
|
|
54
|
+
## Security & Safety Protocols
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
We touch your files. We take that seriously.
|
|
57
|
+
|
|
58
|
+
1. **100% Local**: No telemetry. No cloud uploads. No API calls to remote servers. Your file names and structures never leave your RAM.
|
|
59
|
+
2. **Dry-Run by Default**: Organization commands require an explicit execution or a `--dry-run` preview.
|
|
60
|
+
3. **Rollback Journaling**: Every move is recorded in a local `.filemayor-journal.json`. The `undo` command uses this to restore your filesystem state perfectly.
|
|
61
|
+
4. **Supply-Chain Secured**: **Zero** runtime dependencies. No hidden npm vulnerabilities. No `node_modules` rot in production.
|
|
53
62
|
|
|
54
63
|
## Configuration
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
Control your engine with a simple `.filemayor.yml`:
|
|
57
66
|
|
|
58
67
|
```yaml
|
|
59
68
|
organize:
|
|
60
|
-
naming: category_prefix # original | category_prefix | date_prefix
|
|
61
|
-
duplicates: rename # rename | skip | overwrite
|
|
62
|
-
ignore: [node_modules, .git, dist]
|
|
69
|
+
naming: category_prefix # options: original | category_prefix | date_prefix | clean
|
|
70
|
+
duplicates: rename # options: rename | skip | overwrite
|
|
71
|
+
ignore: [node_modules, .git, dist, build]
|
|
63
72
|
|
|
64
73
|
watch:
|
|
65
74
|
directories: [~/Downloads]
|
|
66
75
|
rules:
|
|
67
76
|
- match: "*.pdf"
|
|
68
77
|
action: move
|
|
69
|
-
dest: ~/Documents/
|
|
78
|
+
dest: ~/Documents/Finance
|
|
70
79
|
```
|
|
71
80
|
|
|
72
|
-
##
|
|
73
|
-
|
|
74
|
-
- All processing is **100% local** — no data leaves your machine
|
|
75
|
-
- Path traversal protection on all operations
|
|
76
|
-
- System directory safeguards (won't touch OS files)
|
|
77
|
-
- Zero runtime dependencies
|
|
78
|
-
|
|
79
|
-
## License
|
|
81
|
+
## Pricing
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
FileMayor is built by builders for builders.
|
|
84
|
+
- **Free**: Core scanning, organization, cleaning, and undo.
|
|
85
|
+
- **Pro**: Real-time Watch mode, AI-powered SOP parsing, and Bulk processing.
|
|
86
|
+
- **Activation**: `filemayor license activate FM-PRO-XXXX`
|
|
82
87
|
|
|
83
88
|
---
|
|
84
89
|
|
|
@@ -86,6 +91,6 @@ Proprietary — see [LICENSE](LICENSE) for details.
|
|
|
86
91
|
|
|
87
92
|
Created by **Lehlohonolo Goodwill Nchefu (Chevza)**
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
Built for the builders who refuse to let digital rot win.
|
|
90
95
|
|
|
91
96
|
</div>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
3
|
+
* FILEMAYOR AGENTIC CREW — CURATIVE PLANNER
|
|
4
|
+
* The Generative Brain (Gemini). Orchestrates the move plan.
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const IntentInterpreter = require('../intent-interpreter');
|
|
11
|
+
|
|
12
|
+
class CurativePlanner {
|
|
13
|
+
constructor(apiKey) {
|
|
14
|
+
this.interpreter = new IntentInterpreter(apiKey);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a Curative Plan using AI logic
|
|
19
|
+
* @param {Object} intent - From Strategist
|
|
20
|
+
* @param {Object} sentryData - From MetadataSentry
|
|
21
|
+
* @param {string} rawPrompt - Original user input
|
|
22
|
+
* @returns {Promise<Object>} Curative Plan
|
|
23
|
+
*/
|
|
24
|
+
async plan(intent, sentryData, rawPrompt) {
|
|
25
|
+
// v3.2 Intuition: Pass enriched clusters with relational metadata
|
|
26
|
+
const context = {
|
|
27
|
+
intent: intent.intent,
|
|
28
|
+
strategy: intent.strategy,
|
|
29
|
+
isRefine: intent.isRefine,
|
|
30
|
+
cwd: process.cwd()
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Standardized Bridge: Telemetry is passed as clusters
|
|
34
|
+
return await this.interpreter.interpret(
|
|
35
|
+
rawPrompt,
|
|
36
|
+
sentryData.clusters,
|
|
37
|
+
context
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = CurativePlanner;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
3
|
+
* FILEMAYOR AGENTIC CREW — METADATA SENTRY
|
|
4
|
+
* Responsible for scanning, clustering, and summarizing filesystem state.
|
|
5
|
+
* Reduces token cost by 90% via representative sampling.
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
class MetadataSentry {
|
|
15
|
+
/**
|
|
16
|
+
* Clustered Analysis: Group files by category and provided samples
|
|
17
|
+
* Now includes v3.2 Intuition metadata (Ancestry & Bundles)
|
|
18
|
+
* @param {Object[]} files - Raw FileInfo objects from scanner
|
|
19
|
+
* @returns {Object} Summarized perspective for the planner
|
|
20
|
+
*/
|
|
21
|
+
analyze(files) {
|
|
22
|
+
const clusters = {};
|
|
23
|
+
const bundles = new Map(); // UUID map for Atomic Bundles
|
|
24
|
+
let totalBytes = 0;
|
|
25
|
+
|
|
26
|
+
// Pass 1: Dependency Mapping (Detect Anchors)
|
|
27
|
+
const ANCHORS = ['.git', '.als', '.flp', '.xcodeproj', 'package.json', 'index.js', 'project.json'];
|
|
28
|
+
|
|
29
|
+
for (const f of files) {
|
|
30
|
+
if (!f.path) continue; // Skip files without a path
|
|
31
|
+
const fileName = path.basename(f.path).toLowerCase();
|
|
32
|
+
if (ANCHORS.includes(fileName) || ANCHORS.some(a => fileName.endsWith(a))) {
|
|
33
|
+
const bundlePath = path.dirname(f.path);
|
|
34
|
+
const bundleId = crypto.randomUUID();
|
|
35
|
+
bundles.set(bundlePath, bundleId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pass 2: Analysis & Contextual Tagging
|
|
40
|
+
for (const f of files) {
|
|
41
|
+
const cat = f.category || 'other';
|
|
42
|
+
if (!clusters[cat]) {
|
|
43
|
+
clusters[cat] = {
|
|
44
|
+
count: 0,
|
|
45
|
+
totalSize: 0,
|
|
46
|
+
samples: []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
clusters[cat].count++;
|
|
51
|
+
clusters[cat].totalSize += f.size || 0;
|
|
52
|
+
totalBytes += f.size || 0;
|
|
53
|
+
|
|
54
|
+
// v3.2 Intuition: Build Ancestry (Depth 3)
|
|
55
|
+
const filePath = f.path || f.name || '';
|
|
56
|
+
const parts = filePath.split(/[\\/]/);
|
|
57
|
+
const ancestry = parts.length > 4 ? parts.slice(-4, -1) : parts.slice(0, -1);
|
|
58
|
+
|
|
59
|
+
// v3.2 Intuition: Resolve Bundle affinity
|
|
60
|
+
let bundleId = null;
|
|
61
|
+
for (const [bPath, bId] of bundles.entries()) {
|
|
62
|
+
if (filePath.startsWith(bPath)) {
|
|
63
|
+
bundleId = bId;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Keep only a few samples per category to save tokens
|
|
69
|
+
if (clusters[cat].samples.length < 5) {
|
|
70
|
+
clusters[cat].samples.push({
|
|
71
|
+
name: f.name,
|
|
72
|
+
path: f.path,
|
|
73
|
+
ext: path.extname(f.name),
|
|
74
|
+
ancestry,
|
|
75
|
+
bundleId
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
summary: {
|
|
82
|
+
totalFiles: files.length,
|
|
83
|
+
totalSize: totalBytes,
|
|
84
|
+
categories: Object.keys(clusters),
|
|
85
|
+
bundleCount: bundles.size
|
|
86
|
+
},
|
|
87
|
+
clusters
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = MetadataSentry;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
3
|
+
* FILEMAYOR AGENTIC CREW — INTENT STRATEGIST
|
|
4
|
+
*
|
|
5
|
+
* Universal domain-aware intent classification.
|
|
6
|
+
* Detects folder "archetypes" from filenames, extensions, and
|
|
7
|
+
* directory names — then maps to the optimal organization strategy.
|
|
8
|
+
*
|
|
9
|
+
* Supports: Music Production, Civics/Law, Agri-Tech, Business,
|
|
10
|
+
* Technical/Code, Library, and General Archive.
|
|
11
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// ─── Domain Archetype Definitions ─────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const ARCHETYPES = {
|
|
21
|
+
production: {
|
|
22
|
+
label: 'Music Production',
|
|
23
|
+
extSignals: ['.wav', '.mp3', '.flac', '.als', '.flp', '.mid', '.aif', '.ogg', '.aac', '.stem'],
|
|
24
|
+
nameSignals: ['_v1', '_v2', '_final', '_master', '_mix', 'stem', 'beat', 'vocal', 'inst'],
|
|
25
|
+
strategy: 'atomic_bundle_preservation',
|
|
26
|
+
hierarchy: 'Project Name → Stems / Masters / Project Files'
|
|
27
|
+
},
|
|
28
|
+
civics: {
|
|
29
|
+
label: 'Civics & Law (RSA)',
|
|
30
|
+
extSignals: ['.pdf', '.docx', '.doc', '.odt', '.jpg', '.png'],
|
|
31
|
+
nameSignals: ['sars', 'gazette', 'court', 'affidavit', 'invoice', 'contract', 'act_',
|
|
32
|
+
'regulation', 'compliance', 'permit', 'certificate', 'agsa', 'pfma', 'sca',
|
|
33
|
+
'judgment', 'bill', 'irp5', 'it3a', 'green_paper', 'white_paper', 'audit_report',
|
|
34
|
+
'fica', 'cipc', 'bbbee', 'labour_', 'municipal'],
|
|
35
|
+
strategy: 'entity_year_doctype',
|
|
36
|
+
hierarchy: 'Authority → Year → Document Type',
|
|
37
|
+
folders: ['01_Legislation', '02_Compliance', '03_Oversight', '04_Judiciary']
|
|
38
|
+
},
|
|
39
|
+
agritech: {
|
|
40
|
+
label: 'Agri-Tech (Farm)',
|
|
41
|
+
extSignals: ['.csv', '.xlsx', '.xls', '.tsv'],
|
|
42
|
+
nameSignals: ['tag_', 'vet_', 'herd', 'cattle', 'livestock', 'feed', 'breed',
|
|
43
|
+
'pasture', 'dip_', 'auction', 'kraal', 'bovine', 'vaccination', 'fmd',
|
|
44
|
+
'brucella', 'deworming', 'weaner', 'heifer', 'bull_', 'calf_', 'stud_',
|
|
45
|
+
'transport_permit', 'movement_permit', 'feedlot'],
|
|
46
|
+
strategy: 'category_datestamp',
|
|
47
|
+
hierarchy: 'Livestock Category → Date-Stamped Logs',
|
|
48
|
+
folders: ['Inventory', 'Health', 'Logistics']
|
|
49
|
+
},
|
|
50
|
+
business: {
|
|
51
|
+
label: 'Business',
|
|
52
|
+
extSignals: ['.pdf', '.png', '.jpg', '.xlsx'],
|
|
53
|
+
nameSignals: ['invoice_', 'receipt', 'quotation', 'po_', 'statement', 'expense', 'tax_', 'vat', 'payslip', 'order_'],
|
|
54
|
+
strategy: 'quarter_vendor_status',
|
|
55
|
+
hierarchy: 'Quarter → Vendor → Status'
|
|
56
|
+
},
|
|
57
|
+
technical: {
|
|
58
|
+
label: 'Technical / Code',
|
|
59
|
+
extSignals: ['.js', '.ts', '.py', '.go', '.rs', '.java', '.cpp', '.c', '.h', '.json', '.yaml', '.yml', '.toml', '.env', '.sh'],
|
|
60
|
+
nameSignals: ['config', 'test_', '_spec', 'build', 'deploy', 'docker', 'readme', 'license', 'makefile'],
|
|
61
|
+
strategy: 'substrate_preservation',
|
|
62
|
+
hierarchy: 'Project Root → src / tests / config / docs'
|
|
63
|
+
},
|
|
64
|
+
library: {
|
|
65
|
+
label: 'Library / Reference',
|
|
66
|
+
extSignals: ['.pdf', '.epub', '.mobi', '.djvu'],
|
|
67
|
+
nameSignals: ['book', 'chapter', 'isbn', 'edition', 'vol_', 'author'],
|
|
68
|
+
strategy: 'ancestry_matching',
|
|
69
|
+
hierarchy: 'Genre / Subject → Author → Title'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
class IntentStrategist {
|
|
74
|
+
/**
|
|
75
|
+
* Classify user intent from a prompt string.
|
|
76
|
+
* Uses keyword matching for fast, local classification.
|
|
77
|
+
*/
|
|
78
|
+
classify(prompt, context = {}) {
|
|
79
|
+
const p = prompt.toLowerCase();
|
|
80
|
+
let strategy = 'clustering';
|
|
81
|
+
let intent = 'organize';
|
|
82
|
+
let archetype = null;
|
|
83
|
+
|
|
84
|
+
const isRefine = p.includes('refine') || p.includes('structure') || p.includes('within');
|
|
85
|
+
|
|
86
|
+
if (p.includes('clean') || p.includes('space') || p.includes('junk')) {
|
|
87
|
+
intent = 'cleanup';
|
|
88
|
+
strategy = 'retention';
|
|
89
|
+
} else if (p.includes('tax') || p.includes('invoice') || p.includes('receipt')) {
|
|
90
|
+
intent = 'financial_sorting';
|
|
91
|
+
strategy = 'deep_scan';
|
|
92
|
+
archetype = 'business';
|
|
93
|
+
} else if (p.includes('music') || p.includes('stem') || p.includes('production') || p.includes('beat')) {
|
|
94
|
+
intent = 'music_production';
|
|
95
|
+
strategy = 'atomic_bundle_preservation';
|
|
96
|
+
archetype = 'production';
|
|
97
|
+
} else if (p.includes('book') || p.includes('library') || p.includes('read')) {
|
|
98
|
+
intent = 'library_curation';
|
|
99
|
+
strategy = 'ancestry_matching';
|
|
100
|
+
archetype = 'library';
|
|
101
|
+
} else if (p.includes('farm') || p.includes('cattle') || p.includes('herd') || p.includes('livestock')) {
|
|
102
|
+
intent = 'agritech_management';
|
|
103
|
+
strategy = 'category_datestamp';
|
|
104
|
+
archetype = 'agritech';
|
|
105
|
+
} else if (p.includes('law') || p.includes('legal') || p.includes('court') || p.includes('compliance')) {
|
|
106
|
+
intent = 'civics_filing';
|
|
107
|
+
strategy = 'entity_year_doctype';
|
|
108
|
+
archetype = 'civics';
|
|
109
|
+
} else if (p.includes('code') || p.includes('project') || p.includes('repo')) {
|
|
110
|
+
intent = 'codebase_org';
|
|
111
|
+
strategy = 'substrate_preservation';
|
|
112
|
+
archetype = 'technical';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
intent,
|
|
117
|
+
strategy,
|
|
118
|
+
isRefine,
|
|
119
|
+
archetype: archetype ? ARCHETYPES[archetype] : null,
|
|
120
|
+
rules: {
|
|
121
|
+
lockBundles: true,
|
|
122
|
+
respectAncestry: true,
|
|
123
|
+
depthLimit: 3
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Detect the folder archetype from file metadata (zero-content).
|
|
130
|
+
* Scores each archetype by counting matching extension and name signals.
|
|
131
|
+
* Includes "Parental Hint" — the directory name itself influences scoring.
|
|
132
|
+
*
|
|
133
|
+
* @param {Array} files - Array of { name, ext?, size? }
|
|
134
|
+
* @param {string} [folderName] - Name of the parent directory (parental hint)
|
|
135
|
+
* @returns {{ archetype: string, label: string, confidence: number, strategy: string }}
|
|
136
|
+
*/
|
|
137
|
+
detectArchetype(files, folderName = '') {
|
|
138
|
+
const scores = {};
|
|
139
|
+
const folderHint = (folderName || '').toLowerCase();
|
|
140
|
+
|
|
141
|
+
for (const [key, arch] of Object.entries(ARCHETYPES)) {
|
|
142
|
+
let score = 0;
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const name = (file.name || '').toLowerCase();
|
|
146
|
+
const ext = (file.ext || path.extname(name)).toLowerCase();
|
|
147
|
+
|
|
148
|
+
// Extension match
|
|
149
|
+
if (arch.extSignals.includes(ext)) score += 2;
|
|
150
|
+
|
|
151
|
+
// Name signal match
|
|
152
|
+
for (const signal of arch.nameSignals) {
|
|
153
|
+
if (name.includes(signal)) score += 3;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Parental hint bonus (directory name contains archetype keywords)
|
|
158
|
+
for (const signal of arch.nameSignals) {
|
|
159
|
+
if (folderHint.includes(signal)) score += 5;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
scores[key] = score;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Find the winner
|
|
166
|
+
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
167
|
+
const [topKey, topScore] = sorted[0];
|
|
168
|
+
const totalSignals = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
169
|
+
const confidence = totalSignals > 0 ? Math.round((topScore / totalSignals) * 100) : 0;
|
|
170
|
+
|
|
171
|
+
if (topScore === 0) {
|
|
172
|
+
return { archetype: 'general', label: 'General / Mixed', confidence: 0, strategy: 'clustering' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const arch = ARCHETYPES[topKey];
|
|
176
|
+
return {
|
|
177
|
+
archetype: topKey,
|
|
178
|
+
label: arch.label,
|
|
179
|
+
confidence,
|
|
180
|
+
strategy: arch.strategy,
|
|
181
|
+
hierarchy: arch.hierarchy
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = IntentStrategist;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
3
|
+
* FILEMAYOR AGENTIC CREW — SECURITY ARCHITECT
|
|
4
|
+
* Deterministic validator. Never trusts AI output.
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { isFileSafe, validatePath } = require('../security');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
class SecurityArchitect {
|
|
14
|
+
constructor() {
|
|
15
|
+
// Dynamically resolve home directory parts for path normalization
|
|
16
|
+
const homeParts = os.homedir().toLowerCase().replace(/\\/g, '/').split('/').filter(x => x.length > 0);
|
|
17
|
+
this._homePrefixParts = new Set(homeParts); // e.g. Set{'c:', 'users', 'hloni'}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Final validation of an AI-generated plan
|
|
22
|
+
* @param {Object} plan - The Curative Plan from Planner
|
|
23
|
+
* @param {Object} sentryData - The ground-truth telemetry
|
|
24
|
+
* @returns {Object} Validated/Sanitized Plan
|
|
25
|
+
*/
|
|
26
|
+
validate(plan, sentryData) {
|
|
27
|
+
if (!plan || !Array.isArray(plan.plan)) {
|
|
28
|
+
return { ...plan, plan: [], status: 'invalid' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const domainTriggerFolders = ['books', 'library', 'projects', 'music', 'work', 'downloads'];
|
|
32
|
+
const clusters = sentryData?.clusters || {};
|
|
33
|
+
const allSamples = Object.values(clusters).flatMap(c => c.samples || []);
|
|
34
|
+
const homeParts = this._homePrefixParts;
|
|
35
|
+
|
|
36
|
+
// Robust Normalizer: strips drive letter and home dir parts, leaving only domain-relevant segments
|
|
37
|
+
const normalize = (p) => {
|
|
38
|
+
if (!p) return [];
|
|
39
|
+
return p.toLowerCase()
|
|
40
|
+
.replace(/\\/g, '/')
|
|
41
|
+
.split('/')
|
|
42
|
+
.filter(x => x.length > 0 && !homeParts.has(x));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const safeSteps = plan.plan.filter(step => {
|
|
46
|
+
try {
|
|
47
|
+
if (!step.source || !step.destination) return false;
|
|
48
|
+
|
|
49
|
+
const srcCheck = isFileSafe(step.source);
|
|
50
|
+
if (!srcCheck.safe) return false;
|
|
51
|
+
|
|
52
|
+
// 1. Domain Preservation (Rule of Ancestry)
|
|
53
|
+
const srcParts = normalize(step.source);
|
|
54
|
+
const destParts = normalize(step.destination);
|
|
55
|
+
|
|
56
|
+
const triggerFound = srcParts.find(p => domainTriggerFolders.includes(p));
|
|
57
|
+
|
|
58
|
+
if (triggerFound) {
|
|
59
|
+
// A move is valid if the EXACT trigger folder is present in the destination path
|
|
60
|
+
const preserved = destParts.includes(triggerFound);
|
|
61
|
+
|
|
62
|
+
if (!preserved && !step.reason.includes('COLLECTION_MOVE')) {
|
|
63
|
+
console.warn(`[ARCHITECTURE] Blocked domain scattering for ${step.source} -> ${step.destination}`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Atomic Bundle Lock
|
|
69
|
+
const sourceMeta = allSamples.find(s => s.path === step.source);
|
|
70
|
+
if (sourceMeta && sourceMeta.bundleId) {
|
|
71
|
+
const bundleId = sourceMeta.bundleId;
|
|
72
|
+
const bundleMembers = allSamples.filter(s => s.bundleId === bundleId);
|
|
73
|
+
const planMembers = plan.plan.filter(s => {
|
|
74
|
+
const meta = allSamples.find(m => m.path === s.source);
|
|
75
|
+
return meta && meta.bundleId === bundleId;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// If not all members are moving, it's a split.
|
|
79
|
+
if (planMembers.length < bundleMembers.length) {
|
|
80
|
+
console.warn(`[ARCHITECTURE] Blocked bundle split for ${step.source} (Bundle: ${bundleId})`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Ensure all members are going to the same parent domain
|
|
85
|
+
const firstDestParts = normalize(planMembers[0].destination);
|
|
86
|
+
const bundleDestRoot = firstDestParts.slice(0, -1).join('/');
|
|
87
|
+
|
|
88
|
+
const matches = planMembers.every(s => {
|
|
89
|
+
const dParts = normalize(s.destination);
|
|
90
|
+
const dRoot = dParts.slice(0, -1).join('/');
|
|
91
|
+
return dRoot === bundleDestRoot;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!matches) {
|
|
95
|
+
console.warn(`[ARCHITECTURE] Blocked bundle scattering for ${step.source}`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[ARCHITECTURE] Validation Error:', err);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...plan,
|
|
109
|
+
plan: safeSteps,
|
|
110
|
+
validatedCount: safeSteps.length,
|
|
111
|
+
status: safeSteps.length > 0 ? 'safe' : 'blocked'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = SecurityArchitect;
|