@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.
- package/README.md +28 -0
- package/index.html +16 -0
- package/main.js +36 -83
- package/mint-cli-logic.js +19 -0
- package/mint-cli.js +117 -15
- package/package.json +8 -2
- package/src/AI_Brain/Gemini_API.js +175 -9
- package/src/AI_Brain/knowledge_base.js +199 -125
- package/src/Automation_Layer/file_operations.js +41 -19
- package/src/CLI/chat_router.js +166 -0
- package/src/CLI/chat_ui.js +239 -110
- package/src/CLI/code_agent.js +443 -0
- package/src/CLI/code_session_memory.js +62 -0
- package/src/CLI/list_features.js +1 -0
- package/src/Plugins/mcp_manager.js +95 -0
- package/src/Plugins/plugin_manager.js +2 -2
- package/src/System/config_manager.js +27 -7
- package/src/System/granular_automation.js +88 -0
- package/src/UI/settings.html +24 -0
- package/src/UI/settings.js +98 -1
|
@@ -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
|
+
};
|
package/src/CLI/list_features.js
CHANGED
|
@@ -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
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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 =
|
|
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)) {
|