@pheem49/mint 1.4.0 → 1.4.2

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.
@@ -9,26 +9,35 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
 
12
- const WORKSPACE_FILE = path.join(os.homedir(), '.config', 'mint', 'workspaces.json');
12
+ function getWorkspaceFile() {
13
+ return process.env.MINT_WORKSPACE_FILE || path.join(os.homedir(), '.config', 'mint', 'workspaces.json');
14
+ }
13
15
 
14
16
  function ensureDir() {
15
- const dir = path.dirname(WORKSPACE_FILE);
17
+ const dir = path.dirname(getWorkspaceFile());
16
18
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
17
19
  }
18
20
 
19
21
  function loadWorkspaces() {
22
+ const workspaceFile = getWorkspaceFile();
20
23
  ensureDir();
21
- if (!fs.existsSync(WORKSPACE_FILE)) return {};
24
+ if (!fs.existsSync(workspaceFile)) return {};
22
25
  try {
23
- return JSON.parse(fs.readFileSync(WORKSPACE_FILE, 'utf8'));
26
+ return JSON.parse(fs.readFileSync(workspaceFile, 'utf8'));
24
27
  } catch (e) {
25
28
  return {};
26
29
  }
27
30
  }
28
31
 
29
32
  function saveWorkspaces(data) {
33
+ const workspaceFile = getWorkspaceFile();
30
34
  ensureDir();
31
- fs.writeFileSync(WORKSPACE_FILE, JSON.stringify(data, null, 2));
35
+ fs.writeFileSync(workspaceFile, JSON.stringify(data, null, 2));
36
+ }
37
+
38
+ function isPathInsideWorkspace(currentPath, workspacePath) {
39
+ const relative = path.relative(workspacePath, currentPath);
40
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
32
41
  }
33
42
 
34
43
  function addWorkspace(name, rootPath, instructions = '') {
@@ -62,7 +71,7 @@ function getWorkspaceByPath(currentPath) {
62
71
  // Find workspace where current path is inside or equal to workspace path
63
72
  for (const name in workspaces) {
64
73
  const ws = workspaces[name];
65
- if (absoluteCurrent.startsWith(ws.path)) {
74
+ if (isPathInsideWorkspace(absoluteCurrent, ws.path)) {
66
75
  return ws;
67
76
  }
68
77
  }
@@ -1,4 +1,4 @@
1
- const { exec } = require('child_process');
1
+ const { execFile } = require('child_process');
2
2
 
3
3
  module.exports = {
4
4
  name: 'docker',
@@ -8,28 +8,30 @@ module.exports = {
8
8
  return new Promise((resolve) => {
9
9
  console.log(`[Docker Plugin] Executing command: ${target}`);
10
10
 
11
- const [action, ...args] = target.toLowerCase().split(' ');
11
+ const rawTarget = (target || '').trim();
12
+ const [rawAction, ...args] = rawTarget.split(/\s+/);
13
+ const action = (rawAction || '').toLowerCase();
12
14
  const containerName = args.join(' ');
13
-
14
- let cmd = '';
15
+ let commandArgs = [];
15
16
 
16
17
  if (action === 'list') {
17
- cmd = 'docker ps --format "{{.Names}} ({{.Status}})"';
18
+ commandArgs = ['ps', '--format', '{{.Names}} ({{.Status}})'];
18
19
  } else if (['start', 'stop', 'restart'].includes(action) && containerName) {
19
- cmd = `docker ${action} ${containerName}`;
20
+ commandArgs = [action, containerName];
20
21
  } else {
21
22
  return resolve(`Invalid docker command or missing container name: ${target}`);
22
23
  }
23
24
 
24
- exec(cmd, (error, stdout, stderr) => {
25
+ execFile('docker', commandArgs, (error, stdout, stderr) => {
25
26
  if (error) {
26
- if (error.code === 127 || stderr.includes('not found')) {
27
+ const stderrText = stderr || '';
28
+ if (error.code === 127 || stderrText.includes('not found') || error.code === 'ENOENT') {
27
29
  return resolve('Error: Docker is not installed or not in PATH.');
28
30
  }
29
- if (stderr.includes('permission denied')) {
31
+ if (stderrText.toLowerCase().includes('permission denied')) {
30
32
  return resolve('Error: Permission denied. You might need to add your user to the "docker" group.');
31
33
  }
32
- return resolve(`Docker Error: ${stderr || error.message}`);
34
+ return resolve(`Docker Error: ${stderrText || error.message}`);
33
35
  }
34
36
 
35
37
  if (action === 'list') {
@@ -67,7 +67,7 @@ const DEFAULT_CONFIG = {
67
67
  localApiBaseUrl: 'http://localhost:1234/v1',
68
68
  localModelName: 'local-model',
69
69
  ollamaHost: 'http://localhost:11434',
70
- enableAgentCollaboration: true
70
+ enableAgentCollaboration: false
71
71
  };
72
72
 
73
73
 
@@ -3,6 +3,10 @@ const path = require('path');
3
3
  const { app, shell } = require('electron');
4
4
  const { exec } = require('child_process');
5
5
 
6
+ function escapeRegExp(text) {
7
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
+ }
9
+
6
10
  class CustomWorkflows {
7
11
  constructor() {
8
12
  this.configPath = path.join(app.getPath('userData'), 'workflows.json');
@@ -86,7 +90,7 @@ class CustomWorkflows {
86
90
  if (wf.trigger && wf.trigger.type === 'process_running' && wf.trigger.processName) {
87
91
  const targetName = wf.trigger.processName.toLowerCase();
88
92
  // simplistic exact-word match to avoid partial matches
89
- const regex = new RegExp(`^${targetName}$`, 'm');
93
+ const regex = new RegExp(`^${escapeRegExp(targetName)}$`, 'm');
90
94
  const isRunning = regex.test(runningProcesses);
91
95
 
92
96
  if (isRunning) {
@@ -124,4 +128,7 @@ class CustomWorkflows {
124
128
  }
125
129
  }
126
130
 
127
- module.exports = new CustomWorkflows();
131
+ const instance = new CustomWorkflows();
132
+ instance._helpers = { escapeRegExp };
133
+
134
+ module.exports = instance;
@@ -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,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
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Tests: file_operations helpers
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ describe('file_operations findPath', () => {
10
+ let tempDir;
11
+ let originalCwd;
12
+
13
+ beforeEach(() => {
14
+ jest.resetModules();
15
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-file-ops-'));
16
+ originalCwd = process.cwd();
17
+ process.chdir(tempDir);
18
+ });
19
+
20
+ afterEach(() => {
21
+ process.chdir(originalCwd);
22
+ fs.rmSync(tempDir, { recursive: true, force: true });
23
+ });
24
+
25
+ test('finds directory matches by name', () => {
26
+ const targetDir = path.join(tempDir, 'nested', 'xidaidai');
27
+ fs.mkdirSync(targetDir, { recursive: true });
28
+
29
+ const { findPath } = require('../src/Automation_Layer/file_operations');
30
+ const result = findPath('xidaidai', { type: 'dir', maxResults: 10, roots: [tempDir] });
31
+
32
+ expect(result.success).toBe(true);
33
+ expect(result.matches.some(match => match.path === targetDir && match.type === 'dir')).toBe(true);
34
+ });
35
+
36
+ test('returns not found message when no path matches', () => {
37
+ const { findPath } = require('../src/Automation_Layer/file_operations');
38
+ const result = findPath('does-not-exist', { type: 'dir', maxResults: 10, roots: [tempDir] });
39
+
40
+ expect(result.success).toBe(false);
41
+ expect(result.message).toContain('ไม่พบ');
42
+ });
43
+
44
+ test('prefers exact directory name matches over nested partial matches', () => {
45
+ const exactDir = path.join(tempDir, 'xidaidai');
46
+ const nestedPartial = path.join(tempDir, 'xidaidai collection', 'xidaidai gif');
47
+ fs.mkdirSync(exactDir, { recursive: true });
48
+ fs.mkdirSync(nestedPartial, { recursive: true });
49
+
50
+ const { findPath } = require('../src/Automation_Layer/file_operations');
51
+ const result = findPath('xidaidai', { type: 'dir', maxResults: 10, roots: [tempDir] });
52
+
53
+ expect(result.success).toBe(true);
54
+ expect(result.matches.length).toBe(1);
55
+ expect(result.matches[0].path).toBe(exactDir);
56
+ });
57
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Tests: Gemini_API provider routing helpers
3
+ */
4
+
5
+ jest.mock('@google/genai', () => ({
6
+ GoogleGenAI: jest.fn(() => ({
7
+ chats: {
8
+ create: jest.fn(() => ({
9
+ sendMessage: jest.fn(),
10
+ sendMessageStream: jest.fn(),
11
+ getHistory: jest.fn(async () => [])
12
+ }))
13
+ },
14
+ models: {
15
+ generateContent: jest.fn()
16
+ }
17
+ }))
18
+ }));
19
+
20
+ jest.mock('../src/System/chat_history_manager', () => ({
21
+ readChatHistory: jest.fn(() => []),
22
+ writeChatHistory: jest.fn(),
23
+ clearChatHistory: jest.fn()
24
+ }));
25
+
26
+ jest.mock('../src/System/config_manager', () => ({
27
+ readConfig: jest.fn(() => ({})),
28
+ getAvailableProviders: jest.fn(() => ['ollama', 'gemini'])
29
+ }));
30
+
31
+ jest.mock('../src/Plugins/plugin_manager', () => ({
32
+ loadPlugins: jest.fn(),
33
+ getPromptDescriptions: jest.fn(() => '')
34
+ }));
35
+
36
+ jest.mock('../src/Plugins/mcp_manager', () => ({
37
+ getAllTools: jest.fn(() => [])
38
+ }));
39
+
40
+ jest.mock('../src/AI_Brain/memory_store', () => ({
41
+ getUserContext: jest.fn(() => ''),
42
+ getCachedResponse: jest.fn(),
43
+ recordInteraction: jest.fn(),
44
+ cacheResponse: jest.fn()
45
+ }));
46
+
47
+ jest.mock('../src/AI_Brain/agent_orchestrator', () => ({
48
+ getCurrentAgent: jest.fn(() => ({ name: 'Mint Default', instruction: 'default' }))
49
+ }));
50
+
51
+ jest.mock('../src/CLI/workspace_manager', () => ({
52
+ getWorkspaceByPath: jest.fn(() => null)
53
+ }));
54
+
55
+ describe('Gemini_API provider routing helpers', () => {
56
+ test('prioritizes configured provider, then falls back to available providers', () => {
57
+ const geminiApi = require('../src/AI_Brain/Gemini_API');
58
+ const order = geminiApi._helpers.getProviderAttemptOrder({
59
+ aiProvider: 'openai',
60
+ openaiApiKey: 'key',
61
+ localApiBaseUrl: 'http://localhost:1234/v1'
62
+ });
63
+
64
+ expect(order[0]).toBe('openai');
65
+ expect(order).toContain('ollama');
66
+ });
67
+ });
@@ -9,13 +9,17 @@ const os = require('os');
9
9
 
10
10
  describe('Workspace Manager', () => {
11
11
  let tempDir;
12
+ let workspaceFile;
12
13
 
13
14
  beforeEach(() => {
14
15
  // Create a temp workspace directory
15
16
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-ws-test-'));
17
+ workspaceFile = path.join(tempDir, 'workspaces.json');
18
+ process.env.MINT_WORKSPACE_FILE = workspaceFile;
16
19
  });
17
20
 
18
21
  afterEach(() => {
22
+ delete process.env.MINT_WORKSPACE_FILE;
19
23
  fs.rmSync(tempDir, { recursive: true, force: true });
20
24
  });
21
25
 
@@ -32,6 +36,17 @@ describe('Workspace Manager', () => {
32
36
  expect(ws.name).toBe('test-ws');
33
37
  });
34
38
 
39
+ test('does not match sibling paths with same prefix', () => {
40
+ const workspaceRoot = path.join(tempDir, 'project');
41
+ const siblingPath = path.join(tempDir, 'project-two');
42
+ fs.mkdirSync(workspaceRoot, { recursive: true });
43
+ fs.mkdirSync(siblingPath, { recursive: true });
44
+
45
+ wsManager.addWorkspace('test-ws', workspaceRoot);
46
+ const ws = wsManager.getWorkspaceByPath(siblingPath);
47
+ expect(ws).toBeNull();
48
+ });
49
+
35
50
  test('removes workspaces', () => {
36
51
  wsManager.addWorkspace('test-ws', tempDir);
37
52
  wsManager.removeWorkspace('test-ws');