@pheem49/mint 1.2.4 → 1.3.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.
@@ -0,0 +1,443 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFile } = require('child_process');
4
+ const { promisify } = require('util');
5
+ const { GoogleGenAI } = require('@google/genai');
6
+ const { readConfig } = require('../System/config_manager');
7
+ const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
11
+ const MAX_TOOL_OUTPUT = 12000;
12
+ const MAX_AGENT_STEPS = 16;
13
+
14
+ const CODE_AGENT_PROMPT = `You are Mint Code Mode, a careful coding agent for a local workspace.
15
+
16
+ You help with software development tasks inside the provided working directory.
17
+ Work in an inspect -> plan -> act -> verify loop.
18
+
19
+ Rules:
20
+ 1. Respond with valid JSON only.
21
+ 2. Prefer reading files and searching before editing.
22
+ 3. Make focused edits that preserve existing project style.
23
+ 4. Use shell commands for inspection, tests, and formatting when useful.
24
+ 5. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
25
+ 6. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
26
+ 7. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
27
+ 8. Use "write_file" only for new files or when a full rewrite is clearly safer.
28
+ 9. When you are done, return "finish" with a concise summary, verification, and an updated session summary.
29
+
30
+ Response format:
31
+ {
32
+ "thought": "short reasoning",
33
+ "action": "list_files" | "read_file" | "search_code" | "run_shell" | "apply_patch" | "write_file" | "finish",
34
+ "input": {
35
+ "path": "relative/path",
36
+ "query": "search text",
37
+ "command": "shell command",
38
+ "startLine": 1,
39
+ "endLine": 120,
40
+ "content": "full file content for write_file",
41
+ "summary": "final summary",
42
+ "verification": "tests or checks",
43
+ "sessionSummary": "brief persistent summary for the workspace",
44
+ "patch": {
45
+ "path": "relative/path",
46
+ "hunks": [
47
+ {
48
+ "oldText": "exact existing text",
49
+ "newText": "replacement text"
50
+ }
51
+ ]
52
+ }
53
+ }
54
+ }
55
+
56
+ Tool notes:
57
+ - "list_files": inspect the workspace or a subdirectory.
58
+ - "read_file": read a file, optionally with startLine/endLine.
59
+ - "search_code": search by text or regex-like pattern.
60
+ - "run_shell": run a non-destructive command in the workspace.
61
+ - "apply_patch": update an existing file using one or more exact replacement hunks.
62
+ - "write_file": create a new file or fully rewrite a file when replacement is not practical.
63
+ - "finish": stop once the task is complete or blocked.
64
+ `;
65
+
66
+ function truncate(text, max = MAX_TOOL_OUTPUT) {
67
+ if (!text) return '';
68
+ return text.length > max ? `${text.slice(0, max)}\n...<truncated>` : text;
69
+ }
70
+
71
+ function extractJson(text) {
72
+ try {
73
+ return JSON.parse(text);
74
+ } catch (error) {
75
+ const match = text.match(/\{[\s\S]*\}/);
76
+ if (!match) {
77
+ throw error;
78
+ }
79
+ return JSON.parse(match[0]);
80
+ }
81
+ }
82
+
83
+ function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
84
+ const resolved = path.resolve(workspaceRoot, targetPath);
85
+ const relative = path.relative(workspaceRoot, resolved);
86
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
87
+ throw new Error(`Path is outside the workspace: ${targetPath}`);
88
+ }
89
+ return resolved;
90
+ }
91
+
92
+ async function safeExecFile(command, args, options = {}) {
93
+ try {
94
+ return await execFileAsync(command, args, {
95
+ maxBuffer: 1024 * 1024 * 4,
96
+ ...options
97
+ });
98
+ } catch (error) {
99
+ if (typeof error.code === 'number' && error.code === 1) {
100
+ return { stdout: error.stdout || '', stderr: error.stderr || '' };
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ async function listFiles(workspaceRoot, targetPath = '.') {
107
+ const cwd = resolveWorkspacePath(workspaceRoot, targetPath);
108
+ try {
109
+ const { stdout } = await execFileAsync('rg', ['--files', cwd], { cwd: workspaceRoot, maxBuffer: 1024 * 1024 * 4 });
110
+ const rel = stdout
111
+ .split('\n')
112
+ .filter(Boolean)
113
+ .map(file => path.relative(workspaceRoot, file))
114
+ .slice(0, 400)
115
+ .join('\n');
116
+ return rel || '(no files found)';
117
+ } catch (error) {
118
+ if (error.code !== 'ENOENT' && error.stdout) {
119
+ return truncate(error.stdout);
120
+ }
121
+ const entries = fs.readdirSync(cwd, { withFileTypes: true })
122
+ .slice(0, 200)
123
+ .map(entry => `${entry.isDirectory() ? '[dir]' : '[file]'} ${path.relative(workspaceRoot, path.join(cwd, entry.name))}`)
124
+ .join('\n');
125
+ return entries || '(empty directory)';
126
+ }
127
+ }
128
+
129
+ function readFileRange(workspaceRoot, targetPath, startLine = 1, endLine = 200) {
130
+ const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
131
+ const content = fs.readFileSync(resolved, 'utf8');
132
+ const lines = content.split('\n');
133
+ const start = Math.max(1, startLine);
134
+ const end = Math.max(start, endLine);
135
+ return lines
136
+ .slice(start - 1, end)
137
+ .map((line, index) => `${start + index}: ${line}`)
138
+ .join('\n');
139
+ }
140
+
141
+ async function searchCode(workspaceRoot, query) {
142
+ if (!query || !query.trim()) {
143
+ throw new Error('Search query is required.');
144
+ }
145
+ try {
146
+ const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, workspaceRoot], {
147
+ cwd: workspaceRoot,
148
+ maxBuffer: 1024 * 1024 * 4
149
+ });
150
+ return truncate(stdout || '(no matches)');
151
+ } catch (error) {
152
+ if (typeof error.code === 'number' && error.code === 1) {
153
+ return '(no matches)';
154
+ }
155
+ if (error.stdout) {
156
+ return truncate(error.stdout);
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ function assertSafeShell(command) {
163
+ const blockedPatterns = [
164
+ /\brm\s+-rf\b/,
165
+ /\bgit\s+reset\s+--hard\b/,
166
+ /\bgit\s+checkout\s+--\b/,
167
+ /\bmkfs\b/,
168
+ /\bshutdown\b/,
169
+ /\breboot\b/,
170
+ />\s*\/dev\//,
171
+ /\bcurl\b.*\|\s*(sh|bash)\b/,
172
+ /\bwget\b.*\|\s*(sh|bash)\b/
173
+ ];
174
+
175
+ if (blockedPatterns.some(pattern => pattern.test(command))) {
176
+ throw new Error(`Blocked unsafe command: ${command}`);
177
+ }
178
+ }
179
+
180
+ async function runShell(workspaceRoot, command) {
181
+ if (!command || !command.trim()) {
182
+ throw new Error('Shell command is required.');
183
+ }
184
+ assertSafeShell(command);
185
+ const { stdout, stderr } = await execFileAsync('bash', ['-lc', command], {
186
+ cwd: workspaceRoot,
187
+ maxBuffer: 1024 * 1024 * 4
188
+ });
189
+ return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
190
+ }
191
+
192
+ function formatPatchPreview(patchInput) {
193
+ const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
194
+ const preview = hunks
195
+ .slice(0, 3)
196
+ .map((hunk, index) => {
197
+ const oldPreview = truncate(hunk.oldText || '', 240);
198
+ const newPreview = truncate(hunk.newText || '', 240);
199
+ return [
200
+ `Hunk ${index + 1}:`,
201
+ '--- old',
202
+ oldPreview,
203
+ '+++ new',
204
+ newPreview
205
+ ].join('\n');
206
+ })
207
+ .join('\n\n');
208
+ return `${patchInput.path}\n${preview}`;
209
+ }
210
+
211
+ function applyPatch(workspaceRoot, patchInput) {
212
+ if (!patchInput || !patchInput.path) {
213
+ throw new Error('Patch path is required.');
214
+ }
215
+ const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
216
+ if (!fs.existsSync(resolved)) {
217
+ throw new Error(`Patch target does not exist: ${patchInput.path}`);
218
+ }
219
+
220
+ const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
221
+ if (hunks.length === 0) {
222
+ throw new Error('Patch hunks are required.');
223
+ }
224
+
225
+ let content = fs.readFileSync(resolved, 'utf8');
226
+ hunks.forEach((hunk, index) => {
227
+ if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
228
+ throw new Error(`Patch hunk ${index + 1} is invalid.`);
229
+ }
230
+ if (!content.includes(hunk.oldText)) {
231
+ throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
232
+ }
233
+ content = content.replace(hunk.oldText, hunk.newText);
234
+ });
235
+
236
+ fs.writeFileSync(resolved, content, 'utf8');
237
+ return `Patched ${patchInput.path} with ${hunks.length} hunk(s).`;
238
+ }
239
+
240
+ function writeFile(workspaceRoot, targetPath, content) {
241
+ const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
242
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
243
+ fs.writeFileSync(resolved, content || '', 'utf8');
244
+ return `Wrote ${targetPath}`;
245
+ }
246
+
247
+ function getAiClientAndModel() {
248
+ const config = readConfig();
249
+ const apiKey = (config.apiKey || process.env.GEMINI_API_KEY || '').trim();
250
+ if (!apiKey) {
251
+ throw new Error("Missing Gemini API key. Run 'mint onboard' first.");
252
+ }
253
+ return {
254
+ ai: new GoogleGenAI({ apiKey }),
255
+ model: (config.geminiModel || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL
256
+ };
257
+ }
258
+
259
+ function detectPackageManager(workspaceRoot) {
260
+ if (fs.existsSync(path.join(workspaceRoot, 'package-lock.json'))) return 'npm';
261
+ if (fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'))) return 'pnpm';
262
+ if (fs.existsSync(path.join(workspaceRoot, 'yarn.lock'))) return 'yarn';
263
+ if (fs.existsSync(path.join(workspaceRoot, 'bun.lockb')) || fs.existsSync(path.join(workspaceRoot, 'bun.lock'))) return 'bun';
264
+ return 'npm';
265
+ }
266
+
267
+ function detectTestCommands(workspaceRoot) {
268
+ const commands = [];
269
+ const packageJsonPath = path.join(workspaceRoot, 'package.json');
270
+ if (fs.existsSync(packageJsonPath)) {
271
+ try {
272
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
273
+ const scripts = pkg.scripts || {};
274
+ if (scripts.test) commands.push(`${detectPackageManager(workspaceRoot)} test`);
275
+ if (scripts.lint) commands.push(`${detectPackageManager(workspaceRoot)} run lint`);
276
+ if (scripts.build) commands.push(`${detectPackageManager(workspaceRoot)} run build`);
277
+ } catch (error) {
278
+ // Ignore malformed package.json for context gathering.
279
+ }
280
+ }
281
+ return commands;
282
+ }
283
+
284
+ async function getGitContext(workspaceRoot) {
285
+ const gitDir = path.join(workspaceRoot, '.git');
286
+ if (!fs.existsSync(gitDir)) {
287
+ return {
288
+ isRepo: false,
289
+ branch: '(not a git repo)',
290
+ status: '',
291
+ diffSummary: ''
292
+ };
293
+ }
294
+
295
+ const branch = (await safeExecFile('git', ['branch', '--show-current'], { cwd: workspaceRoot })).stdout.trim() || '(detached HEAD)';
296
+ const status = truncate((await safeExecFile('git', ['status', '--short'], { cwd: workspaceRoot })).stdout.trim() || '(clean)');
297
+ const diffSummary = truncate((await safeExecFile('git', ['diff', '--stat'], { cwd: workspaceRoot })).stdout.trim() || '(no unstaged diff)');
298
+ return { isRepo: true, branch, status, diffSummary };
299
+ }
300
+
301
+ async function buildInitialObservation(task, workspaceRoot) {
302
+ const session = readWorkspaceSession(workspaceRoot);
303
+ const gitContext = await getGitContext(workspaceRoot);
304
+ const testCommands = detectTestCommands(workspaceRoot);
305
+
306
+ return [
307
+ `Task: ${task}`,
308
+ `Workspace: ${workspaceRoot}`,
309
+ `Git branch: ${gitContext.branch}`,
310
+ 'Git status:',
311
+ gitContext.status || '(none)',
312
+ 'Git diff summary:',
313
+ gitContext.diffSummary || '(none)',
314
+ 'Suggested verification commands:',
315
+ testCommands.length > 0 ? testCommands.join('\n') : '(none detected)',
316
+ 'Previous workspace session summary:',
317
+ session.summary || '(none)',
318
+ `Previous task: ${session.lastTask || '(none)'}`,
319
+ `Previous verification: ${session.lastVerification || '(none)'}`,
320
+ 'Start by inspecting the workspace before making edits unless the task is trivial.'
321
+ ].join('\n');
322
+ }
323
+
324
+ async function executeCodeTask(task, options = {}) {
325
+ const workspaceRoot = path.resolve(options.cwd || process.cwd());
326
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
327
+ const requestApproval = typeof options.requestApproval === 'function'
328
+ ? options.requestApproval
329
+ : async () => true;
330
+ const { ai, model } = getAiClientAndModel();
331
+
332
+ const chat = ai.chats.create({
333
+ model,
334
+ config: {
335
+ systemInstruction: CODE_AGENT_PROMPT,
336
+ responseMimeType: 'application/json'
337
+ },
338
+ history: []
339
+ });
340
+
341
+ let observation = await buildInitialObservation(task, workspaceRoot);
342
+
343
+ for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
344
+ onProgress(`Step ${step}: thinking`);
345
+ const response = await chat.sendMessage({ message: [{ text: observation }] });
346
+ const text = typeof response.text === 'function' ? response.text() : response.text;
347
+ const decision = extractJson(text);
348
+ const action = decision.action;
349
+ const input = decision.input || {};
350
+
351
+ onProgress(`Step ${step}: ${action}${input.path ? ` ${input.path}` : input.command ? ` ${input.command}` : ''}`);
352
+
353
+ if (action === 'finish') {
354
+ const sessionSummary = input.sessionSummary || input.summary || task;
355
+ writeWorkspaceSession(workspaceRoot, {
356
+ summary: sessionSummary,
357
+ lastTask: task,
358
+ lastVerification: input.verification || 'Not specified.'
359
+ });
360
+ return {
361
+ summary: input.summary || 'Task complete.',
362
+ verification: input.verification || 'Not specified.',
363
+ steps: step
364
+ };
365
+ }
366
+
367
+ let toolResult = '';
368
+ switch (action) {
369
+ case 'list_files':
370
+ toolResult = await listFiles(workspaceRoot, input.path || '.');
371
+ break;
372
+ case 'read_file':
373
+ toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
374
+ break;
375
+ case 'search_code':
376
+ toolResult = await searchCode(workspaceRoot, input.query);
377
+ break;
378
+ case 'run_shell': {
379
+ const approved = await requestApproval({
380
+ type: 'shell',
381
+ label: input.command,
382
+ preview: input.command
383
+ });
384
+ if (!approved) {
385
+ toolResult = `User denied shell command: ${input.command}`;
386
+ break;
387
+ }
388
+ toolResult = await runShell(workspaceRoot, input.command);
389
+ break;
390
+ }
391
+ case 'apply_patch': {
392
+ const patchInput = input.patch || {};
393
+ const approved = await requestApproval({
394
+ type: 'patch',
395
+ label: patchInput.path,
396
+ preview: formatPatchPreview(patchInput)
397
+ });
398
+ if (!approved) {
399
+ toolResult = `User denied patch for ${patchInput.path}`;
400
+ break;
401
+ }
402
+ toolResult = applyPatch(workspaceRoot, patchInput);
403
+ break;
404
+ }
405
+ case 'write_file': {
406
+ const approved = await requestApproval({
407
+ type: 'write_file',
408
+ label: input.path,
409
+ preview: `${input.path}\n${truncate(input.content || '', 800)}`
410
+ });
411
+ if (!approved) {
412
+ toolResult = `User denied full file write for ${input.path}`;
413
+ break;
414
+ }
415
+ toolResult = writeFile(workspaceRoot, input.path, input.content);
416
+ break;
417
+ }
418
+ default:
419
+ throw new Error(`Unsupported action: ${action}`);
420
+ }
421
+
422
+ observation = [
423
+ `Previous thought: ${decision.thought || '(none)'}`,
424
+ `Action: ${action}`,
425
+ 'Observation:',
426
+ toolResult
427
+ ].join('\n');
428
+ }
429
+
430
+ writeWorkspaceSession(workspaceRoot, {
431
+ summary: `Task stopped before completion: ${task}`,
432
+ lastTask: task,
433
+ lastVerification: 'Agent limit reached before explicit completion.'
434
+ });
435
+
436
+ return {
437
+ summary: 'Stopped after reaching the maximum number of agent steps.',
438
+ verification: 'Agent limit reached before explicit completion.',
439
+ steps: MAX_AGENT_STEPS
440
+ };
441
+ }
442
+
443
+ module.exports = { executeCodeTask };
@@ -0,0 +1,62 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { CONFIG_PATH } = require('../System/config_manager');
5
+
6
+ const SESSION_FILE = path.join(path.dirname(CONFIG_PATH), 'code-sessions.json');
7
+
8
+ function ensureSessionStore() {
9
+ if (!fs.existsSync(SESSION_FILE)) {
10
+ fs.writeFileSync(SESSION_FILE, JSON.stringify({}, null, 2), 'utf8');
11
+ }
12
+ }
13
+
14
+ function readAllSessions() {
15
+ ensureSessionStore();
16
+ try {
17
+ return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
18
+ } catch (error) {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function writeAllSessions(data) {
24
+ ensureSessionStore();
25
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), 'utf8');
26
+ }
27
+
28
+ function getWorkspaceKey(workspaceRoot) {
29
+ return crypto.createHash('sha1').update(path.resolve(workspaceRoot)).digest('hex');
30
+ }
31
+
32
+ function readWorkspaceSession(workspaceRoot) {
33
+ const sessions = readAllSessions();
34
+ const key = getWorkspaceKey(workspaceRoot);
35
+ return sessions[key] || {
36
+ workspaceRoot: path.resolve(workspaceRoot),
37
+ summary: '',
38
+ lastTask: '',
39
+ lastVerification: '',
40
+ updatedAt: null
41
+ };
42
+ }
43
+
44
+ function writeWorkspaceSession(workspaceRoot, updates) {
45
+ const sessions = readAllSessions();
46
+ const key = getWorkspaceKey(workspaceRoot);
47
+ const current = readWorkspaceSession(workspaceRoot);
48
+ sessions[key] = {
49
+ ...current,
50
+ ...updates,
51
+ workspaceRoot: path.resolve(workspaceRoot),
52
+ updatedAt: new Date().toISOString()
53
+ };
54
+ writeAllSessions(sessions);
55
+ return sessions[key];
56
+ }
57
+
58
+ module.exports = {
59
+ readWorkspaceSession,
60
+ writeWorkspaceSession,
61
+ SESSION_FILE
62
+ };
@@ -21,6 +21,7 @@ function displayFeatures() {
21
21
  console.log(`\n${colors.bright}CLI Commands:${colors.reset}`);
22
22
  const commands = [
23
23
  { cmd: 'mint', desc: 'Start interactive chat session (Default)' },
24
+ { cmd: 'mint code "<task>"', desc: 'Run workspace-aware coding agent in current directory' },
24
25
  { cmd: 'mint onboard', desc: 'Run setup wizard (API Key, Model, Daemon)' },
25
26
  { cmd: 'mint agent', desc: 'Run Mint as a background agent (Headless)' },
26
27
  { cmd: 'mint list', desc: 'Show this features & commands list' }
@@ -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
 
@@ -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,16 @@ 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
+ anthropicModel: 'claude-3-5-sonnet-latest',
64
+ openaiModel: 'gpt-4o'
47
65
  };
48
66
 
67
+
68
+
49
69
  function readConfig() {
50
70
  try {
51
71
  if (!fs.existsSync(CONFIG_PATH)) {