@pheem49/mint 1.2.4 → 1.4.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.
@@ -18,28 +18,75 @@ async function runOnboarding(options = {}) {
18
18
  {
19
19
  type: 'input',
20
20
  name: 'apiKey',
21
- message: 'Please enter your Google Gemini API Key:',
21
+ message: 'Enter your Google Gemini API Key (Required for basic features):',
22
22
  default: config.apiKey || undefined,
23
- validate: (input) => input.length > 0 ? true : 'API Key is required.'
23
+ validate: (input) => input.trim().length > 0 ? true : 'API Key is required.'
24
24
  },
25
25
  {
26
26
  type: 'list',
27
- name: 'geminiModel',
28
- message: 'Select the Gemini model to use:',
27
+ name: 'geminiModelChoice',
28
+ message: 'Select the primary Gemini model to use:',
29
29
  choices: [
30
30
  'gemini-2.5-flash',
31
+ 'gemini-2.0-pro-exp-02-05',
31
32
  'gemini-3.1-flash-lite-preview',
32
33
  'gemini-3.1-flash-lite',
33
- 'gemini-2.0-pro-exp-02-05'
34
+ 'Custom model name'
34
35
  ],
35
36
  default: config.geminiModel || 'gemini-2.5-flash'
37
+ },
38
+ {
39
+ type: 'input',
40
+ name: 'customGeminiModel',
41
+ message: 'Enter your custom Gemini model name:',
42
+ when: (answers) => answers.geminiModelChoice === 'Custom model name',
43
+ validate: (input) => input.trim().length > 0 ? true : 'Please enter a valid model name.'
44
+ },
45
+ {
46
+ type: 'input',
47
+ name: 'anthropicApiKey',
48
+ message: 'Enter your Anthropic API Key (Optional, press Enter to skip):',
49
+ default: config.anthropicApiKey || ''
50
+ },
51
+ {
52
+ type: 'input',
53
+ name: 'openaiApiKey',
54
+ message: 'Enter your OpenAI API Key (Optional, press Enter to skip):',
55
+ default: config.openaiApiKey || ''
56
+ },
57
+ {
58
+ type: 'input',
59
+ name: 'hfApiKey',
60
+ message: 'Enter your Hugging Face API Key (Optional, press Enter to skip):',
61
+ default: config.hfApiKey || ''
62
+ },
63
+ {
64
+ type: 'input',
65
+ name: 'localApiBaseUrl',
66
+ message: 'Enter your Local AI (LM Studio/OpenAI Compatible) Base URL (Optional, press Enter to skip):',
67
+ default: config.localApiBaseUrl || ''
68
+ },
69
+ {
70
+ type: 'input',
71
+ name: 'localModelName',
72
+ message: 'Enter your Local Model Name (Optional, press Enter to skip):',
73
+ default: config.localModelName || ''
36
74
  }
37
75
  ];
38
76
 
39
77
  const answers = await inquirer.prompt(questions);
78
+
79
+ // Resolve custom gemini model if selected
80
+ const geminiModel = answers.geminiModelChoice === 'Custom model name'
81
+ ? answers.customGeminiModel
82
+ : answers.geminiModelChoice;
83
+
84
+ // Remove temporary choice fields before saving
85
+ delete answers.geminiModelChoice;
86
+ delete answers.customGeminiModel;
40
87
 
41
88
  // Save configuration
42
- const newConfig = { ...config, ...answers };
89
+ const newConfig = { ...config, ...answers, geminiModel };
43
90
  writeConfig(newConfig);
44
91
  console.log('\nāœ… Configuration saved successfully!');
45
92
 
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Mint Workspace Manager
3
+ * -----------------------
4
+ * Manages project-specific contexts and persistent workspaces.
5
+ * Stores data in ~/.config/mint/workspaces.json
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const WORKSPACE_FILE = path.join(os.homedir(), '.config', 'mint', 'workspaces.json');
13
+
14
+ function ensureDir() {
15
+ const dir = path.dirname(WORKSPACE_FILE);
16
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ function loadWorkspaces() {
20
+ ensureDir();
21
+ if (!fs.existsSync(WORKSPACE_FILE)) return {};
22
+ try {
23
+ return JSON.parse(fs.readFileSync(WORKSPACE_FILE, 'utf8'));
24
+ } catch (e) {
25
+ return {};
26
+ }
27
+ }
28
+
29
+ function saveWorkspaces(data) {
30
+ ensureDir();
31
+ fs.writeFileSync(WORKSPACE_FILE, JSON.stringify(data, null, 2));
32
+ }
33
+
34
+ function addWorkspace(name, rootPath, instructions = '') {
35
+ const workspaces = loadWorkspaces();
36
+ const absolutePath = path.resolve(rootPath);
37
+ workspaces[name] = {
38
+ name,
39
+ path: absolutePath,
40
+ instructions,
41
+ addedAt: new Date().toISOString(),
42
+ lastAccessed: new Date().toISOString()
43
+ };
44
+ saveWorkspaces(workspaces);
45
+ return workspaces[name];
46
+ }
47
+
48
+ function removeWorkspace(name) {
49
+ const workspaces = loadWorkspaces();
50
+ if (workspaces[name]) {
51
+ delete workspaces[name];
52
+ saveWorkspaces(workspaces);
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ function getWorkspaceByPath(currentPath) {
59
+ const workspaces = loadWorkspaces();
60
+ const absoluteCurrent = path.resolve(currentPath);
61
+
62
+ // Find workspace where current path is inside or equal to workspace path
63
+ for (const name in workspaces) {
64
+ const ws = workspaces[name];
65
+ if (absoluteCurrent.startsWith(ws.path)) {
66
+ return ws;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function listWorkspaces() {
73
+ return loadWorkspaces();
74
+ }
75
+
76
+ module.exports = {
77
+ addWorkspace,
78
+ removeWorkspace,
79
+ getWorkspaceByPath,
80
+ listWorkspaces
81
+ };
@@ -0,0 +1,95 @@
1
+ const { spawn } = require('child_process');
2
+ const { readConfig } = require('../System/config_manager');
3
+
4
+ /**
5
+ * McpManager handles the lifecycle of multiple MCP servers.
6
+ * Since MCP SDK is ESM and this project is CommonJS, we use dynamic imports.
7
+ */
8
+ class McpManager {
9
+ constructor() {
10
+ this.clients = new Map(); // serverName -> { client, transport }
11
+ this.tools = [];
12
+ }
13
+
14
+ async init() {
15
+ const config = readConfig();
16
+ const mcpServers = config.mcpServers || {};
17
+
18
+ console.log(`[MCP] Initializing ${Object.keys(mcpServers).length} servers...`);
19
+
20
+ // Load SDK via dynamic import
21
+ const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
22
+ const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
23
+
24
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
25
+ try {
26
+ console.log(`[MCP] Connecting to server: ${name}`);
27
+
28
+ const transport = new StdioClientTransport({
29
+ command: serverConfig.command,
30
+ args: serverConfig.args,
31
+ env: { ...process.env, ...serverConfig.env }
32
+ });
33
+
34
+ const client = new Client(
35
+ { name: 'mint-ai-assistant', version: '1.2.4' },
36
+ { capabilities: {} }
37
+ );
38
+
39
+ await client.connect(transport);
40
+
41
+ // Discover tools
42
+ const toolsResponse = await client.listTools();
43
+ const serverTools = (toolsResponse.tools || []).map(t => ({
44
+ ...t,
45
+ serverName: name
46
+ }));
47
+
48
+ this.clients.set(name, { client, transport, tools: serverTools });
49
+ this.tools.push(...serverTools);
50
+
51
+ console.log(`[MCP] Server ${name} connected. Found ${serverTools.length} tools.`);
52
+ } catch (err) {
53
+ console.error(`[MCP] Failed to connect to server ${name}:`, err.message);
54
+ }
55
+ }
56
+ }
57
+
58
+ getAllTools() {
59
+ return this.tools;
60
+ }
61
+
62
+ async callTool(serverName, toolName, args) {
63
+ const server = this.clients.get(serverName);
64
+ if (!server) throw new Error(`MCP Server "${serverName}" not found or not connected.`);
65
+
66
+ try {
67
+ console.log(`[MCP] Calling tool ${toolName} on server ${serverName}...`);
68
+ const result = await server.client.callTool({
69
+ name: toolName,
70
+ arguments: args
71
+ });
72
+ return result;
73
+ } catch (err) {
74
+ console.error(`[MCP] Error calling tool ${toolName}:`, err);
75
+ throw err;
76
+ }
77
+ }
78
+
79
+ async shutdown() {
80
+ console.log('[MCP] Shutting down all servers...');
81
+ for (const [name, server] of this.clients.entries()) {
82
+ try {
83
+ await server.client.close();
84
+ console.log(`[MCP] Server ${name} closed.`);
85
+ } catch (err) {
86
+ console.error(`[MCP] Error closing server ${name}:`, err.message);
87
+ }
88
+ }
89
+ this.clients.clear();
90
+ this.tools = [];
91
+ }
92
+ }
93
+
94
+ const instance = new McpManager();
95
+ module.exports = instance;
@@ -16,8 +16,8 @@ class PluginManager {
16
16
 
17
17
  const files = fs.readdirSync(this.pluginsDir);
18
18
  for (const file of files) {
19
- // Ignore self and non-JS files
20
- if (file === 'plugin_manager.js' || !file.endsWith('.js')) continue;
19
+ // Ignore self and core system managers
20
+ if (file === 'plugin_manager.js' || file === 'mcp_manager.js' || !file.endsWith('.js')) continue;
21
21
 
22
22
  const pluginPath = path.join(this.pluginsDir, file);
23
23
 
@@ -1,45 +1,173 @@
1
- const { exec } = require('child_process');
1
+ /**
2
+ * Mint Spotify Plugin — Complete Edition
3
+ * ----------------------------------------
4
+ * Controls Spotify playback via playerctl (no OAuth required).
5
+ * Supports: play, pause, next, previous, stop, shuffle, volume,
6
+ * now_playing, search (opens Spotify search URL).
7
+ *
8
+ * Requirements: playerctl installed (sudo apt install playerctl)
9
+ * Spotify must be running (Desktop app or Snap).
10
+ */
11
+
12
+ const { exec, execSync } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const execAsync = promisify(exec);
15
+
16
+ // ── Helpers ────────────────────────────────────────────────────────────────
17
+
18
+ async function runPlayerctl(args) {
19
+ try {
20
+ const { stdout } = await execAsync(`playerctl -p spotify ${args}`);
21
+ return { ok: true, output: stdout.trim() };
22
+ } catch (err) {
23
+ const msg = (err.stderr || err.message || '').toLowerCase();
24
+ if (msg.includes('no players found') || msg.includes('could not find player')) {
25
+ return { ok: false, error: 'spotify_not_running' };
26
+ }
27
+ if (err.code === 127) {
28
+ return { ok: false, error: 'playerctl_missing' };
29
+ }
30
+ return { ok: false, error: err.message };
31
+ }
32
+ }
33
+
34
+ function formatError(errorCode) {
35
+ if (errorCode === 'spotify_not_running') {
36
+ return 'šŸŽµ Spotify ąø¢ąø±ąø‡ą¹„ąø”ą¹ˆą¹„ąø”ą¹‰ą¹€ąø›ąø“ąø”ąø­ąø¢ąø¹ą¹ˆąø™ąø°ąø„ąø° กรุณาเปณด Spotify ąøą¹ˆąø­ąø™ąø™ąø°ąø„ąø°';
37
+ }
38
+ if (errorCode === 'playerctl_missing') {
39
+ return 'āš ļø ą¹„ąø”ą¹ˆąøžąøš playerctl ąøąø£ąøøąø“ąø²ąø•ąø“ąø”ąø•ąø±ą¹‰ąø‡ąø”ą¹‰ąø§ąø¢ąø„ąø³ąøŖąø±ą¹ˆąø‡: sudo apt install playerctl';
40
+ }
41
+ return `āŒ ą¹€ąøąø“ąø”ąø‚ą¹‰ąø­ąøœąø“ąø”ąøžąø„ąø²ąø”: ${errorCode}`;
42
+ }
43
+
44
+ // ── Action Handlers ────────────────────────────────────────────────────────
45
+
46
+ const ACTION_MAP = {
47
+ 'play': () => runPlayerctl('play'),
48
+ 'pause': () => runPlayerctl('pause'),
49
+ 'stop': () => runPlayerctl('stop'),
50
+ 'next': () => runPlayerctl('next'),
51
+ 'previous': () => runPlayerctl('previous'),
52
+ 'prev': () => runPlayerctl('previous'),
53
+ };
54
+
55
+ const ACTION_MESSAGES = {
56
+ 'play': 'ā–¶ļø ą¹€ąø„ą¹ˆąø™ Spotify ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø° šŸŽµ',
57
+ 'pause': 'āøļø ąø«ąø¢ąøøąø”ą¹€ąøžąø„ąø‡ąøŠąø±ą¹ˆąø§ąø„ąø£ąø²ąø§ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø°',
58
+ 'stop': 'ā¹ļø หยุด Spotify ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø°',
59
+ 'next': 'ā­ļø ąø‚ą¹‰ąø²ąø”ą¹„ąø›ą¹€ąøžąø„ąø‡ąø–ąø±ąø”ą¹„ąø›ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø° šŸŽµ',
60
+ 'previous': 'ā®ļø ąøąø„ąø±ąøšą¹„ąø›ą¹€ąøžąø„ąø‡ąøą¹ˆąø­ąø™ąø«ąø™ą¹‰ąø²ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø°',
61
+ 'prev': 'ā®ļø ąøąø„ąø±ąøšą¹„ąø›ą¹€ąøžąø„ąø‡ąøą¹ˆąø­ąø™ąø«ąø™ą¹‰ąø²ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø°',
62
+ };
63
+
64
+ async function getNowPlaying() {
65
+ const [title, artist, album, status] = await Promise.all([
66
+ runPlayerctl('metadata title'),
67
+ runPlayerctl('metadata artist'),
68
+ runPlayerctl('metadata album'),
69
+ runPlayerctl('status'),
70
+ ]);
71
+
72
+ if (!title.ok) return formatError(title.error);
73
+
74
+ const statusIcon = (status.output || '').toLowerCase() === 'playing' ? 'ā–¶ļø' : 'āøļø';
75
+ const titleText = title.output || 'ą¹„ąø”ą¹ˆąø—ąø£ąø²ąøšąøŠąø·ą¹ˆąø­ą¹€ąøžąø„ąø‡';
76
+ const artistText = artist.output || 'ą¹„ąø”ą¹ˆąø—ąø£ąø²ąøšąøØąø“ąø„ąø›ąø“ąø™';
77
+ const albumText = album.output || '';
78
+
79
+ let reply = `${statusIcon} ąøąø³ąø„ąø±ąø‡ą¹€ąø„ą¹ˆąø™: **${titleText}**\n`;
80
+ reply += `šŸŽ¤ ศณคปณน: ${artistText}`;
81
+ if (albumText) reply += `\nšŸ’æ ąø­ąø±ąø„ąøšąø±ą¹‰ąø”: ${albumText}`;
82
+ return reply;
83
+ }
84
+
85
+ async function setVolume(levelStr) {
86
+ const level = parseInt(levelStr, 10);
87
+ if (isNaN(level) || level < 0 || level > 100) {
88
+ return 'āš ļø ąøąø£ąøøąø“ąø²ąø£ąø°ąøšąøøąø£ąø°ąø”ąø±ąøšą¹€ąøŖąøµąø¢ąø‡ 0-100 ąø„ą¹ˆąø° ą¹€ąøŠą¹ˆąø™ "volume 70"';
89
+ }
90
+ // playerctl volume uses 0.0–1.0
91
+ const result = await runPlayerctl(`volume ${(level / 100).toFixed(2)}`);
92
+ if (!result.ok) return formatError(result.error);
93
+ return `šŸ”Š ąø›ąø£ąø±ąøšą¹€ąøŖąøµąø¢ąø‡ą¹€ąø›ą¹‡ąø™ ${level}% ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø°`;
94
+ }
95
+
96
+ async function setShuffle(state) {
97
+ // state: 'on' | 'off' | 'toggle'
98
+ const shuffleState = state === 'on' ? 'On' : state === 'off' ? 'Off' : 'Toggle';
99
+ const result = await runPlayerctl(`shuffle ${shuffleState}`);
100
+ if (!result.ok) return formatError(result.error);
101
+ if (state === 'toggle') return 'šŸ”€ ąøŖąø„ąø±ąøšą¹‚ąø«ąø”ąø” Shuffle ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø°';
102
+ return `šŸ”€ Shuffle ${state === 'on' ? 'เปณด' : 'ปณด'}ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø°`;
103
+ }
104
+
105
+ function searchSpotify(query) {
106
+ if (!query || !query.trim()) {
107
+ return 'āš ļø ąøąø£ąøøąø“ąø²ąø£ąø°ąøšąøøąø„ąø³ąø—ąøµą¹ˆąø•ą¹‰ąø­ąø‡ąøąø²ąø£ąø„ą¹‰ąø™ąø«ąø²ąø”ą¹‰ąø§ąø¢ąø™ąø°ąø„ąø° ą¹€ąøŠą¹ˆąø™ "search BTS"';
108
+ }
109
+ const encoded = encodeURIComponent(query.trim());
110
+ const url = `https://open.spotify.com/search/${encoded}`;
111
+ try {
112
+ const { exec: execSync2 } = require('child_process');
113
+ execSync2(`xdg-open "${url}"`, { detached: true, stdio: 'ignore' });
114
+ return `šŸ” เปณดค้นหา "${query}" ą¹ƒąø™ Spotify ą¹ąø„ą¹‰ąø§ąø„ą¹ˆąø° šŸŽµ`;
115
+ } catch (_) {
116
+ return `šŸ” ค้นหา "${query}" ąø—ąøµą¹ˆ: ${url}`;
117
+ }
118
+ }
119
+
120
+ // ── Main Plugin Export ─────────────────────────────────────────────────────
2
121
 
3
122
  module.exports = {
4
123
  name: 'spotify',
5
- description: 'Controls Spotify playback (play, pause, next, previous). Only works if Spotify is running. Valid targets are: "play", "pause", "next", "previous".',
6
-
124
+ description: [
125
+ 'Controls Spotify playback and gets now-playing info.',
126
+ 'Valid targets:',
127
+ ' "play" | "pause" | "stop" | "next" | "previous" — playback control',
128
+ ' "now_playing" or "status" — get current song info',
129
+ ' "volume <0-100>" — set volume level (e.g. "volume 70")',
130
+ ' "shuffle on" | "shuffle off" | "shuffle toggle" — toggle shuffle',
131
+ ' "search <query>" — search Spotify (e.g. "search BTS Dynamite")',
132
+ ].join(' '),
133
+
7
134
  async execute(target) {
8
- return new Promise((resolve) => {
9
- const commandMap = {
10
- 'play': 'playerctl -p spotify play',
11
- 'pause': 'playerctl -p spotify pause',
12
- 'next': 'playerctl -p spotify next',
13
- 'previous': 'playerctl -p spotify previous'
14
- };
15
-
16
- const cmd = commandMap[target.toLowerCase()];
17
-
18
- if (!cmd) {
19
- return resolve(`Invalid spotify command: ${target}`);
20
- }
21
-
22
- exec(cmd, (error) => {
23
- if (error) {
24
- // Check if playerctl is missing or Spotify isn't running
25
- if (error.message.includes('No players found')) {
26
- return resolve('Spotify is not currently running or playing anything.');
27
- }
28
- if (error.code === 127) {
29
- return resolve('Error: "playerctl" is not installed on this system. Please install it (e.g., sudo apt install playerctl).');
30
- }
31
- return resolve(`Failed to execute Spotify command: ${error.message}`);
32
- }
33
-
34
- const actionText = {
35
- 'play': 'Playing Spotify.',
36
- 'pause': 'Paused Spotify.',
37
- 'next': 'Skipped to the next song.',
38
- 'previous': 'Went back to the previous song.'
39
- };
40
-
41
- resolve(actionText[target.toLowerCase()]);
42
- });
43
- });
44
- }
135
+ const raw = (target || '').trim().toLowerCase();
136
+
137
+ // ── Basic playback commands ───────────────────────────────────────
138
+ if (ACTION_MAP[raw]) {
139
+ const result = await ACTION_MAP[raw]();
140
+ if (!result.ok) return formatError(result.error);
141
+ return ACTION_MESSAGES[raw];
142
+ }
143
+
144
+ // ── Now Playing ───────────────────────────────────────────────────
145
+ if (raw === 'now_playing' || raw === 'status' || raw === 'what\'s playing' || raw === 'current') {
146
+ return await getNowPlaying();
147
+ }
148
+
149
+ // ── Volume ────────────────────────────────────────────────────────
150
+ if (raw.startsWith('volume')) {
151
+ const levelStr = raw.replace('volume', '').trim();
152
+ return await setVolume(levelStr);
153
+ }
154
+
155
+ // ── Shuffle ───────────────────────────────────────────────────────
156
+ if (raw.startsWith('shuffle')) {
157
+ const state = raw.replace('shuffle', '').trim() || 'toggle';
158
+ return await setShuffle(state);
159
+ }
160
+
161
+ // ── Search ────────────────────────────────────────────────────────
162
+ if (raw.startsWith('search')) {
163
+ const query = target.replace(/^search\s*/i, '').trim();
164
+ return searchSpotify(query);
165
+ }
166
+
167
+ // ── Fallback: try as playerctl arg directly ───────────────────────
168
+ return `āš ļø ą¹„ąø”ą¹ˆąø£ąø¹ą¹‰ąøˆąø±ąøąø„ąø³ąøŖąø±ą¹ˆąø‡ Spotify: "${target}"\nąø„ąø³ąøŖąø±ą¹ˆąø‡ąø—ąøµą¹ˆąø£ąø­ąø‡ąø£ąø±ąøš: play, pause, stop, next, previous, now_playing, volume <0-100>, shuffle on/off, search <query>`;
169
+ },
170
+
171
+ // Expose helpers for testing
172
+ _helpers: { runPlayerctl, getNowPlaying, setVolume, setShuffle, searchSpotify }
45
173
  };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Mint System Monitor Plugin
3
+ * --------------------------
4
+ * Provides real-time system statistics for the host machine.
5
+ * Uses standard Linux commands (uptime, free, df) for lightweight monitoring.
6
+ */
7
+
8
+ const { exec } = require('child_process');
9
+ const { promisify } = require('util');
10
+ const execAsync = promisify(exec);
11
+ const os = require('os');
12
+
13
+ async function getStats() {
14
+ try {
15
+ const [uptime, free, df] = await Promise.all([
16
+ execAsync('uptime -p'),
17
+ execAsync('free -h'),
18
+ execAsync('df -h / --output=pcent,avail')
19
+ ]);
20
+
21
+ // Parse Memory
22
+ const memLines = free.stdout.split('\n');
23
+ const memLine = memLines.find(l => l.startsWith('Mem:')) || '';
24
+ const memParts = memLine.split(/\s+/).filter(Boolean);
25
+ const memUsed = memParts[2] || 'Unknown';
26
+ const memTotal = memParts[1] || 'Unknown';
27
+
28
+ // Parse Disk
29
+ const diskLines = df.stdout.trim().split('\n');
30
+ const diskLine = diskLines[1] || '';
31
+ const [diskPercent, diskAvail] = diskLine.trim().split(/\s+/);
32
+
33
+ const cpuLoad = os.loadavg()[0].toFixed(2);
34
+ const cpuCores = os.cpus().length;
35
+
36
+ let report = `šŸ“Š **System Health Report**\n`;
37
+ report += `ā±ļø **Uptime:** ${uptime.stdout.trim()}\n`;
38
+ report += `šŸ’» **CPU Load:** ${cpuLoad} (on ${cpuCores} cores)\n`;
39
+ report += `🧠 **Memory:** ${memUsed} / ${memTotal} used\n`;
40
+ report += `šŸ’½ **Disk (/):** ${diskAvail} available (${diskPercent} full)`;
41
+
42
+ return report;
43
+ } catch (err) {
44
+ return `āŒ Error fetching system stats: ${err.message}`;
45
+ }
46
+ }
47
+
48
+ module.exports = {
49
+ name: 'system_monitor',
50
+ description: 'Provides system statistics like CPU load, memory usage, disk space, and uptime. Target can be "stats", "cpu", "memory", or "disk".',
51
+
52
+ async execute(target) {
53
+ const cmd = (target || 'stats').toLowerCase().trim();
54
+
55
+ switch (cmd) {
56
+ case 'stats':
57
+ case 'health':
58
+ return await getStats();
59
+ case 'cpu':
60
+ return `šŸ’» **CPU Load (1m):** ${os.loadavg()[0].toFixed(2)}\nCores: ${os.cpus().length}\nModel: ${os.cpus()[0].model}`;
61
+ case 'memory':
62
+ case 'ram':
63
+ const { stdout: mem } = await execAsync('free -h');
64
+ return `🧠 **Memory Status:**\n\`\`\`\n${mem}\`\`\``;
65
+ case 'disk':
66
+ const { stdout: disk } = await execAsync('df -h /');
67
+ return `šŸ’½ **Disk Status:**\n\`\`\`\n${disk}\`\`\``;
68
+ default:
69
+ return await getStats();
70
+ }
71
+ }
72
+ };
@@ -10,14 +10,27 @@ try {
10
10
  app = null;
11
11
  }
12
12
 
13
- const MINT_DIR = path.join(os.homedir(), '.mint');
14
- if (!fs.existsSync(MINT_DIR)) {
15
- fs.mkdirSync(MINT_DIR, { recursive: true });
13
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'mint');
14
+ const LEGACY_DIR = path.join(os.homedir(), '.mint');
15
+
16
+ if (!fs.existsSync(CONFIG_DIR)) {
17
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
18
+ }
19
+
20
+ // Migration: If old .mint exists but new .config/mint is empty, move files
21
+ if (fs.existsSync(LEGACY_DIR) && fs.readdirSync(CONFIG_DIR).length === 0) {
22
+ try {
23
+ const files = fs.readdirSync(LEGACY_DIR);
24
+ for (const file of files) {
25
+ fs.copyFileSync(path.join(LEGACY_DIR, file), path.join(CONFIG_DIR, file));
26
+ }
27
+ console.log('[Config] Migrated settings from ~/.mint to ~/.config/mint');
28
+ } catch (e) {
29
+ console.error('[Config] Migration failed:', e);
30
+ }
16
31
  }
17
32
 
18
- const CONFIG_PATH = app && app.getPath
19
- ? path.join(app.getPath('userData'), 'mint-config.json')
20
- : path.join(MINT_DIR, 'mint-config.json');
33
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'mint-config.json');
21
34
 
22
35
  const DEFAULT_CONFIG = {
23
36
  theme: 'dark',
@@ -43,9 +56,22 @@ const DEFAULT_CONFIG = {
43
56
  pluginSpotifyEnabled: true,
44
57
  pluginCalendarEnabled: false,
45
58
  pluginDiscordEnabled: false,
46
- showDesktopWidget: true
59
+ showDesktopWidget: true,
60
+ mcpServers: {},
61
+ anthropicApiKey: '',
62
+ openaiApiKey: '',
63
+ hfApiKey: '',
64
+ anthropicModel: 'claude-3-5-sonnet-latest',
65
+ openaiModel: 'gpt-4o',
66
+ hfModel: 'meta-llama/Meta-Llama-3-8B-Instruct',
67
+ localApiBaseUrl: 'http://localhost:1234/v1',
68
+ localModelName: 'local-model',
69
+ ollamaHost: 'http://localhost:11434',
70
+ enableAgentCollaboration: true
47
71
  };
48
72
 
73
+
74
+
49
75
  function readConfig() {
50
76
  try {
51
77
  if (!fs.existsSync(CONFIG_PATH)) {
@@ -70,4 +96,31 @@ function writeConfig(config) {
70
96
  }
71
97
  }
72
98
 
73
- module.exports = { readConfig, writeConfig, CONFIG_PATH };
99
+ function getAvailableProviders(config) {
100
+ const providers = [];
101
+ const cfg = config || readConfig();
102
+
103
+ const isPlaceholder = (val) => !val || val.startsWith('your_') || val.includes('key_here') || val.trim() === '';
104
+
105
+ // Check which providers have API keys or URLs configured
106
+ const anthropicKey = cfg.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
107
+ if (!isPlaceholder(anthropicKey)) providers.push('anthropic');
108
+
109
+ const openaiKey = cfg.openaiApiKey || process.env.OPENAI_API_KEY;
110
+ if (!isPlaceholder(openaiKey)) providers.push('openai');
111
+
112
+ const geminiKey = cfg.apiKey || process.env.GEMINI_API_KEY;
113
+ if (!isPlaceholder(geminiKey)) providers.push('gemini');
114
+
115
+ const hfKey = cfg.hfApiKey || process.env.HF_API_KEY;
116
+ if (!isPlaceholder(hfKey)) providers.push('huggingface');
117
+
118
+ if (cfg.localApiBaseUrl && cfg.localApiBaseUrl.trim() !== '') providers.push('local_openai');
119
+
120
+ // Always push ollama at the end since it's local
121
+ providers.push('ollama');
122
+
123
+ return providers;
124
+ }
125
+
126
+ module.exports = { readConfig, writeConfig, getAvailableProviders, CONFIG_PATH };