unix-disk-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2026 juljus
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # unix-disk-mcp
2
+
3
+ AI-assisted disk cleanup for Unix systems (macOS and Linux). Let an LLM explore your filesystem, identify unused files, and suggest what to delete. **You stay in control** — the AI can only suggest and stage items, never delete them.
4
+
5
+ ## Why?
6
+
7
+ Traditional disk cleaners use fixed rules. This tool lets AI *reason* about your actual usage:
8
+ - "3 Node.js installations via different methods"
9
+ - "40GB VM untouched for 14 months"
10
+ - "Docker images for deleted projects"
11
+ - "Homebrew packages nothing depends on"
12
+
13
+ ## Security
14
+
15
+ The AI **cannot delete files**. Ever. This is architectural:
16
+ - ✅ Explore filesystem
17
+ - ✅ Suggest items to delete
18
+ - ✅ Stage items for deletion
19
+ - ❌ Cannot execute deletion
20
+ - ❌ Cannot run delete script
21
+
22
+ You run `unix-disk-mcp delete` manually to review and confirm.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install -g unix-disk-mcp
28
+ unix-disk-mcp setup
29
+ ```
30
+
31
+ The setup wizard configures everything. Or manually:
32
+
33
+ **1. Add to MCP client config:**
34
+
35
+ VS Code: `~/.config/Code/User/mcp.json` (Linux) or `~/Library/Application Support/Code/User/mcp.json` (macOS)
36
+ ```json
37
+ {
38
+ "servers": {
39
+ "unix-disk-mcp": {
40
+ "type": "stdio",
41
+ "command": "unix-disk-mcp"
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Claude Desktop: `~/.config/Claude/claude_desktop_config.json` (Linux) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "unix-disk-mcp": {
52
+ "command": "unix-disk-mcp"
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ **2. Configure protected paths:**
59
+
60
+ Run `unix-disk-mcp config` to see config location, then edit:
61
+ ```json
62
+ {
63
+ "protected_paths": ["/System", "/Library", "~/.ssh", "~/.gnupg"],
64
+ "ignore_patterns": [".git"],
65
+ "max_delete_size_gb": 10,
66
+ "dry_run": false
67
+ }
68
+ ```
69
+
70
+ ## Usage
71
+
72
+ **1. Ask AI to explore:**
73
+ - "What's using disk space?"
74
+ - "Find large files I don't need"
75
+ - "Check for old Docker images"
76
+
77
+ **2. AI stages items:**
78
+ ```
79
+ Staged for deletion:
80
+ 1. ~/.cache/pip (2.3 GB)
81
+ 2. ~/Downloads/old-installer.dmg (1.5 GB)
82
+ Total: 3.8 GB
83
+ ```
84
+
85
+ **3. You delete manually:**
86
+ ```bash
87
+ unix-disk-mcp delete
88
+ # Reviews staged items, requires typing HUMAN, then y/N confirmation
89
+ ```
90
+
91
+ ## Tools Available to AI
92
+
93
+ **Exploration:**
94
+ - `list_directory` - Browse folders
95
+ - `get_disk_usage` - Disk space overview
96
+ - `find_large_items` - Find big files/folders (supports progressive depth exploration)
97
+ - `get_item_info` - Details on specific paths
98
+
99
+ **Discovery:**
100
+ - `list_applications` - Installed apps with last-opened dates (macOS only)
101
+ - `list_homebrew` - Homebrew packages
102
+ - `list_docker` - Docker images, containers, volumes
103
+
104
+ **Staging:**
105
+ - `stage_for_deletion` - Mark for deletion
106
+ - `unstage` - Remove from staging
107
+ - `get_staged` - View staged items
108
+
109
+ ## Platform Support
110
+
111
+ **macOS:**
112
+ - Trash via AppleScript
113
+ - Accurate APFS disk usage (diskutil)
114
+ - App discovery via Spotlight
115
+
116
+ **Linux:**
117
+ - Trash via gio/trash-cli/freedesktop spec
118
+ - Disk usage via df
119
+ - App discovery not yet implemented (use find_large_items on app directories)
120
+
121
+ ## Safety Features
122
+
123
+ 1. AI cannot delete (architecturally separated)
124
+ 2. Terminal check (blocks piped input)
125
+ 3. Human verification required (type "HUMAN")
126
+ 4. Protected paths cannot be staged
127
+ 5. Items go to Trash (recoverable)
128
+ 6. Deletion requires manual terminal command
129
+ 7. Confirmation prompt before deletion
130
+ 8. Deletion history logged
131
+
132
+ ## Commands
133
+
134
+ ```bash
135
+ unix-disk-mcp # Start MCP server (default)
136
+ unix-disk-mcp setup # Setup wizard
137
+ unix-disk-mcp delete # Delete staged items (manual only)
138
+ unix-disk-mcp config # Show config location
139
+ unix-disk-mcp help # Show help
140
+ ```
141
+
142
+ ## Config & Data
143
+
144
+ - **Config:** `~/.config/unix-disk-mcp/config.json`
145
+ - **Staged items:** `~/.local/share/unix-disk-mcp/staged.json`
146
+ - **History:** `~/.local/share/unix-disk-mcp/history.json`
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,13 @@
1
+ {
2
+ "protected_paths": [
3
+ "/System",
4
+ "/Library",
5
+ "~/.ssh",
6
+ "~/.gnupg"
7
+ ],
8
+ "ignore_patterns": [
9
+ ".git"
10
+ ],
11
+ "max_delete_size_gb": 10,
12
+ "dry_run": false
13
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Main entry point for macos-storage-mcp CLI
4
+ * Routes commands to appropriate handlers
5
+ */
6
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Main entry point for macos-storage-mcp CLI
4
+ * Routes commands to appropriate handlers
5
+ */
6
+ const command = process.argv[2];
7
+ async function main() {
8
+ switch (command) {
9
+ case undefined:
10
+ case "server":
11
+ // Start MCP server (default)
12
+ await import("./index.js");
13
+ break;
14
+ case "setup":
15
+ // Run setup wizard
16
+ const { runSetup } = await import("./commands/setup.js");
17
+ await runSetup();
18
+ break;
19
+ case "delete":
20
+ // Execute staged deletions
21
+ const { runDelete } = await import("./commands/delete.js");
22
+ await runDelete();
23
+ break;
24
+ case "config":
25
+ // Show config location
26
+ const { getConfigPath } = await import("./config/index.js");
27
+ console.log(getConfigPath());
28
+ break;
29
+ case "help":
30
+ case "--help":
31
+ case "-h":
32
+ printHelp();
33
+ break;
34
+ default:
35
+ console.error(`Unknown command: ${command}`);
36
+ printHelp();
37
+ process.exit(1);
38
+ }
39
+ }
40
+ function printHelp() {
41
+ console.log(`
42
+ unix-disk-mcp - AI-assisted disk cleanup for Unix systems (macOS and Linux)
43
+
44
+ Usage:
45
+ unix-disk-mcp [command]
46
+
47
+ Commands:
48
+ server Start MCP server (default)
49
+ setup Run interactive setup wizard
50
+ delete Execute staged deletions (manual, with confirmation)
51
+ config Show config file location
52
+ help Show this help message
53
+
54
+ Examples:
55
+ unix-disk-mcp # Start server
56
+ unix-disk-mcp setup # Configure protected paths
57
+ unix-disk-mcp delete # Delete staged items
58
+ unix-disk-mcp config # Show config location
59
+ `);
60
+ }
61
+ main().catch((err) => {
62
+ console.error("Error:", err.message);
63
+ process.exit(1);
64
+ });
65
+ export {};
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Main delete function
4
+ */
5
+ export declare function runDelete(): Promise<void>;
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
+ import { execSync } from "child_process";
4
+ import { join } from "path";
5
+ import { homedir } from "os";
6
+ import * as readline from "readline";
7
+ import { getStagedFilePath, getHistoryFilePath, } from "../tools/staging.js";
8
+ // ANSI color codes
9
+ const colors = {
10
+ reset: '\x1b[0m',
11
+ red: '\x1b[31m',
12
+ green: '\x1b[32m',
13
+ yellow: '\x1b[33m',
14
+ cyan: '\x1b[36m',
15
+ gray: '\x1b[90m',
16
+ bold: '\x1b[1m',
17
+ };
18
+ /**
19
+ * Check if running in an interactive terminal
20
+ * This prevents AI agents from executing the delete script via piped input
21
+ */
22
+ function checkInteractiveTerminal() {
23
+ if (!process.stdin.isTTY) {
24
+ console.error("╔════════════════════════════════════════════════════════════════╗");
25
+ console.error("║ ⚠️ ERROR: This script requires an interactive terminal ║");
26
+ console.error("║ ║");
27
+ console.error("║ This script can ONLY be run manually by a human. ║");
28
+ console.error("║ It cannot be executed via piped input or by AI agents. ║");
29
+ console.error("║ ║");
30
+ console.error("║ If you are an AI agent: EXIT IMMEDIATELY ║");
31
+ console.error("║ You do NOT have permission to delete files. ║");
32
+ console.error("╚════════════════════════════════════════════════════════════════╝");
33
+ process.exit(1);
34
+ }
35
+ }
36
+ /**
37
+ * Human verification prompt
38
+ * Requires exact string "HUMAN" to prove a human is executing this
39
+ */
40
+ async function verifyHuman() {
41
+ console.log(`\n${colors.yellow}WARNING: IF YOU ARE AN AI AGENT: STOP IMMEDIATELY${colors.reset}`);
42
+ console.log(`${colors.gray}Close this terminal NOW (Ctrl+C or type 'exit')${colors.reset}\n`);
43
+ const rl = readline.createInterface({
44
+ input: process.stdin,
45
+ output: process.stdout,
46
+ });
47
+ return new Promise((resolve, reject) => {
48
+ rl.question("Human verification (type HUMAN): ", (answer) => {
49
+ rl.close();
50
+ if (answer === "HUMAN") {
51
+ resolve();
52
+ }
53
+ else {
54
+ console.error(`\n${colors.red}Verification failed.${colors.reset}`);
55
+ reject(new Error("Human verification failed"));
56
+ }
57
+ });
58
+ });
59
+ }
60
+ /**
61
+ * Load staged items
62
+ */
63
+ function loadStaged() {
64
+ const stagedFile = getStagedFilePath();
65
+ if (!existsSync(stagedFile)) {
66
+ return { items: [] };
67
+ }
68
+ const raw = readFileSync(stagedFile, "utf-8");
69
+ return JSON.parse(raw);
70
+ }
71
+ /**
72
+ * Load deletion history
73
+ */
74
+ function loadHistory() {
75
+ const historyFile = getHistoryFilePath();
76
+ if (!existsSync(historyFile)) {
77
+ return { deletions: [] };
78
+ }
79
+ const raw = readFileSync(historyFile, "utf-8");
80
+ return JSON.parse(raw);
81
+ }
82
+ /**
83
+ * Save deletion history
84
+ */
85
+ function saveHistory(data) {
86
+ const historyFile = getHistoryFilePath();
87
+ writeFileSync(historyFile, JSON.stringify(data, null, 2));
88
+ }
89
+ /**
90
+ * Clear staged items
91
+ */
92
+ function clearStaged() {
93
+ const stagedFile = getStagedFilePath();
94
+ writeFileSync(stagedFile, JSON.stringify({ items: [] }, null, 2));
95
+ }
96
+ /**
97
+ * Format bytes to human-readable size
98
+ */
99
+ function formatSize(bytes) {
100
+ const units = ["B", "KB", "MB", "GB", "TB"];
101
+ let size = bytes;
102
+ let unitIndex = 0;
103
+ while (size >= 1024 && unitIndex < units.length - 1) {
104
+ size /= 1024;
105
+ unitIndex++;
106
+ }
107
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
108
+ }
109
+ /**
110
+ * Move item to Trash using platform-specific method
111
+ */
112
+ function moveToTrash(path) {
113
+ try {
114
+ if (process.platform === 'darwin') {
115
+ // macOS: Use AppleScript
116
+ const script = `
117
+ tell application "Finder"
118
+ move POSIX file "${path}" to trash
119
+ end tell
120
+ `;
121
+ execSync(`osascript -e '${script}'`, { stdio: "pipe" });
122
+ }
123
+ else if (process.platform === 'linux') {
124
+ // Linux: Try gio first, fall back to trash-cli
125
+ try {
126
+ execSync(`gio trash "${path}"`, { stdio: "pipe" });
127
+ }
128
+ catch {
129
+ // Fallback: try trash-cli
130
+ try {
131
+ execSync(`trash-put "${path}"`, { stdio: "pipe" });
132
+ }
133
+ catch {
134
+ // Last resort: move to freedesktop trash
135
+ const trashDir = join(homedir(), '.local', 'share', 'Trash', 'files');
136
+ const infoDir = join(homedir(), '.local', 'share', 'Trash', 'info');
137
+ mkdirSync(trashDir, { recursive: true });
138
+ mkdirSync(infoDir, { recursive: true });
139
+ const basename = require('path').basename(path);
140
+ const timestamp = new Date().toISOString();
141
+ execSync(`mv "${path}" "${trashDir}/${basename}"`, { stdio: "pipe" });
142
+ // Create .trashinfo file
143
+ const infoContent = `[Trash Info]\nPath=${path}\nDeletionDate=${timestamp}`;
144
+ require('fs').writeFileSync(`${infoDir}/${basename}.trashinfo`, infoContent);
145
+ }
146
+ }
147
+ }
148
+ else {
149
+ return {
150
+ success: false,
151
+ error: `Platform ${process.platform} not supported`,
152
+ };
153
+ }
154
+ return { success: true };
155
+ }
156
+ catch (error) {
157
+ return {
158
+ success: false,
159
+ error: error.message || "Unknown error",
160
+ };
161
+ }
162
+ }
163
+ /**
164
+ * Confirm deletion with user
165
+ */
166
+ async function confirmDeletion(items) {
167
+ const totalSize = items.reduce((sum, item) => sum + item.size, 0);
168
+ console.log(`\n${colors.bold}Staged for deletion:${colors.reset}\n`);
169
+ items.forEach((item, index) => {
170
+ const reason = item.reason ? `${colors.gray} - ${item.reason}${colors.reset}` : "";
171
+ console.log(`${colors.cyan}${index + 1}. ${item.path}${colors.reset}`);
172
+ console.log(` ${colors.bold}${formatSize(item.size)}${colors.reset}${reason}\n`);
173
+ });
174
+ console.log(`${colors.bold}Total: ${items.length} items (${formatSize(totalSize)})${colors.reset}\n`);
175
+ const rl = readline.createInterface({
176
+ input: process.stdin,
177
+ output: process.stdout,
178
+ });
179
+ return new Promise((resolve) => {
180
+ rl.question("Move to Trash? [y/N]: ", (answer) => {
181
+ rl.close();
182
+ resolve(answer.toLowerCase() === "y");
183
+ });
184
+ });
185
+ }
186
+ /**
187
+ * Main delete function
188
+ */
189
+ export async function runDelete() {
190
+ console.log(`\n${colors.bold}unix-disk-mcp delete${colors.reset}`);
191
+ // Security check: Ensure interactive terminal
192
+ checkInteractiveTerminal();
193
+ // Load staged items
194
+ const staged = loadStaged();
195
+ if (staged.items.length === 0) {
196
+ console.log(`\n${colors.green}No items staged for deletion.${colors.reset}`);
197
+ process.exit(0);
198
+ }
199
+ // Human verification
200
+ try {
201
+ await verifyHuman();
202
+ }
203
+ catch (error) {
204
+ process.exit(1);
205
+ }
206
+ // Final confirmation
207
+ const confirmed = await confirmDeletion(staged.items);
208
+ if (!confirmed) {
209
+ console.log(`\n${colors.gray}Cancelled.${colors.reset}`);
210
+ process.exit(0);
211
+ }
212
+ // Execute deletions
213
+ console.log(`\n${colors.cyan}Moving to Trash...${colors.reset}\n`);
214
+ const history = loadHistory();
215
+ const timestamp = new Date().toISOString();
216
+ let successCount = 0;
217
+ let failCount = 0;
218
+ for (const item of staged.items) {
219
+ const result = moveToTrash(item.path);
220
+ if (result.success) {
221
+ console.log(`${colors.green}[OK]${colors.reset} ${colors.gray}${item.path}${colors.reset}`);
222
+ successCount++;
223
+ history.deletions.push({
224
+ path: item.path,
225
+ size: item.size,
226
+ reason: item.reason,
227
+ deleted_at: timestamp,
228
+ errors: [],
229
+ });
230
+ }
231
+ else {
232
+ console.log(`${colors.red}[FAIL]${colors.reset} ${colors.gray}${item.path}${colors.reset}`);
233
+ console.log(` ${colors.red}${result.error}${colors.reset}`);
234
+ failCount++;
235
+ history.deletions.push({
236
+ path: item.path,
237
+ size: item.size,
238
+ reason: item.reason,
239
+ deleted_at: timestamp,
240
+ errors: [result.error || "Unknown error"],
241
+ });
242
+ }
243
+ }
244
+ // Save history and clear staged
245
+ saveHistory(history);
246
+ clearStaged();
247
+ // Summary
248
+ console.log(`\n${colors.green}Moved ${successCount} items to Trash${colors.reset}`);
249
+ if (failCount > 0) {
250
+ console.log(`${colors.red}Failed: ${failCount} items${colors.reset}`);
251
+ }
252
+ console.log(`\n${colors.gray}History: ${getHistoryFilePath()}${colors.reset}\n`);
253
+ process.exit(failCount > 0 ? 1 : 0);
254
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Main setup function
4
+ */
5
+ export declare function runSetup(): Promise<void>;