filemayor 2.1.0 → 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/core/index.js CHANGED
@@ -11,6 +11,8 @@
11
11
 
12
12
  const scanner = require('./scanner');
13
13
  const organizer = require('./organizer');
14
+ const IntentInterpreter = require('./intent-interpreter');
15
+ const FileMayorFS = require('./fs-abstraction');
14
16
  const cleaner = require('./cleaner');
15
17
  const watcher = require('./watcher');
16
18
  const config = require('./config');
@@ -18,6 +20,22 @@ const reporter = require('./reporter');
18
20
  const categories = require('./categories');
19
21
  const security = require('./security');
20
22
  const sopParser = require('./sop-parser');
23
+ const analyzer = require('./analyzer');
24
+ const license = require('./license');
25
+ const ExplainEngine = require('./engine/explain-engine');
26
+ const CureEngine = require('./engine/cure-engine');
27
+ const ApplyEngine = require('./engine/apply-engine');
28
+ const PreviewEngine = require('./engine/preview-engine');
29
+ const DedupeEngine = require('./engine/dedupe-engine');
30
+ const MetadataStore = require('./metadata-store');
31
+ const strategist = require('./ai/strategist');
32
+ const sentry = require('./ai/sentry');
33
+ const planner = require('./ai/planner');
34
+ const validator = require('./ai/validator');
35
+ const { FileMayorJailer, enforceUserSpace } = require('./jailer');
36
+ const Vault = require('./vault');
37
+ const LogicGuardrail = require('./guardrail');
38
+ const { initEmergencyHalt, updateJournalRef, clearEmergencyHalt } = require('./emergency-halt');
21
39
 
22
40
  module.exports = {
23
41
  // Scanner
@@ -26,6 +44,7 @@ module.exports = {
26
44
  scanByCategory: scanner.scanByCategory,
27
45
  scanSummary: scanner.scanSummary,
28
46
  formatBytes: scanner.formatBytes,
47
+ analyzeDirectory: analyzer.analyzeDirectory,
29
48
 
30
49
  // Organizer
31
50
  organize: organizer.organize,
@@ -35,6 +54,12 @@ module.exports = {
35
54
  loadJournal: organizer.loadJournal,
36
55
  NAMING_CONVENTIONS: organizer.NAMING_CONVENTIONS,
37
56
 
57
+ // Intent Interpreter
58
+ IntentInterpreter: IntentInterpreter,
59
+
60
+ // FS Abstraction
61
+ FileMayorFS: FileMayorFS,
62
+
38
63
  // Cleaner
39
64
  Cleaner: cleaner.Cleaner,
40
65
  findJunk: cleaner.findJunk,
@@ -74,6 +99,37 @@ module.exports = {
74
99
  rulesToConfig: sopParser.rulesToConfig,
75
100
  FILE_TYPE_ALIASES: sopParser.FILE_TYPE_ALIASES,
76
101
 
102
+ // License
103
+ activateLicense: license.activateLicense,
104
+ deactivateLicense: license.deactivateLicense,
105
+ getLicenseInfo: license.getLicenseInfo,
106
+ checkProFeature: license.checkProFeature,
107
+ checkBulkLimit: license.checkBulkLimit,
108
+ hasFeature: license.hasFeature,
109
+
110
+ // Intent Agents
111
+ IntentStrategist: strategist,
112
+ MetadataSentry: sentry,
113
+ CurativePlanner: planner,
114
+ SecurityArchitect: validator,
115
+
116
+ // Engines
117
+ ExplainEngine,
118
+ CureEngine,
119
+ ApplyEngine,
120
+ PreviewEngine,
121
+ DedupeEngine,
122
+ MetadataStore,
123
+
124
+ // Hardened Runtime
125
+ FileMayorJailer,
126
+ enforceUserSpace,
127
+ Vault,
128
+ LogicGuardrail,
129
+ initEmergencyHalt,
130
+ updateJournalRef,
131
+ clearEmergencyHalt,
132
+
77
133
  // Version
78
134
  VERSION: require('../package.json').version || '2.0.0'
79
135
  };
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR v3.0 — INTENT INTERPRETER
6
+ * AI Logic Layer: Bridges fuzzy human language and atomic execution.
7
+ * ═══════════════════════════════════════════════════════════════════
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const path = require('path');
13
+
14
+ const GEMINI_MODEL = 'gemini-2.0-flash';
15
+ const GEMINI_ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
16
+
17
+ class IntentInterpreter {
18
+ constructor(apiKey) {
19
+ this.apiKey = apiKey || process.env.GEMINI_API_KEY;
20
+ if (!this.apiKey) {
21
+ console.warn('[WARN] GEMINI_API_KEY not found. AI features will be disabled.');
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Interpret a user prompt against clustered telemetry
27
+ * @param {string} userPrompt
28
+ * @param {Object} clusters - From MetadataSentry
29
+ * @param {Object} context
30
+ * @returns {Promise<Object>} The Curative Plan
31
+ */
32
+ async interpret(userPrompt, clusters, context = {}) {
33
+ if (!this.apiKey) {
34
+ throw new Error('API Key missing. Cannot interpret intent.');
35
+ }
36
+
37
+ const telemetry = this._summarizeFiles(clusters);
38
+ const systemPrompt = this._getSystemPrompt(context);
39
+
40
+ const response = await this._callGemini(systemPrompt, userPrompt, telemetry);
41
+ const curativePlan = this._parseResponse(response);
42
+
43
+ // [Production Grade] Confidence Gating
44
+ if (curativePlan.confidence < 60) {
45
+ curativePlan.status = 'low_confidence';
46
+ curativePlan.message = 'I am not entirely sure about this request. Could you clarify your intent?';
47
+ }
48
+
49
+ // [Production Grade] Plan Validation
50
+ curativePlan.plan = this._validatePlan(curativePlan.plan);
51
+
52
+ return curativePlan;
53
+ }
54
+
55
+ /**
56
+ * Summarize clustered file list for AI ingestion
57
+ */
58
+ _summarizeFiles(clusters) {
59
+ const summary = {};
60
+
61
+ for (const cat in clusters) {
62
+ summary[cat] = {
63
+ count: clusters[cat].count,
64
+ samples: clusters[cat].samples.map(s => ({
65
+ name: s.name,
66
+ path: s.path,
67
+ ancestry: s.ancestry,
68
+ bundleId: s.bundleId
69
+ }))
70
+ };
71
+ }
72
+
73
+ return summary;
74
+ }
75
+
76
+ _getSystemPrompt(context) {
77
+ return `You are FileMayor v3.2, the "Intuition & Dependency Engine".
78
+ Your goal is to organize files while strictly preserving the integrity of projects and collections.
79
+
80
+ INPUT:
81
+ 1. User Intent: "${context.intent}" (Strategy: "${context.strategy}")
82
+ 2. Telemetry: Clustered file metadata including 'ancestry' and 'bundleId'.
83
+
84
+ GOLDEN RULES:
85
+ - RULE OF ANCESTRY: Respect parent directory names in 'ancestry'. If a file is in /Books/ or a project folder, DO NOT scatter it based on extension alone.
86
+ - ATOMIC BUNDLING: Files sharing a 'bundleId' (like music stems with a project file) MUST stay together.
87
+ - REFINE, DON'T DESTROY: A "Refine" intent means improving structure WITHIN a domain, not extracting files OUT of it.
88
+ - FORMAT: Return a VALID JSON object. Use absolute paths for source.
89
+
90
+ OUTPUT SCHEMA:
91
+ {
92
+ "narrative": "A curative explanation of the structural refinement.",
93
+ "plan": [
94
+ { "source": "absolute/path", "destination": "suggested/target", "reason": "Why?" }
95
+ ],
96
+ "confidence": 0-100
97
+ }`;
98
+ }
99
+
100
+ async _callGemini(systemPrompt, userPrompt, files) {
101
+ const body = {
102
+ contents: [{
103
+ parts: [{
104
+ text: `${systemPrompt}\n\nUSER INTENT: "${userPrompt}"\n\nFILES:\n${JSON.stringify(files, null, 2)}`
105
+ }]
106
+ }],
107
+ generationConfig: {
108
+ temperature: 0.1, // Even lower for higher determinism
109
+ topP: 0.95,
110
+ maxOutputTokens: 2048,
111
+ responseMimeType: "application/json" // [Production Grade] Force JSON Mode
112
+ }
113
+ };
114
+
115
+ const url = `${GEMINI_ENDPOINT}?key=${this.apiKey}`;
116
+ const response = await fetch(url, {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify(body),
120
+ });
121
+
122
+ if (!response.ok) {
123
+ const err = await response.text();
124
+ throw new Error(`Gemini API Error: ${response.status} - ${err}`);
125
+ }
126
+
127
+ const data = await response.json();
128
+ return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
129
+ }
130
+
131
+ /**
132
+ * Validate plan entries from AI response
133
+ * Filters out any entries missing required fields
134
+ */
135
+ _validatePlan(plan) {
136
+ if (!Array.isArray(plan)) return [];
137
+ return plan.filter(entry => {
138
+ if (!entry || typeof entry !== 'object') return false;
139
+ if (!entry.source || typeof entry.source !== 'string') return false;
140
+ if (!entry.destination || typeof entry.destination !== 'string') return false;
141
+ if (!entry.reason) entry.reason = 'AI suggested move';
142
+ return true;
143
+ });
144
+ }
145
+
146
+ _parseResponse(rawText) {
147
+ try {
148
+ const jsonMatch = rawText.match(/\{[\s\S]*\}/);
149
+ if (!jsonMatch) throw new Error('No JSON found in AI response');
150
+ return JSON.parse(jsonMatch[0]);
151
+ } catch (err) {
152
+ console.error('[FAIL] Failed to parse AI response:', rawText);
153
+ throw new Error(`AI Parsing Error: ${err.message}`);
154
+ }
155
+ }
156
+ }
157
+
158
+ module.exports = IntentInterpreter;
package/core/jailer.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR CORE — THE JAILER (IMMUTABLE SECURITY LAYER)
6
+ *
7
+ * This is the final firewall between FileMayor's AI "intuition" and
8
+ * the actual operating system. Even if Gemini suggests moving files
9
+ * into System32, this module kills the operation cold.
10
+ *
11
+ * CRITICAL: This file must NEVER be modified by AI assistants.
12
+ * It is the immutable source of truth for filesystem safety.
13
+ * ═══════════════════════════════════════════════════════════════════
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const path = require('path');
19
+ const fs = require('fs');
20
+ const os = require('os');
21
+
22
+ // ─── Forbidden Zones (OS-Agnostic) ───────────────────────────────
23
+ const FORBIDDEN_ZONES = [
24
+ // Windows
25
+ 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)',
26
+ 'C:\\ProgramData', 'C:\\Users\\Public', 'C:\\Users\\Default',
27
+ // Linux / macOS
28
+ '/etc', '/usr', '/bin', '/sbin', '/var', '/root',
29
+ '/boot', '/dev', '/proc', '/sys', '/run',
30
+ // macOS specific
31
+ '/System', '/Library', '/private'
32
+ ].map(p => path.resolve(p).toLowerCase());
33
+
34
+ /**
35
+ * THE JAILER: Final security check for ALL FileMayor operations.
36
+ * Use this before ANY fs.rename, fs.unlink, or fs.copy operation.
37
+ */
38
+ class FileMayorJailer {
39
+ /**
40
+ * @param {string} rootDirectory - The user's working directory (the "jail")
41
+ */
42
+ constructor(rootDirectory = process.cwd()) {
43
+ // Resolve the root to its absolute, REAL path (no symlinks)
44
+ try {
45
+ this.safeRoot = fs.realpathSync(path.resolve(rootDirectory)).toLowerCase();
46
+ } catch {
47
+ this.safeRoot = path.resolve(rootDirectory).toLowerCase();
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Validate that a target path is safe for FileMayor to operate on.
53
+ * Resolves symlinks to their TRUE destination before checking.
54
+ *
55
+ * @param {string} targetPath - The path to validate
56
+ * @returns {{ safe: boolean, realPath: string, error?: string }}
57
+ */
58
+ validate(targetPath) {
59
+ try {
60
+ if (!targetPath || typeof targetPath !== 'string') {
61
+ return { safe: false, realPath: '', error: 'Empty or invalid path' };
62
+ }
63
+
64
+ // Reject null bytes (classic injection vector)
65
+ if (targetPath.includes('\0')) {
66
+ return { safe: false, realPath: '', error: 'Null byte injection detected' };
67
+ }
68
+
69
+ const absoluteTarget = path.resolve(targetPath);
70
+
71
+ // [ANTI-SYMLINK] Resolve the REAL path (follows symlinks to true source)
72
+ // This defeats CVE-2025-55130 style symlink race conditions
73
+ let realTarget;
74
+ try {
75
+ realTarget = fs.existsSync(absoluteTarget)
76
+ ? fs.realpathSync(absoluteTarget)
77
+ : absoluteTarget;
78
+ } catch {
79
+ realTarget = absoluteTarget;
80
+ }
81
+
82
+ const normalizedTarget = realTarget.toLowerCase();
83
+
84
+ // [JAIL CHECK] Is the target inside our allowed project folder?
85
+ if (!normalizedTarget.startsWith(this.safeRoot + path.sep) &&
86
+ normalizedTarget !== this.safeRoot) {
87
+ return {
88
+ safe: false,
89
+ realPath: realTarget,
90
+ error: `JAIL BREACH: "${realTarget}" is outside the safe root "${this.safeRoot}"`
91
+ };
92
+ }
93
+
94
+ // [SYSTEM CHECK] Is the target a protected OS zone?
95
+ for (const zone of FORBIDDEN_ZONES) {
96
+ if (normalizedTarget.startsWith(zone + path.sep) || normalizedTarget === zone) {
97
+ return {
98
+ safe: false,
99
+ realPath: realTarget,
100
+ error: `SYSTEM PROTECTION: "${realTarget}" is within forbidden zone "${zone}"`
101
+ };
102
+ }
103
+ }
104
+
105
+ // [EXTENSION CHECK] Reject system-critical file types
106
+ const ext = path.extname(realTarget).toLowerCase();
107
+ const dangerousExts = new Set(['.sys', '.drv', '.dll', '.so', '.dylib', '.kext', '.plist', '.reg']);
108
+ if (dangerousExts.has(ext)) {
109
+ return {
110
+ safe: false,
111
+ realPath: realTarget,
112
+ error: `DANGEROUS FILE: "${ext}" files are system-critical and cannot be moved`
113
+ };
114
+ }
115
+
116
+ return { safe: true, realPath: realTarget };
117
+
118
+ } catch (err) {
119
+ return { safe: false, realPath: '', error: `Jailer error: ${err.message}` };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Validate a move operation (both source AND destination must be safe)
125
+ * @param {string} source - Source file path
126
+ * @param {string} destination - Destination file path
127
+ * @returns {{ safe: boolean, error?: string }}
128
+ */
129
+ validateMove(source, destination) {
130
+ const srcCheck = this.validate(source);
131
+ if (!srcCheck.safe) return { safe: false, error: `Source: ${srcCheck.error}` };
132
+
133
+ const dstCheck = this.validate(destination);
134
+ if (!dstCheck.safe) return { safe: false, error: `Destination: ${dstCheck.error}` };
135
+
136
+ return { safe: true };
137
+ }
138
+ }
139
+
140
+ // ─── Root/Sudo Kill-Switch ────────────────────────────────────────
141
+ // If running as root on Unix, exit immediately. FileMayor should
142
+ // NEVER have elevated privileges to limit blast radius.
143
+ function enforceUserSpace() {
144
+ if (typeof process.getuid === 'function' && process.getuid() === 0) {
145
+ console.error('❌ FILEMAYOR SECURITY ERROR: Running as root/sudo is strictly forbidden.');
146
+ console.error(' FileMayor is designed to run in User Space only.');
147
+ process.exit(1);
148
+ }
149
+ }
150
+
151
+ module.exports = { FileMayorJailer, enforceUserSpace, FORBIDDEN_ZONES };
package/core/license.js CHANGED
@@ -66,10 +66,12 @@ const BUILTIN_KEYS = {
66
66
  * @returns {string} 5-char checksum
67
67
  */
68
68
  function generateChecksum(body) {
69
- const hash = crypto.createHmac('sha256', CHECKSUM_SECRET)
70
- .update(body)
71
- .digest('hex');
72
- return hash.substring(0, 5).toUpperCase();
69
+ // Ported from Admin Dashboard simpleHash for cross-platform compatibility
70
+ let hash = 0;
71
+ for (let i = 0; i < body.length; i++) {
72
+ hash = ((hash << 5) - hash + body.charCodeAt(i)) | 0;
73
+ }
74
+ return Math.abs(hash).toString(36).toUpperCase().padEnd(5, '0').substring(0, 5);
73
75
  }
74
76
 
75
77
  /**
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR CORE — METADATA STORE
6
+ * Lightweight, persistent indexing for S2 compliance (<300ms search).
7
+ * ═══════════════════════════════════════════════════════════════════
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs').promises;
13
+ const fssync = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const crypto = require('crypto');
17
+
18
+ class MetadataStore {
19
+ constructor(dbPath) {
20
+ this.rootPath = dbPath || path.join(os.homedir(), '.filemayor', 'metadata_index.json');
21
+ this.index = {
22
+ version: '1.0',
23
+ lastUpdated: null,
24
+ files: {} // path -> metadata
25
+ };
26
+ this._ensureDir();
27
+ this.load();
28
+ }
29
+
30
+ _ensureDir() {
31
+ const dir = path.dirname(this.rootPath);
32
+ if (!fssync.existsSync(dir)) {
33
+ fssync.mkdirSync(dir, { recursive: true });
34
+ }
35
+ }
36
+
37
+ load() {
38
+ try {
39
+ if (fssync.existsSync(this.rootPath)) {
40
+ const data = fssync.readFileSync(this.rootPath, 'utf8');
41
+ this.index = JSON.parse(data);
42
+ }
43
+ } catch (err) {
44
+ console.error('[IDX] Failed to load index:', err.message);
45
+ }
46
+ }
47
+
48
+ async save() {
49
+ try {
50
+ this.index.lastUpdated = new Date().toISOString();
51
+ await fs.writeFile(this.rootPath, JSON.stringify(this.index, null, 2));
52
+ } catch (err) {
53
+ console.error('[IDX] Failed to save index:', err.message);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Update the index with fresh file metadata
59
+ * @param {Object[]} files
60
+ */
61
+ update(files) {
62
+ for (const file of files) {
63
+ this.index.files[file.path] = {
64
+ name: file.name,
65
+ size: file.size,
66
+ mtime: file.modified,
67
+ category: file.category,
68
+ ext: file.ext
69
+ };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Search the index with < 300ms latency
75
+ * @param {string} query
76
+ * @returns {Object[]}
77
+ */
78
+ search(query) {
79
+ const startTime = Date.now();
80
+ const results = [];
81
+ const lowerQuery = query.toLowerCase();
82
+
83
+ for (const [filePath, meta] of Object.entries(this.index.files)) {
84
+ if (meta.name.toLowerCase().includes(lowerQuery) || filePath.toLowerCase().includes(lowerQuery)) {
85
+ results.push({ path: filePath, ...meta });
86
+ }
87
+ if (results.length > 100) break; // Cap results for performance
88
+ }
89
+
90
+ const duration = Date.now() - startTime;
91
+ return {
92
+ results,
93
+ duration,
94
+ totalFound: results.length
95
+ };
96
+ }
97
+
98
+ clear() {
99
+ this.index.files = {};
100
+ this.save();
101
+ }
102
+ }
103
+
104
+ module.exports = new MetadataStore();