@pheem49/mint 1.3.0 ā 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.
- package/README.md +28 -24
- package/mint-cli.js +201 -26
- package/package.json +13 -2
- package/src/AI_Brain/Gemini_API.js +299 -46
- package/src/AI_Brain/agent_orchestrator.js +73 -0
- package/src/AI_Brain/autonomous_brain.js +2 -0
- package/src/AI_Brain/memory_store.js +318 -0
- package/src/CLI/chat_router.js +17 -2
- package/src/CLI/chat_ui.js +83 -1
- package/src/CLI/code_agent.js +143 -30
- package/src/CLI/onboarding.js +53 -6
- package/src/CLI/workspace_manager.js +81 -0
- package/src/Plugins/spotify.js +168 -40
- package/src/Plugins/system_monitor.js +72 -0
- package/src/System/config_manager.js +35 -2
- package/src/System/notifications.js +23 -0
- package/src/UI/settings.html +143 -65
- package/src/UI/settings.js +155 -41
- package/tests/agent_orchestrator.test.js +41 -0
- package/tests/config_manager.test.js +141 -0
- package/tests/memory_store.test.js +185 -0
- package/tests/spotify.test.js +201 -0
- package/tests/system_monitor.test.js +37 -0
- package/tests/workspace_manager.test.js +41 -0
package/src/CLI/onboarding.js
CHANGED
|
@@ -18,28 +18,75 @@ async function runOnboarding(options = {}) {
|
|
|
18
18
|
{
|
|
19
19
|
type: 'input',
|
|
20
20
|
name: 'apiKey',
|
|
21
|
-
message: '
|
|
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: '
|
|
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
|
-
'
|
|
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
|
+
};
|
package/src/Plugins/spotify.js
CHANGED
|
@@ -1,45 +1,173 @@
|
|
|
1
|
-
|
|
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:
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
};
|
|
@@ -60,8 +60,14 @@ const DEFAULT_CONFIG = {
|
|
|
60
60
|
mcpServers: {},
|
|
61
61
|
anthropicApiKey: '',
|
|
62
62
|
openaiApiKey: '',
|
|
63
|
+
hfApiKey: '',
|
|
63
64
|
anthropicModel: 'claude-3-5-sonnet-latest',
|
|
64
|
-
openaiModel: 'gpt-4o'
|
|
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
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
|
|
@@ -90,4 +96,31 @@ function writeConfig(config) {
|
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
|
|
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 };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint Notification System
|
|
3
|
+
* ------------------------
|
|
4
|
+
* Sends system-level notifications to the user.
|
|
5
|
+
* Supports Linux (notify-send) as a primary target for CLI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { exec } = require('child_process');
|
|
9
|
+
|
|
10
|
+
function sendNotification(title, message, urgency = 'normal') {
|
|
11
|
+
// Attempt to use notify-send (Linux)
|
|
12
|
+
const cmd = `notify-send -u ${urgency} "${title}" "${message}"`;
|
|
13
|
+
exec(cmd, (err) => {
|
|
14
|
+
if (err) {
|
|
15
|
+
// Fallback: Silent console log if no notifier found
|
|
16
|
+
console.log(`[Notification] ${title}: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
sendNotification
|
|
23
|
+
};
|