@pheem49/mint 1.3.0 → 1.4.1
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/.codex +0 -0
- package/README.md +174 -126
- package/main.js +21 -1
- package/mint-cli-logic.js +21 -1
- package/mint-cli.js +287 -45
- package/package.json +13 -2
- package/src/AI_Brain/Gemini_API.js +331 -64
- 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/AI_Brain/proactive_engine.js +2 -8
- package/src/Automation_Layer/file_operations.js +123 -4
- package/src/Automation_Layer/open_app.js +72 -43
- package/src/Automation_Layer/open_website.js +3 -3
- package/src/CLI/chat_router.js +57 -9
- package/src/CLI/chat_ui.js +117 -11
- package/src/CLI/code_agent.js +249 -36
- package/src/CLI/onboarding.js +53 -6
- package/src/CLI/workspace_manager.js +90 -0
- package/src/Plugins/docker.js +12 -10
- 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/custom_workflows.js +9 -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/chat_router.test.js +42 -0
- package/tests/code_agent.test.js +69 -0
- package/tests/config_manager.test.js +141 -0
- package/tests/docker.test.js +46 -0
- package/tests/file_operations.test.js +57 -0
- package/tests/memory_store.test.js +185 -0
- package/tests/provider_routing.test.js +67 -0
- package/tests/spotify.test.js +201 -0
- package/tests/system_monitor.test.js +37 -0
- package/tests/workspace_manager.test.js +56 -0
package/src/UI/settings.js
CHANGED
|
@@ -16,6 +16,7 @@ const DEFAULT_CONFIG = {
|
|
|
16
16
|
ollamaModel: 'llama3:latest',
|
|
17
17
|
enableVoiceReply: true,
|
|
18
18
|
enableCustomWorkflows: true,
|
|
19
|
+
enableAgentCollaboration: true,
|
|
19
20
|
ttsProvider: 'google',
|
|
20
21
|
ttsVolume: 1.0,
|
|
21
22
|
ttsSpeed: 1.0,
|
|
@@ -57,6 +58,18 @@ function applyConfig(config) {
|
|
|
57
58
|
// Apply API key
|
|
58
59
|
document.getElementById('api-key-input').value = config.apiKey || '';
|
|
59
60
|
|
|
61
|
+
const anthropicInput = document.getElementById('anthropic-api-key-input');
|
|
62
|
+
if (anthropicInput) anthropicInput.value = config.anthropicApiKey || '';
|
|
63
|
+
|
|
64
|
+
const openaiInput = document.getElementById('openai-api-key-input');
|
|
65
|
+
if (openaiInput) openaiInput.value = config.openaiApiKey || '';
|
|
66
|
+
|
|
67
|
+
const hfInput = document.getElementById('hf-api-key');
|
|
68
|
+
if (hfInput) hfInput.value = config.hfApiKey || '';
|
|
69
|
+
|
|
70
|
+
const ollamaHostInput = document.getElementById('ollama-host-input');
|
|
71
|
+
if (ollamaHostInput) ollamaHostInput.value = config.ollamaHost || 'http://localhost:11434';
|
|
72
|
+
|
|
60
73
|
// Apply Gemini model
|
|
61
74
|
applyModelSelection(config.geminiModel);
|
|
62
75
|
|
|
@@ -72,6 +85,24 @@ function applyConfig(config) {
|
|
|
72
85
|
ollamaInput.value = config.ollamaModel || 'llama3:latest';
|
|
73
86
|
}
|
|
74
87
|
|
|
88
|
+
applyModelSelectionGeneric('openai-model-select', 'openai-model-custom-row', 'openai-model-custom', config.openaiModel || 'gpt-4o');
|
|
89
|
+
applyModelSelectionGeneric('anthropic-model-select', 'anthropic-model-custom-row', 'anthropic-model-custom', config.anthropicModel || 'claude-3-5-sonnet-latest');
|
|
90
|
+
|
|
91
|
+
const hfModelInput = document.getElementById('hf-model-name');
|
|
92
|
+
if (hfModelInput) {
|
|
93
|
+
hfModelInput.value = config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const localApiBaseUrlInput = document.getElementById('local-api-base-url');
|
|
97
|
+
if (localApiBaseUrlInput) {
|
|
98
|
+
localApiBaseUrlInput.value = config.localApiBaseUrl || 'http://localhost:1234/v1';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const localModelNameInput = document.getElementById('local-model-name');
|
|
102
|
+
if (localModelNameInput) {
|
|
103
|
+
localModelNameInput.value = config.localModelName || 'local-model';
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
const voiceReplyToggle = document.getElementById('enable-voice-reply');
|
|
76
107
|
if (voiceReplyToggle) {
|
|
77
108
|
voiceReplyToggle.checked = config.enableVoiceReply !== false;
|
|
@@ -103,6 +134,11 @@ function applyConfig(config) {
|
|
|
103
134
|
enableWorkflowsToggle.checked = config.enableCustomWorkflows !== false;
|
|
104
135
|
}
|
|
105
136
|
|
|
137
|
+
const enableAgentCollaborationToggle = document.getElementById('enable-agent-collaboration');
|
|
138
|
+
if (enableAgentCollaborationToggle) {
|
|
139
|
+
enableAgentCollaborationToggle.checked = config.enableAgentCollaboration !== false;
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
// Plugins logic
|
|
107
143
|
updatePluginButton('spotify', config.pluginSpotifyEnabled);
|
|
108
144
|
updatePluginButton('calendar', config.pluginCalendarEnabled);
|
|
@@ -168,9 +204,15 @@ function lightenColor(hex, amount) {
|
|
|
168
204
|
}
|
|
169
205
|
|
|
170
206
|
function applyModelSelection(model) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
applyModelSelectionGeneric('gemini-model-select', 'gemini-model-custom-row', 'gemini-model-custom', model);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applyModelSelectionGeneric(selectId, customRowId, customInputId, model) {
|
|
211
|
+
const select = document.getElementById(selectId);
|
|
212
|
+
const customRow = document.getElementById(customRowId);
|
|
213
|
+
const customInput = document.getElementById(customInputId);
|
|
214
|
+
if (!select || !customRow || !customInput) return;
|
|
215
|
+
|
|
174
216
|
const normalized = (model || '').trim();
|
|
175
217
|
const optionValues = Array.from(select.options).map(opt => opt.value);
|
|
176
218
|
|
|
@@ -186,11 +228,16 @@ function applyModelSelection(model) {
|
|
|
186
228
|
}
|
|
187
229
|
|
|
188
230
|
function getSelectedModel() {
|
|
189
|
-
|
|
190
|
-
|
|
231
|
+
return getSelectedModelGeneric('gemini-model-select', 'gemini-model-custom', DEFAULT_CONFIG.geminiModel);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getSelectedModelGeneric(selectId, customInputId, defaultModel) {
|
|
235
|
+
const select = document.getElementById(selectId);
|
|
236
|
+
const customInput = document.getElementById(customInputId);
|
|
237
|
+
if (!select || !customInput) return defaultModel;
|
|
191
238
|
if (select.value === 'custom') {
|
|
192
239
|
const custom = (customInput.value || '').trim();
|
|
193
|
-
return custom ||
|
|
240
|
+
return custom || defaultModel;
|
|
194
241
|
}
|
|
195
242
|
return select.value;
|
|
196
243
|
}
|
|
@@ -198,21 +245,28 @@ function getSelectedModel() {
|
|
|
198
245
|
// --- Event Listeners ---
|
|
199
246
|
|
|
200
247
|
// Close button
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
248
|
+
// Close button
|
|
249
|
+
const closeBtn = document.getElementById('close-btn');
|
|
250
|
+
if (closeBtn) {
|
|
251
|
+
closeBtn.addEventListener('click', () => {
|
|
252
|
+
window.settingsApi.closeSettings();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
204
255
|
|
|
205
256
|
// Toggle API key visibility
|
|
206
|
-
document.getElementById('toggle-key')
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
257
|
+
const toggleKey = document.getElementById('toggle-key');
|
|
258
|
+
if (toggleKey) {
|
|
259
|
+
toggleKey.addEventListener('click', () => {
|
|
260
|
+
const input = document.getElementById('api-key-input');
|
|
261
|
+
input.type = input.type === 'password' ? 'text' : 'password';
|
|
262
|
+
});
|
|
263
|
+
}
|
|
210
264
|
|
|
211
265
|
async function saveApiKeyOnly() {
|
|
212
266
|
const input = document.getElementById('api-key-input');
|
|
213
267
|
const status = document.getElementById('api-save-status');
|
|
214
268
|
const btn = document.getElementById('save-api-key');
|
|
215
|
-
const apiKey = input.value.trim();
|
|
269
|
+
const apiKey = input ? input.value.trim() : '';
|
|
216
270
|
|
|
217
271
|
try {
|
|
218
272
|
const baseConfig = await window.settingsApi.getSettings();
|
|
@@ -220,26 +274,31 @@ async function saveApiKeyOnly() {
|
|
|
220
274
|
await window.settingsApi.saveSettings(nextConfig);
|
|
221
275
|
currentConfig.apiKey = apiKey;
|
|
222
276
|
|
|
223
|
-
btn.textContent = 'Saved!';
|
|
224
|
-
status.textContent = 'API key saved';
|
|
277
|
+
if (btn) btn.textContent = 'Saved!';
|
|
278
|
+
if (status) status.textContent = 'API key saved';
|
|
225
279
|
setTimeout(() => {
|
|
226
|
-
btn.textContent = 'Save API Key';
|
|
227
|
-
status.textContent = '';
|
|
280
|
+
if (btn) btn.textContent = 'Save API Key';
|
|
281
|
+
if (status) status.textContent = '';
|
|
228
282
|
}, 1500);
|
|
229
283
|
} catch (err) {
|
|
230
284
|
console.error('Failed to save API key:', err);
|
|
231
|
-
status.textContent = 'Save failed';
|
|
232
|
-
setTimeout(() => { status.textContent = ''; }, 1500);
|
|
285
|
+
if (status) status.textContent = 'Save failed';
|
|
286
|
+
setTimeout(() => { if (status) status.textContent = ''; }, 1500);
|
|
233
287
|
}
|
|
234
288
|
}
|
|
235
289
|
|
|
236
|
-
document.getElementById('save-api-key')
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
290
|
+
const saveApiKey = document.getElementById('save-api-key');
|
|
291
|
+
if (saveApiKey) saveApiKey.addEventListener('click', saveApiKeyOnly);
|
|
292
|
+
|
|
293
|
+
const apiKeyInput = document.getElementById('api-key-input');
|
|
294
|
+
if (apiKeyInput) {
|
|
295
|
+
apiKeyInput.addEventListener('keydown', (e) => {
|
|
296
|
+
if (e.key === 'Enter') {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
saveApiKeyOnly();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
243
302
|
|
|
244
303
|
// Gemini model select
|
|
245
304
|
document.getElementById('gemini-model-select').addEventListener('change', (e) => {
|
|
@@ -257,23 +316,45 @@ document.getElementById('gemini-model-custom').addEventListener('input', (e) =>
|
|
|
257
316
|
currentConfig.geminiModel = e.target.value.trim();
|
|
258
317
|
});
|
|
259
318
|
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
geminiOptions.style.display = 'none';
|
|
267
|
-
ollamaOptions.style.display = 'block';
|
|
319
|
+
// OpenAI model select
|
|
320
|
+
document.getElementById('openai-model-select').addEventListener('change', (e) => {
|
|
321
|
+
const customRow = document.getElementById('openai-model-custom-row');
|
|
322
|
+
if (e.target.value === 'custom') {
|
|
323
|
+
customRow.style.display = 'block';
|
|
324
|
+
currentConfig.openaiModel = (document.getElementById('openai-model-custom').value || '').trim();
|
|
268
325
|
} else {
|
|
269
|
-
|
|
270
|
-
|
|
326
|
+
customRow.style.display = 'none';
|
|
327
|
+
currentConfig.openaiModel = e.target.value;
|
|
271
328
|
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
document.getElementById('openai-model-custom').addEventListener('input', (e) => {
|
|
332
|
+
currentConfig.openaiModel = e.target.value.trim();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Anthropic model select
|
|
336
|
+
document.getElementById('anthropic-model-select').addEventListener('change', (e) => {
|
|
337
|
+
const customRow = document.getElementById('anthropic-model-custom-row');
|
|
338
|
+
if (e.target.value === 'custom') {
|
|
339
|
+
customRow.style.display = 'block';
|
|
340
|
+
currentConfig.anthropicModel = (document.getElementById('anthropic-model-custom').value || '').trim();
|
|
341
|
+
} else {
|
|
342
|
+
customRow.style.display = 'none';
|
|
343
|
+
currentConfig.anthropicModel = e.target.value;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
document.getElementById('anthropic-model-custom').addEventListener('input', (e) => {
|
|
348
|
+
currentConfig.anthropicModel = e.target.value.trim();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// AI Provider toggle (No-op since all sections stay visible)
|
|
352
|
+
function toggleProviderOptions(provider) {
|
|
353
|
+
// No-op
|
|
272
354
|
}
|
|
273
355
|
|
|
274
356
|
document.getElementById('ai-provider-select').addEventListener('change', (e) => {
|
|
275
357
|
currentConfig.aiProvider = e.target.value;
|
|
276
|
-
toggleProviderOptions(e.target.value);
|
|
277
358
|
});
|
|
278
359
|
|
|
279
360
|
document.getElementById('ollama-model-input').addEventListener('input', (e) => {
|
|
@@ -281,9 +362,12 @@ document.getElementById('ollama-model-input').addEventListener('input', (e) => {
|
|
|
281
362
|
});
|
|
282
363
|
|
|
283
364
|
// AI Studio link
|
|
284
|
-
document.getElementById('ai-studio-link')
|
|
285
|
-
|
|
286
|
-
|
|
365
|
+
const aiStudioLink = document.getElementById('ai-studio-link');
|
|
366
|
+
if (aiStudioLink) {
|
|
367
|
+
aiStudioLink.addEventListener('click', () => {
|
|
368
|
+
window.settingsApi.openExternal('https://aistudio.google.com/');
|
|
369
|
+
});
|
|
370
|
+
}
|
|
287
371
|
|
|
288
372
|
// Theme cards
|
|
289
373
|
document.querySelectorAll('.theme-card').forEach(card => {
|
|
@@ -400,9 +484,34 @@ if (document.getElementById('tts-pitch')) {
|
|
|
400
484
|
// Save
|
|
401
485
|
document.getElementById('save-btn').addEventListener('click', async () => {
|
|
402
486
|
currentConfig.apiKey = document.getElementById('api-key-input').value.trim();
|
|
487
|
+
|
|
488
|
+
const anthropicInput = document.getElementById('anthropic-api-key-input');
|
|
489
|
+
if (anthropicInput) currentConfig.anthropicApiKey = anthropicInput.value.trim();
|
|
490
|
+
|
|
491
|
+
const openaiInput = document.getElementById('openai-api-key-input');
|
|
492
|
+
if (openaiInput) currentConfig.openaiApiKey = openaiInput.value.trim();
|
|
493
|
+
|
|
494
|
+
const hfInput = document.getElementById('hf-api-key');
|
|
495
|
+
if (hfInput) currentConfig.hfApiKey = hfInput.value.trim();
|
|
496
|
+
|
|
497
|
+
const ollamaHostInput = document.getElementById('ollama-host-input');
|
|
498
|
+
if (ollamaHostInput) currentConfig.ollamaHost = ollamaHostInput.value.trim();
|
|
499
|
+
|
|
403
500
|
currentConfig.geminiModel = getSelectedModel();
|
|
404
501
|
currentConfig.aiProvider = document.getElementById('ai-provider-select').value;
|
|
405
502
|
currentConfig.ollamaModel = document.getElementById('ollama-model-input').value.trim();
|
|
503
|
+
|
|
504
|
+
currentConfig.openaiModel = getSelectedModelGeneric('openai-model-select', 'openai-model-custom', DEFAULT_CONFIG.openaiModel || 'gpt-4o');
|
|
505
|
+
currentConfig.anthropicModel = getSelectedModelGeneric('anthropic-model-select', 'anthropic-model-custom', DEFAULT_CONFIG.anthropicModel || 'claude-3-5-sonnet-latest');
|
|
506
|
+
|
|
507
|
+
const hfModelInput = document.getElementById('hf-model-name');
|
|
508
|
+
if (hfModelInput) currentConfig.hfModel = hfModelInput.value.trim();
|
|
509
|
+
|
|
510
|
+
const localApiBaseUrlInput = document.getElementById('local-api-base-url');
|
|
511
|
+
if (localApiBaseUrlInput) currentConfig.localApiBaseUrl = localApiBaseUrlInput.value.trim();
|
|
512
|
+
|
|
513
|
+
const localModelNameInput = document.getElementById('local-model-name');
|
|
514
|
+
if (localModelNameInput) currentConfig.localModelName = localModelNameInput.value.trim();
|
|
406
515
|
|
|
407
516
|
const voiceReplyToggle = document.getElementById('enable-voice-reply');
|
|
408
517
|
if (voiceReplyToggle) {
|
|
@@ -421,6 +530,11 @@ document.getElementById('save-btn').addEventListener('click', async () => {
|
|
|
421
530
|
currentConfig.enableCustomWorkflows = enableWorkflowsToggle.checked;
|
|
422
531
|
}
|
|
423
532
|
|
|
533
|
+
const enableAgentCollaborationToggle = document.getElementById('enable-agent-collaboration');
|
|
534
|
+
if (enableAgentCollaborationToggle) {
|
|
535
|
+
currentConfig.enableAgentCollaboration = enableAgentCollaborationToggle.checked;
|
|
536
|
+
}
|
|
537
|
+
|
|
424
538
|
const showWidgetToggle = document.getElementById('show-desktop-widget');
|
|
425
539
|
if (showWidgetToggle) {
|
|
426
540
|
currentConfig.showDesktopWidget = showWidgetToggle.checked;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: agent_orchestrator.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const orchestrator = require('../src/AI_Brain/agent_orchestrator');
|
|
6
|
+
|
|
7
|
+
describe('Agent Orchestrator', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
orchestrator.resetAgent();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('starts with general agent', () => {
|
|
13
|
+
const agent = orchestrator.getCurrentAgent();
|
|
14
|
+
expect(agent.name).toBe('Mint Default');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('can switch to coder agent', () => {
|
|
18
|
+
orchestrator.setAgent('coder');
|
|
19
|
+
const agent = orchestrator.getCurrentAgent();
|
|
20
|
+
expect(agent.name).toBe('Mint Coder');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('can switch to researcher agent', () => {
|
|
24
|
+
orchestrator.setAgent('researcher');
|
|
25
|
+
const agent = orchestrator.getCurrentAgent();
|
|
26
|
+
expect(agent.name).toBe('Mint Researcher');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('falls back to general for invalid agent', () => {
|
|
30
|
+
orchestrator.setAgent('nonexistent');
|
|
31
|
+
const agent = orchestrator.getCurrentAgent();
|
|
32
|
+
expect(agent.name).toBe('Mint Default');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('lists available agents', () => {
|
|
36
|
+
const agents = orchestrator.listAgents();
|
|
37
|
+
expect(agents).toContain('general');
|
|
38
|
+
expect(agents).toContain('coder');
|
|
39
|
+
expect(agents).toContain('researcher');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: chat_router routing helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('@google/genai', () => ({
|
|
6
|
+
GoogleGenAI: jest.fn()
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock('../src/CLI/code_agent', () => ({
|
|
10
|
+
executeCodeTask: jest.fn(),
|
|
11
|
+
_helpers: {
|
|
12
|
+
selectSupportedCodeProvider: jest.fn(() => 'gemini')
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock('../src/System/config_manager', () => ({
|
|
17
|
+
readConfig: jest.fn(() => ({})),
|
|
18
|
+
getAvailableProviders: jest.fn(() => ['gemini'])
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('chat_router helpers', () => {
|
|
22
|
+
test('recognizes direct folder open request as chat task', () => {
|
|
23
|
+
const { _helpers } = require('../src/CLI/chat_router');
|
|
24
|
+
expect(_helpers.isDirectFilesystemActionRequest('เปิดโฟลเดอร์ xidaidai ให้หน่อย')).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('does not classify direct folder open request as code intent', () => {
|
|
28
|
+
const { _helpers } = require('../src/CLI/chat_router');
|
|
29
|
+
const route = _helpers.detectCodeIntentHeuristic('open folder xidaidai', process.cwd());
|
|
30
|
+
expect(route).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('treats small file-related request as normal chat', () => {
|
|
34
|
+
const { _helpers } = require('../src/CLI/chat_router');
|
|
35
|
+
expect(_helpers.isLargeCodeTaskRequest('ดูไฟล์ package.json ให้หน่อย', process.cwd())).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('treats substantial project fix request as code task', () => {
|
|
39
|
+
const { _helpers } = require('../src/CLI/chat_router');
|
|
40
|
+
expect(_helpers.isLargeCodeTaskRequest('fix the failing tests in this project and verify the result', process.cwd())).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: code_agent helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('@google/genai', () => ({
|
|
6
|
+
GoogleGenAI: jest.fn()
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock('axios', () => ({}));
|
|
10
|
+
|
|
11
|
+
jest.mock('../src/System/config_manager', () => ({
|
|
12
|
+
readConfig: jest.fn(() => ({})),
|
|
13
|
+
getAvailableProviders: jest.fn(() => ['ollama', 'gemini'])
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock('../src/CLI/code_session_memory', () => ({
|
|
17
|
+
readWorkspaceSession: jest.fn(() => ({
|
|
18
|
+
summary: '',
|
|
19
|
+
lastTask: '',
|
|
20
|
+
lastVerification: '',
|
|
21
|
+
updatedAt: null
|
|
22
|
+
})),
|
|
23
|
+
writeWorkspaceSession: jest.fn()
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
|
|
30
|
+
describe('code_agent helpers', () => {
|
|
31
|
+
test('extractJson recovers JSON embedded in surrounding text', () => {
|
|
32
|
+
const { _helpers } = require('../src/CLI/code_agent');
|
|
33
|
+
const parsed = _helpers.extractJson('note\n{"action":"finish","input":{"summary":"ok"}}\nthanks');
|
|
34
|
+
expect(parsed.action).toBe('finish');
|
|
35
|
+
expect(parsed.input.summary).toBe('ok');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('selectSupportedCodeProvider falls back away from unsupported code providers', () => {
|
|
39
|
+
const { _helpers } = require('../src/CLI/code_agent');
|
|
40
|
+
const selected = _helpers.selectSupportedCodeProvider(
|
|
41
|
+
{ aiProvider: 'ollama' },
|
|
42
|
+
['ollama', 'openai', 'gemini']
|
|
43
|
+
);
|
|
44
|
+
expect(selected).toBe('openai');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('selectSupportedCodeProvider keeps configured supported provider when available', () => {
|
|
48
|
+
const { _helpers } = require('../src/CLI/code_agent');
|
|
49
|
+
const selected = _helpers.selectSupportedCodeProvider(
|
|
50
|
+
{ aiProvider: 'anthropic' },
|
|
51
|
+
['anthropic', 'gemini']
|
|
52
|
+
);
|
|
53
|
+
expect(selected).toBe('anthropic');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('findPaths can locate directories by partial name', async () => {
|
|
57
|
+
const { _helpers } = require('../src/CLI/code_agent');
|
|
58
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-code-agent-'));
|
|
59
|
+
const targetDir = path.join(tempDir, 'projects', 'xidaidai');
|
|
60
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const result = await _helpers.findPaths(tempDir, 'xidaidai', 'dir');
|
|
64
|
+
expect(result).toContain('[dir] projects/xidaidai');
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: config_manager.js
|
|
3
|
+
* Tests readConfig, writeConfig, and getAvailableProviders.
|
|
4
|
+
*
|
|
5
|
+
* Isolation strategy: spy on os.homedir() BEFORE requiring config_manager
|
|
6
|
+
* so CONFIG_DIR and CONFIG_PATH point to a temp directory, never touching
|
|
7
|
+
* the real ~/.config/mint/mint-config.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
let tempDir;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// 1. Reset module registry so config_manager re-initialises fresh each test
|
|
18
|
+
jest.resetModules();
|
|
19
|
+
|
|
20
|
+
// 2. Create an isolated temp dir for this test
|
|
21
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-cfg-test-'));
|
|
22
|
+
|
|
23
|
+
// 3. Mock os.homedir() BEFORE requiring config_manager.
|
|
24
|
+
// config_manager computes CONFIG_DIR = os.homedir() + '/.config/mint'
|
|
25
|
+
// at load time, so the spy must be active when the module first loads.
|
|
26
|
+
jest.spyOn(os, 'homedir').mockReturnValue(tempDir);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
jest.restoreAllMocks();
|
|
31
|
+
jest.resetModules();
|
|
32
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Helper — always gets a fresh, isolated instance of config_manager
|
|
36
|
+
function getModule() {
|
|
37
|
+
return require('../src/System/config_manager');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── readConfig ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe('config_manager — readConfig', () => {
|
|
43
|
+
test('returns DEFAULT_CONFIG when no config file exists', () => {
|
|
44
|
+
const { readConfig } = getModule();
|
|
45
|
+
const config = readConfig();
|
|
46
|
+
expect(config).toHaveProperty('geminiModel', 'gemini-2.5-flash');
|
|
47
|
+
expect(config).toHaveProperty('aiProvider', 'gemini');
|
|
48
|
+
expect(config).toHaveProperty('language', 'th-TH');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('merges saved values with defaults (saved value wins)', () => {
|
|
52
|
+
const { readConfig, writeConfig } = getModule();
|
|
53
|
+
writeConfig({ geminiModel: 'gemini-2.0-pro' });
|
|
54
|
+
const config = readConfig();
|
|
55
|
+
expect(config.geminiModel).toBe('gemini-2.0-pro');
|
|
56
|
+
// Default fields still present
|
|
57
|
+
expect(config.aiProvider).toBe('gemini');
|
|
58
|
+
expect(config.language).toBe('th-TH');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('missing keys in saved file are filled by defaults', () => {
|
|
62
|
+
const { readConfig, writeConfig } = getModule();
|
|
63
|
+
// Write a partial config (no 'ollamaModel' key)
|
|
64
|
+
writeConfig({ geminiModel: 'my-model' });
|
|
65
|
+
const config = readConfig();
|
|
66
|
+
expect(config.ollamaModel).toBe('llama3:latest'); // default
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── writeConfig ────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe('config_manager — writeConfig', () => {
|
|
73
|
+
test('returns { success: true } on valid write', () => {
|
|
74
|
+
const { writeConfig } = getModule();
|
|
75
|
+
const result = writeConfig({ testKey: 'testValue' });
|
|
76
|
+
expect(result).toEqual({ success: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('written JSON is readable back correctly', () => {
|
|
80
|
+
const { readConfig, writeConfig } = getModule();
|
|
81
|
+
writeConfig({ geminiModel: 'custom-model-test' });
|
|
82
|
+
const config = readConfig();
|
|
83
|
+
expect(config.geminiModel).toBe('custom-model-test');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('config file is actually created on disk', () => {
|
|
87
|
+
const { writeConfig, CONFIG_PATH } = getModule();
|
|
88
|
+
writeConfig({ geminiModel: 'test' });
|
|
89
|
+
expect(fs.existsSync(CONFIG_PATH)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── getAvailableProviders ──────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
describe('config_manager — getAvailableProviders', () => {
|
|
96
|
+
test('always includes ollama (local, no key needed)', () => {
|
|
97
|
+
const { getAvailableProviders } = getModule();
|
|
98
|
+
expect(getAvailableProviders({})).toContain('ollama');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('includes gemini when apiKey is set', () => {
|
|
102
|
+
const { getAvailableProviders } = getModule();
|
|
103
|
+
expect(getAvailableProviders({ apiKey: 'test-key' })).toContain('gemini');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('includes anthropic when anthropicApiKey is set', () => {
|
|
107
|
+
const { getAvailableProviders } = getModule();
|
|
108
|
+
expect(getAvailableProviders({ anthropicApiKey: 'ant-key' })).toContain('anthropic');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('includes openai when openaiApiKey is set', () => {
|
|
112
|
+
const { getAvailableProviders } = getModule();
|
|
113
|
+
expect(getAvailableProviders({ openaiApiKey: 'oai-key' })).toContain('openai');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('includes huggingface when hfApiKey is set', () => {
|
|
117
|
+
const { getAvailableProviders } = getModule();
|
|
118
|
+
expect(getAvailableProviders({ hfApiKey: 'hf-key' })).toContain('huggingface');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('includes local_openai when localApiBaseUrl is set', () => {
|
|
122
|
+
const { getAvailableProviders } = getModule();
|
|
123
|
+
expect(
|
|
124
|
+
getAvailableProviders({ localApiBaseUrl: 'http://localhost:1234/v1' })
|
|
125
|
+
).toContain('local_openai');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('does NOT include gemini when no key', () => {
|
|
129
|
+
const { getAvailableProviders } = getModule();
|
|
130
|
+
const savedEnv = process.env.GEMINI_API_KEY;
|
|
131
|
+
delete process.env.GEMINI_API_KEY;
|
|
132
|
+
const providers = getAvailableProviders({ apiKey: '' });
|
|
133
|
+
expect(providers).not.toContain('gemini');
|
|
134
|
+
if (savedEnv) process.env.GEMINI_API_KEY = savedEnv;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('returns array type', () => {
|
|
138
|
+
const { getAvailableProviders } = getModule();
|
|
139
|
+
expect(Array.isArray(getAvailableProviders({}))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: docker.js plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('child_process', () => ({
|
|
6
|
+
execFile: jest.fn()
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
let docker;
|
|
10
|
+
let execFile;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.resetModules();
|
|
14
|
+
({ execFile } = require('child_process'));
|
|
15
|
+
execFile.mockReset();
|
|
16
|
+
docker = require('../src/Plugins/docker');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Docker Plugin', () => {
|
|
20
|
+
test('lists running containers', async () => {
|
|
21
|
+
execFile.mockImplementation((command, args, callback) => {
|
|
22
|
+
callback(null, 'web (Up 2 hours)\n', '');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const result = await docker.execute('list');
|
|
26
|
+
expect(execFile).toHaveBeenCalledWith('docker', ['ps', '--format', '{{.Names}} ({{.Status}})'], expect.any(Function));
|
|
27
|
+
expect(result).toContain('Running Containers');
|
|
28
|
+
expect(result).toContain('web (Up 2 hours)');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('starts a named container without shell interpolation', async () => {
|
|
32
|
+
execFile.mockImplementation((command, args, callback) => {
|
|
33
|
+
callback(null, '', '');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await docker.execute('start my-app');
|
|
37
|
+
expect(execFile).toHaveBeenCalledWith('docker', ['start', 'my-app'], expect.any(Function));
|
|
38
|
+
expect(result).toContain('Successfully executed "docker start"');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns helpful message when command is invalid', async () => {
|
|
42
|
+
const result = await docker.execute('remove my-app');
|
|
43
|
+
expect(result).toContain('Invalid docker command');
|
|
44
|
+
expect(execFile).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
});
|