@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.
@@ -0,0 +1,556 @@
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 axios = require('axios');
7
+ const { readConfig, getAvailableProviders } = require('../System/config_manager');
8
+ const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
9
+
10
+ const execFileAsync = promisify(execFile);
11
+ const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
12
+ const MAX_TOOL_OUTPUT = 12000;
13
+ const MAX_AGENT_STEPS = 16;
14
+
15
+ const CODE_AGENT_PROMPT = `You are Mint Code Mode, a careful coding agent for a local workspace.
16
+
17
+ You help with software development tasks inside the provided working directory.
18
+ Work in an inspect -> plan -> act -> verify loop.
19
+
20
+ Rules:
21
+ 1. Respond with valid JSON only.
22
+ 2. Prefer reading files and searching before editing.
23
+ 3. Make focused edits that preserve existing project style.
24
+ 4. Use shell commands for inspection, tests, and formatting when useful.
25
+ 5. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
26
+ 6. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
27
+ 7. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
28
+ 8. Use "write_file" only for new files or when a full rewrite is clearly safer.
29
+ 9. When you are done, return "finish" with a concise summary, verification, and an updated session summary.
30
+
31
+ Response format:
32
+ {
33
+ "thought": "short reasoning",
34
+ "action": "list_files" | "read_file" | "search_code" | "run_shell" | "apply_patch" | "write_file" | "finish",
35
+ "input": {
36
+ "path": "relative/path",
37
+ "query": "search text",
38
+ "command": "shell command",
39
+ "startLine": 1,
40
+ "endLine": 120,
41
+ "content": "full file content for write_file",
42
+ "summary": "final summary",
43
+ "verification": "tests or checks",
44
+ "sessionSummary": "brief persistent summary for the workspace",
45
+ "patch": {
46
+ "path": "relative/path",
47
+ "hunks": [
48
+ {
49
+ "oldText": "exact existing text",
50
+ "newText": "replacement text"
51
+ }
52
+ ]
53
+ }
54
+ }
55
+ }
56
+
57
+ Tool notes:
58
+ - "list_files": inspect the workspace or a subdirectory.
59
+ - "read_file": read a file, optionally with startLine/endLine.
60
+ - "search_code": search by text or regex-like pattern.
61
+ - "run_shell": run a non-destructive command in the workspace.
62
+ - "apply_patch": update an existing file using one or more exact replacement hunks.
63
+ - "write_file": create a new file or fully rewrite a file when replacement is not practical.
64
+ - "finish": stop once the task is complete or blocked.
65
+ `;
66
+
67
+ function truncate(text, max = MAX_TOOL_OUTPUT) {
68
+ if (!text) return '';
69
+ return text.length > max ? `${text.slice(0, max)}\n...<truncated>` : text;
70
+ }
71
+
72
+ function extractJson(text) {
73
+ try {
74
+ return JSON.parse(text);
75
+ } catch (error) {
76
+ const match = text.match(/\{[\s\S]*\}/);
77
+ if (!match) {
78
+ throw error;
79
+ }
80
+ return JSON.parse(match[0]);
81
+ }
82
+ }
83
+
84
+ function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
85
+ const resolved = path.resolve(workspaceRoot, targetPath);
86
+ const relative = path.relative(workspaceRoot, resolved);
87
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
88
+ throw new Error(`Path is outside the workspace: ${targetPath}`);
89
+ }
90
+ return resolved;
91
+ }
92
+
93
+ async function safeExecFile(command, args, options = {}) {
94
+ try {
95
+ return await execFileAsync(command, args, {
96
+ maxBuffer: 1024 * 1024 * 4,
97
+ ...options
98
+ });
99
+ } catch (error) {
100
+ if (typeof error.code === 'number' && error.code === 1) {
101
+ return { stdout: error.stdout || '', stderr: error.stderr || '' };
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ async function listFiles(workspaceRoot, targetPath = '.') {
108
+ const cwd = resolveWorkspacePath(workspaceRoot, targetPath);
109
+ try {
110
+ const { stdout } = await execFileAsync('rg', ['--files', cwd], { cwd: workspaceRoot, maxBuffer: 1024 * 1024 * 4 });
111
+ const rel = stdout
112
+ .split('\n')
113
+ .filter(Boolean)
114
+ .map(file => path.relative(workspaceRoot, file))
115
+ .slice(0, 400)
116
+ .join('\n');
117
+ return rel || '(no files found)';
118
+ } catch (error) {
119
+ if (error.code !== 'ENOENT' && error.stdout) {
120
+ return truncate(error.stdout);
121
+ }
122
+ const entries = fs.readdirSync(cwd, { withFileTypes: true })
123
+ .slice(0, 200)
124
+ .map(entry => `${entry.isDirectory() ? '[dir]' : '[file]'} ${path.relative(workspaceRoot, path.join(cwd, entry.name))}`)
125
+ .join('\n');
126
+ return entries || '(empty directory)';
127
+ }
128
+ }
129
+
130
+ function readFileRange(workspaceRoot, targetPath, startLine = 1, endLine = 200) {
131
+ const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
132
+ const content = fs.readFileSync(resolved, 'utf8');
133
+ const lines = content.split('\n');
134
+ const start = Math.max(1, startLine);
135
+ const end = Math.max(start, endLine);
136
+ return lines
137
+ .slice(start - 1, end)
138
+ .map((line, index) => `${start + index}: ${line}`)
139
+ .join('\n');
140
+ }
141
+
142
+ async function searchCode(workspaceRoot, query) {
143
+ if (!query || !query.trim()) {
144
+ throw new Error('Search query is required.');
145
+ }
146
+ try {
147
+ const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, workspaceRoot], {
148
+ cwd: workspaceRoot,
149
+ maxBuffer: 1024 * 1024 * 4
150
+ });
151
+ return truncate(stdout || '(no matches)');
152
+ } catch (error) {
153
+ if (typeof error.code === 'number' && error.code === 1) {
154
+ return '(no matches)';
155
+ }
156
+ if (error.stdout) {
157
+ return truncate(error.stdout);
158
+ }
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ function assertSafeShell(command) {
164
+ const blockedPatterns = [
165
+ /\brm\s+-rf\b/,
166
+ /\bgit\s+reset\s+--hard\b/,
167
+ /\bgit\s+checkout\s+--\b/,
168
+ /\bmkfs\b/,
169
+ /\bshutdown\b/,
170
+ /\breboot\b/,
171
+ />\s*\/dev\//,
172
+ /\bcurl\b.*\|\s*(sh|bash)\b/,
173
+ /\bwget\b.*\|\s*(sh|bash)\b/
174
+ ];
175
+
176
+ if (blockedPatterns.some(pattern => pattern.test(command))) {
177
+ throw new Error(`Blocked unsafe command: ${command}`);
178
+ }
179
+ }
180
+
181
+ async function runShell(workspaceRoot, command) {
182
+ if (!command || !command.trim()) {
183
+ throw new Error('Shell command is required.');
184
+ }
185
+ assertSafeShell(command);
186
+ const { stdout, stderr } = await execFileAsync('bash', ['-lc', command], {
187
+ cwd: workspaceRoot,
188
+ maxBuffer: 1024 * 1024 * 4
189
+ });
190
+ return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
191
+ }
192
+
193
+ function formatPatchPreview(patchInput) {
194
+ const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
195
+ const preview = hunks
196
+ .slice(0, 3)
197
+ .map((hunk, index) => {
198
+ const oldPreview = truncate(hunk.oldText || '', 240);
199
+ const newPreview = truncate(hunk.newText || '', 240);
200
+ return [
201
+ `Hunk ${index + 1}:`,
202
+ '--- old',
203
+ oldPreview,
204
+ '+++ new',
205
+ newPreview
206
+ ].join('\n');
207
+ })
208
+ .join('\n\n');
209
+ return `${patchInput.path}\n${preview}`;
210
+ }
211
+
212
+ function applyPatch(workspaceRoot, patchInput) {
213
+ if (!patchInput || !patchInput.path) {
214
+ throw new Error('Patch path is required.');
215
+ }
216
+ const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
217
+ if (!fs.existsSync(resolved)) {
218
+ throw new Error(`Patch target does not exist: ${patchInput.path}`);
219
+ }
220
+
221
+ const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
222
+ if (hunks.length === 0) {
223
+ throw new Error('Patch hunks are required.');
224
+ }
225
+
226
+ let content = fs.readFileSync(resolved, 'utf8');
227
+ hunks.forEach((hunk, index) => {
228
+ if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
229
+ throw new Error(`Patch hunk ${index + 1} is invalid.`);
230
+ }
231
+ if (!content.includes(hunk.oldText)) {
232
+ throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
233
+ }
234
+ content = content.replace(hunk.oldText, hunk.newText);
235
+ });
236
+
237
+ fs.writeFileSync(resolved, content, 'utf8');
238
+ return `Patched ${patchInput.path} with ${hunks.length} hunk(s).`;
239
+ }
240
+
241
+ function writeFile(workspaceRoot, targetPath, content) {
242
+ const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
243
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
244
+ fs.writeFileSync(resolved, content || '', 'utf8');
245
+ return `Wrote ${targetPath}`;
246
+ }
247
+
248
+ class UnifiedAgentClient {
249
+ constructor(provider, config) {
250
+ this.provider = provider;
251
+ this.config = config;
252
+ this.history = [];
253
+ this.systemInstruction = CODE_AGENT_PROMPT;
254
+ }
255
+
256
+ async sendMessage(observation) {
257
+ this.history.push({ role: 'user', content: observation });
258
+
259
+ let responseText = '';
260
+ if (this.provider === 'anthropic') {
261
+ responseText = await this._callAnthropic();
262
+ } else if (this.provider === 'openai' || this.provider === 'local_openai') {
263
+ responseText = await this._callOpenAI();
264
+ } else {
265
+ responseText = await this._callGemini();
266
+ }
267
+
268
+ this.history.push({ role: 'assistant', content: responseText });
269
+ return responseText;
270
+ }
271
+
272
+ async _callAnthropic() {
273
+ const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
274
+ const messages = this.history.map(m => ({
275
+ role: m.role,
276
+ content: m.content
277
+ }));
278
+
279
+ const response = await axios.post('https://api.anthropic.com/v1/messages', {
280
+ model: this.config.anthropicModel || 'claude-3-5-sonnet-latest',
281
+ max_tokens: 8192,
282
+ system: this.systemInstruction,
283
+ messages: messages
284
+ }, {
285
+ headers: {
286
+ 'x-api-key': apiKey,
287
+ 'anthropic-version': '2023-06-01',
288
+ 'content-type': 'application/json'
289
+ }
290
+ });
291
+ return response.data.content[0].text;
292
+ }
293
+
294
+ async _callOpenAI() {
295
+ const isLocal = this.provider === 'local_openai';
296
+ const apiKey = isLocal ? 'not-needed' : (this.config.openaiApiKey || process.env.OPENAI_API_KEY);
297
+ const baseUrl = isLocal ? (this.config.localApiBaseUrl || 'http://localhost:1234/v1') : 'https://api.openai.com/v1';
298
+ const model = isLocal ? (this.config.localModelName || 'local-model') : (this.config.openaiModel || 'gpt-4o');
299
+
300
+ const messages = [
301
+ { role: 'system', content: this.systemInstruction },
302
+ ...this.history
303
+ ];
304
+
305
+ const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
306
+ model: model,
307
+ messages: messages,
308
+ response_format: isLocal ? undefined : { type: "json_object" }
309
+ }, {
310
+ headers: {
311
+ 'Authorization': `Bearer ${apiKey}`,
312
+ 'Content-Type': 'application/json'
313
+ }
314
+ });
315
+ return response.data.choices[0].message.content;
316
+ }
317
+
318
+ async _callGemini() {
319
+ const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
320
+ const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
321
+ const ai = new GoogleGenAI({ apiKey });
322
+
323
+ // Convert history for Gemini
324
+ const geminiHistory = this.history.slice(0, -1).map(m => ({
325
+ role: m.role === 'assistant' ? 'model' : 'user',
326
+ parts: [{ text: m.content }]
327
+ }));
328
+
329
+ const lastMessage = this.history[this.history.length - 1].content;
330
+
331
+ const chat = ai.chats.create({
332
+ model,
333
+ config: {
334
+ systemInstruction: this.systemInstruction,
335
+ responseMimeType: 'application/json'
336
+ },
337
+ history: geminiHistory
338
+ });
339
+
340
+ const response = await chat.sendMessage({ message: [{ text: lastMessage }] });
341
+ return typeof response.text === 'function' ? response.text() : response.text;
342
+ }
343
+ }
344
+
345
+ function detectPackageManager(workspaceRoot) {
346
+ if (fs.existsSync(path.join(workspaceRoot, 'package-lock.json'))) return 'npm';
347
+ if (fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'))) return 'pnpm';
348
+ if (fs.existsSync(path.join(workspaceRoot, 'yarn.lock'))) return 'yarn';
349
+ if (fs.existsSync(path.join(workspaceRoot, 'bun.lockb')) || fs.existsSync(path.join(workspaceRoot, 'bun.lock'))) return 'bun';
350
+ return 'npm';
351
+ }
352
+
353
+ function detectTestCommands(workspaceRoot) {
354
+ const commands = [];
355
+ const packageJsonPath = path.join(workspaceRoot, 'package.json');
356
+ if (fs.existsSync(packageJsonPath)) {
357
+ try {
358
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
359
+ const scripts = pkg.scripts || {};
360
+ if (scripts.test) commands.push(`${detectPackageManager(workspaceRoot)} test`);
361
+ if (scripts.lint) commands.push(`${detectPackageManager(workspaceRoot)} run lint`);
362
+ if (scripts.build) commands.push(`${detectPackageManager(workspaceRoot)} run build`);
363
+ } catch (error) {
364
+ // Ignore malformed package.json for context gathering.
365
+ }
366
+ }
367
+ return commands;
368
+ }
369
+
370
+ async function getGitContext(workspaceRoot) {
371
+ const gitDir = path.join(workspaceRoot, '.git');
372
+ if (!fs.existsSync(gitDir)) {
373
+ return {
374
+ isRepo: false,
375
+ branch: '(not a git repo)',
376
+ status: '',
377
+ diffSummary: ''
378
+ };
379
+ }
380
+
381
+ const branch = (await safeExecFile('git', ['branch', '--show-current'], { cwd: workspaceRoot })).stdout.trim() || '(detached HEAD)';
382
+ const status = truncate((await safeExecFile('git', ['status', '--short'], { cwd: workspaceRoot })).stdout.trim() || '(clean)');
383
+ const diffSummary = truncate((await safeExecFile('git', ['diff', '--stat'], { cwd: workspaceRoot })).stdout.trim() || '(no unstaged diff)');
384
+ return { isRepo: true, branch, status, diffSummary };
385
+ }
386
+
387
+ async function buildInitialObservation(task, workspaceRoot) {
388
+ const session = readWorkspaceSession(workspaceRoot);
389
+ const gitContext = await getGitContext(workspaceRoot);
390
+ const testCommands = detectTestCommands(workspaceRoot);
391
+
392
+ return [
393
+ `Task: ${task}`,
394
+ `Workspace: ${workspaceRoot}`,
395
+ `Git branch: ${gitContext.branch}`,
396
+ 'Git status:',
397
+ gitContext.status || '(none)',
398
+ 'Git diff summary:',
399
+ gitContext.diffSummary || '(none)',
400
+ 'Suggested verification commands:',
401
+ testCommands.length > 0 ? testCommands.join('\n') : '(none detected)',
402
+ 'Previous workspace session summary:',
403
+ session.summary || '(none)',
404
+ `Previous task: ${session.lastTask || '(none)'}`,
405
+ `Previous verification: ${session.lastVerification || '(none)'}`,
406
+ 'Start by inspecting the workspace before making edits unless the task is trivial.'
407
+ ].join('\n');
408
+ }
409
+
410
+ async function executeCodeTask(task, options = {}) {
411
+ const workspaceRoot = path.resolve(options.cwd || process.cwd());
412
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
413
+ const requestApproval = typeof options.requestApproval === 'function'
414
+ ? options.requestApproval
415
+ : async () => true;
416
+ const config = readConfig();
417
+ const provider = options.provider || 'gemini';
418
+ const client = new UnifiedAgentClient(provider, config);
419
+
420
+ let observation = await buildInitialObservation(task, workspaceRoot);
421
+
422
+ let finalSummary = '';
423
+ let finalVerification = '';
424
+ let finalSessionSummary = '';
425
+
426
+ for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
427
+ onProgress(`Step ${step}: thinking`);
428
+ const text = await client.sendMessage(observation);
429
+ const decision = extractJson(text);
430
+ const action = decision.action;
431
+ const input = decision.input || {};
432
+
433
+ onProgress(`Step ${step}: ${action}${input.path ? ` ${input.path}` : input.command ? ` ${input.command}` : ''}`);
434
+
435
+ if (action === 'finish') {
436
+ finalSessionSummary = input.sessionSummary || input.summary || task;
437
+ finalSummary = input.summary || 'Task complete.';
438
+ finalVerification = input.verification || 'Not specified.';
439
+ writeWorkspaceSession(workspaceRoot, {
440
+ summary: finalSessionSummary,
441
+ lastTask: task,
442
+ lastVerification: finalVerification
443
+ });
444
+ break;
445
+ }
446
+
447
+ let toolResult = '';
448
+ switch (action) {
449
+ case 'list_files':
450
+ toolResult = await listFiles(workspaceRoot, input.path || '.');
451
+ break;
452
+ case 'read_file':
453
+ toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
454
+ break;
455
+ case 'search_code':
456
+ toolResult = await searchCode(workspaceRoot, input.query);
457
+ break;
458
+ case 'run_shell': {
459
+ const approved = await requestApproval({
460
+ type: 'shell',
461
+ label: input.command,
462
+ preview: input.command
463
+ });
464
+ if (!approved) {
465
+ toolResult = `User denied shell command: ${input.command}`;
466
+ break;
467
+ }
468
+ toolResult = await runShell(workspaceRoot, input.command);
469
+ break;
470
+ }
471
+ case 'apply_patch': {
472
+ const patchInput = input.patch || {};
473
+ const approved = await requestApproval({
474
+ type: 'patch',
475
+ label: patchInput.path,
476
+ preview: formatPatchPreview(patchInput)
477
+ });
478
+ if (!approved) {
479
+ toolResult = `User denied patch for ${patchInput.path}`;
480
+ break;
481
+ }
482
+ toolResult = applyPatch(workspaceRoot, patchInput);
483
+ break;
484
+ }
485
+ case 'write_file': {
486
+ const approved = await requestApproval({
487
+ type: 'write_file',
488
+ label: input.path,
489
+ preview: `${input.path}\n${truncate(input.content || '', 800)}`
490
+ });
491
+ if (!approved) {
492
+ toolResult = `User denied full file write for ${input.path}`;
493
+ break;
494
+ }
495
+ toolResult = writeFile(workspaceRoot, input.path, input.content);
496
+ break;
497
+ }
498
+ default:
499
+ throw new Error(`Unsupported action: ${action}`);
500
+ }
501
+
502
+ observation = [
503
+ `Previous thought: ${decision.thought || '(none)'}`,
504
+ `Action: ${action}`,
505
+ 'Observation:',
506
+ toolResult
507
+ ].join('\n');
508
+ }
509
+
510
+ // Check for Agent Collaboration (Review)
511
+ if (config.enableAgentCollaboration !== false) {
512
+ const availableProviders = getAvailableProviders(config);
513
+ const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface');
514
+ if (altProviders.length > 0 && finalSummary) {
515
+ const reviewerProvider = altProviders[0];
516
+ onProgress(`Invoking Reviewer Agent (${reviewerProvider})...`);
517
+
518
+ const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
519
+ reviewerClient.systemInstruction = CODE_AGENT_PROMPT + "\n\nYou are the Reviewer Agent. Review the primary agent's changes, test output, and verification. If you spot a critical bug, point it out. Otherwise, confirm it looks good. Return JSON with action: 'finish' and your review in the 'summary' field.";
520
+
521
+ const reviewPrompt = `The primary agent (${provider}) just completed the task: "${task}".\nSummary: ${finalSummary}\nVerification: ${finalVerification}\nGit Status: ${(await getGitContext(workspaceRoot)).status}\n\nPlease review this. Return JSON with action: 'finish'.`;
522
+
523
+ try {
524
+ const reviewResponse = await reviewerClient.sendMessage(reviewPrompt);
525
+ const reviewDecision = extractJson(reviewResponse);
526
+ const reviewInput = reviewDecision.input || {};
527
+
528
+ finalSummary += `\n\n[Review by ${reviewerProvider}]\n${reviewInput.summary || reviewDecision.thought || 'Looks good.'}`;
529
+ } catch (e) {
530
+ onProgress(`Reviewer Agent failed: ${e.message}`);
531
+ }
532
+ }
533
+ }
534
+
535
+ if (finalSummary) {
536
+ return {
537
+ summary: finalSummary,
538
+ verification: finalVerification,
539
+ steps: MAX_AGENT_STEPS
540
+ };
541
+ }
542
+
543
+ writeWorkspaceSession(workspaceRoot, {
544
+ summary: `Task stopped before completion: ${task}`,
545
+ lastTask: task,
546
+ lastVerification: 'Agent limit reached before explicit completion.'
547
+ });
548
+
549
+ return {
550
+ summary: 'Stopped after reaching the maximum number of agent steps.',
551
+ verification: 'Agent limit reached before explicit completion.',
552
+ steps: MAX_AGENT_STEPS
553
+ };
554
+ }
555
+
556
+ 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' }