@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.
Files changed (38) hide show
  1. package/.codex +0 -0
  2. package/README.md +174 -126
  3. package/main.js +21 -1
  4. package/mint-cli-logic.js +21 -1
  5. package/mint-cli.js +287 -45
  6. package/package.json +13 -2
  7. package/src/AI_Brain/Gemini_API.js +331 -64
  8. package/src/AI_Brain/agent_orchestrator.js +73 -0
  9. package/src/AI_Brain/autonomous_brain.js +2 -0
  10. package/src/AI_Brain/memory_store.js +318 -0
  11. package/src/AI_Brain/proactive_engine.js +2 -8
  12. package/src/Automation_Layer/file_operations.js +123 -4
  13. package/src/Automation_Layer/open_app.js +72 -43
  14. package/src/Automation_Layer/open_website.js +3 -3
  15. package/src/CLI/chat_router.js +57 -9
  16. package/src/CLI/chat_ui.js +117 -11
  17. package/src/CLI/code_agent.js +249 -36
  18. package/src/CLI/onboarding.js +53 -6
  19. package/src/CLI/workspace_manager.js +90 -0
  20. package/src/Plugins/docker.js +12 -10
  21. package/src/Plugins/spotify.js +168 -40
  22. package/src/Plugins/system_monitor.js +72 -0
  23. package/src/System/config_manager.js +35 -2
  24. package/src/System/custom_workflows.js +9 -2
  25. package/src/System/notifications.js +23 -0
  26. package/src/UI/settings.html +143 -65
  27. package/src/UI/settings.js +155 -41
  28. package/tests/agent_orchestrator.test.js +41 -0
  29. package/tests/chat_router.test.js +42 -0
  30. package/tests/code_agent.test.js +69 -0
  31. package/tests/config_manager.test.js +141 -0
  32. package/tests/docker.test.js +46 -0
  33. package/tests/file_operations.test.js +57 -0
  34. package/tests/memory_store.test.js +185 -0
  35. package/tests/provider_routing.test.js +67 -0
  36. package/tests/spotify.test.js +201 -0
  37. package/tests/system_monitor.test.js +37 -0
  38. package/tests/workspace_manager.test.js +56 -0
@@ -3,13 +3,16 @@ const path = require('path');
3
3
  const { execFile } = require('child_process');
4
4
  const { promisify } = require('util');
5
5
  const { GoogleGenAI } = require('@google/genai');
6
- const { readConfig } = require('../System/config_manager');
6
+ const axios = require('axios');
7
+ const { readConfig, getAvailableProviders } = require('../System/config_manager');
7
8
  const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
8
9
 
9
10
  const execFileAsync = promisify(execFile);
10
11
  const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
11
12
  const MAX_TOOL_OUTPUT = 12000;
12
13
  const MAX_AGENT_STEPS = 16;
14
+ const MAX_JSON_REPAIR_ATTEMPTS = 2;
15
+ const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
13
16
 
14
17
  const CODE_AGENT_PROMPT = `You are Mint Code Mode, a careful coding agent for a local workspace.
15
18
 
@@ -30,10 +33,11 @@ Rules:
30
33
  Response format:
31
34
  {
32
35
  "thought": "short reasoning",
33
- "action": "list_files" | "read_file" | "search_code" | "run_shell" | "apply_patch" | "write_file" | "finish",
36
+ "action": "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "apply_patch" | "write_file" | "finish",
34
37
  "input": {
35
38
  "path": "relative/path",
36
39
  "query": "search text",
40
+ "type": "file" | "dir" | "any",
37
41
  "command": "shell command",
38
42
  "startLine": 1,
39
43
  "endLine": 120,
@@ -57,6 +61,7 @@ Tool notes:
57
61
  - "list_files": inspect the workspace or a subdirectory.
58
62
  - "read_file": read a file, optionally with startLine/endLine.
59
63
  - "search_code": search by text or regex-like pattern.
64
+ - "find_path": find files or directories by path/name when the user is looking for a folder, filename, or location.
60
65
  - "run_shell": run a non-destructive command in the workspace.
61
66
  - "apply_patch": update an existing file using one or more exact replacement hunks.
62
67
  - "write_file": create a new file or fully rewrite a file when replacement is not practical.
@@ -80,6 +85,22 @@ function extractJson(text) {
80
85
  }
81
86
  }
82
87
 
88
+ function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
89
+ const requestedProvider = (config && config.aiProvider) || 'gemini';
90
+ if (SUPPORTED_CODE_PROVIDERS.includes(requestedProvider) && availableProviders.includes(requestedProvider)) {
91
+ return requestedProvider;
92
+ }
93
+
94
+ const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
95
+ for (const provider of priority) {
96
+ if (availableProviders.includes(provider)) {
97
+ return provider;
98
+ }
99
+ }
100
+
101
+ return 'gemini';
102
+ }
103
+
83
104
  function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
84
105
  const resolved = path.resolve(workspaceRoot, targetPath);
85
106
  const relative = path.relative(workspaceRoot, resolved);
@@ -159,6 +180,40 @@ async function searchCode(workspaceRoot, query) {
159
180
  }
160
181
  }
161
182
 
183
+ async function findPaths(workspaceRoot, query, type = 'any') {
184
+ if (!query || !query.trim()) {
185
+ throw new Error('Path search query is required.');
186
+ }
187
+
188
+ const normalizedType = ['file', 'dir', 'any'].includes(type) ? type : 'any';
189
+ const loweredQuery = query.trim().toLowerCase();
190
+ const results = [];
191
+
192
+ function visit(currentPath) {
193
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
194
+ for (const entry of entries) {
195
+ const absoluteEntryPath = path.join(currentPath, entry.name);
196
+ const relativeEntryPath = path.relative(workspaceRoot, absoluteEntryPath) || '.';
197
+ const entryType = entry.isDirectory() ? 'dir' : 'file';
198
+ const matchesType = normalizedType === 'any' || normalizedType === entryType;
199
+ const matchesQuery = entry.name.toLowerCase().includes(loweredQuery) || relativeEntryPath.toLowerCase().includes(loweredQuery);
200
+
201
+ if (matchesType && matchesQuery) {
202
+ results.push(`${entryType === 'dir' ? '[dir]' : '[file]'} ${relativeEntryPath}`);
203
+ if (results.length >= 200) return;
204
+ }
205
+
206
+ if (entry.isDirectory() && results.length < 200) {
207
+ visit(absoluteEntryPath);
208
+ if (results.length >= 200) return;
209
+ }
210
+ }
211
+ }
212
+
213
+ visit(workspaceRoot);
214
+ return results.length > 0 ? results.join('\n') : '(no matching paths)';
215
+ }
216
+
162
217
  function assertSafeShell(command) {
163
218
  const blockedPatterns = [
164
219
  /\brm\s+-rf\b/,
@@ -244,16 +299,124 @@ function writeFile(workspaceRoot, targetPath, content) {
244
299
  return `Wrote ${targetPath}`;
245
300
  }
246
301
 
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.");
302
+ class UnifiedAgentClient {
303
+ constructor(provider, config) {
304
+ this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
305
+ this.config = config;
306
+ this.history = [];
307
+ this.systemInstruction = CODE_AGENT_PROMPT;
308
+ }
309
+
310
+ async sendMessage(observation) {
311
+ this.history.push({ role: 'user', content: observation });
312
+
313
+ let responseText = '';
314
+ if (this.provider === 'anthropic') {
315
+ responseText = await this._callAnthropic();
316
+ } else if (this.provider === 'openai' || this.provider === 'local_openai') {
317
+ responseText = await this._callOpenAI();
318
+ } else {
319
+ responseText = await this._callGemini();
320
+ }
321
+
322
+ this.history.push({ role: 'assistant', content: responseText });
323
+ return responseText;
324
+ }
325
+
326
+ async _callAnthropic() {
327
+ const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
328
+ const messages = this.history.map(m => ({
329
+ role: m.role,
330
+ content: m.content
331
+ }));
332
+
333
+ const response = await axios.post('https://api.anthropic.com/v1/messages', {
334
+ model: this.config.anthropicModel || 'claude-3-5-sonnet-latest',
335
+ max_tokens: 8192,
336
+ system: this.systemInstruction,
337
+ messages: messages
338
+ }, {
339
+ headers: {
340
+ 'x-api-key': apiKey,
341
+ 'anthropic-version': '2023-06-01',
342
+ 'content-type': 'application/json'
343
+ }
344
+ });
345
+ return response.data.content[0].text;
346
+ }
347
+
348
+ async _callOpenAI() {
349
+ const isLocal = this.provider === 'local_openai';
350
+ const apiKey = isLocal ? 'not-needed' : (this.config.openaiApiKey || process.env.OPENAI_API_KEY);
351
+ const baseUrl = isLocal ? (this.config.localApiBaseUrl || 'http://localhost:1234/v1') : 'https://api.openai.com/v1';
352
+ const model = isLocal ? (this.config.localModelName || 'local-model') : (this.config.openaiModel || 'gpt-4o');
353
+
354
+ const messages = [
355
+ { role: 'system', content: this.systemInstruction },
356
+ ...this.history
357
+ ];
358
+
359
+ const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
360
+ model: model,
361
+ messages: messages,
362
+ response_format: isLocal ? undefined : { type: "json_object" }
363
+ }, {
364
+ headers: {
365
+ 'Authorization': `Bearer ${apiKey}`,
366
+ 'Content-Type': 'application/json'
367
+ }
368
+ });
369
+ return response.data.choices[0].message.content;
370
+ }
371
+
372
+ async _callGemini() {
373
+ const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
374
+ const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
375
+ const ai = new GoogleGenAI({ apiKey });
376
+
377
+ // Convert history for Gemini
378
+ const geminiHistory = this.history.slice(0, -1).map(m => ({
379
+ role: m.role === 'assistant' ? 'model' : 'user',
380
+ parts: [{ text: m.content }]
381
+ }));
382
+
383
+ const lastMessage = this.history[this.history.length - 1].content;
384
+
385
+ const chat = ai.chats.create({
386
+ model,
387
+ config: {
388
+ systemInstruction: this.systemInstruction,
389
+ responseMimeType: 'application/json'
390
+ },
391
+ history: geminiHistory
392
+ });
393
+
394
+ const response = await chat.sendMessage({ message: [{ text: lastMessage }] });
395
+ return typeof response.text === 'function' ? response.text() : response.text;
396
+ }
397
+ }
398
+
399
+ async function getAgentDecision(client, observation, options = {}) {
400
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
401
+ const step = options.step || 0;
402
+
403
+ let rawText = await client.sendMessage(observation);
404
+ for (let attempt = 0; attempt <= MAX_JSON_REPAIR_ATTEMPTS; attempt++) {
405
+ try {
406
+ return extractJson(rawText);
407
+ } catch (error) {
408
+ if (attempt === MAX_JSON_REPAIR_ATTEMPTS) {
409
+ throw new Error(`Agent returned invalid JSON after ${MAX_JSON_REPAIR_ATTEMPTS + 1} attempts: ${error.message}`);
410
+ }
411
+
412
+ onProgress(`Step ${step}: invalid JSON response, requesting repair (${attempt + 1}/${MAX_JSON_REPAIR_ATTEMPTS})`);
413
+ rawText = await client.sendMessage([
414
+ 'Your previous response was not valid JSON for Code Mode.',
415
+ 'Reply again with valid JSON only, following the required schema exactly.',
416
+ `Previous response:\n${truncate(rawText, 4000)}`
417
+ ].join('\n'));
418
+ }
252
419
  }
253
- return {
254
- ai: new GoogleGenAI({ apiKey }),
255
- model: (config.geminiModel || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL
256
- };
257
420
  }
258
421
 
259
422
  function detectPackageManager(workspaceRoot) {
@@ -298,12 +461,17 @@ async function getGitContext(workspaceRoot) {
298
461
  return { isRepo: true, branch, status, diffSummary };
299
462
  }
300
463
 
301
- async function buildInitialObservation(task, workspaceRoot) {
464
+ async function buildInitialObservation(task, workspaceRoot, history = []) {
302
465
  const session = readWorkspaceSession(workspaceRoot);
303
466
  const gitContext = await getGitContext(workspaceRoot);
304
467
  const testCommands = detectTestCommands(workspaceRoot);
305
468
 
469
+ const contextStr = history.length > 0
470
+ ? `Recent Context:\n${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}\n`
471
+ : '';
472
+
306
473
  return [
474
+ contextStr,
307
475
  `Task: ${task}`,
308
476
  `Workspace: ${workspaceRoot}`,
309
477
  `Git branch: ${gitContext.branch}`,
@@ -323,45 +491,41 @@ async function buildInitialObservation(task, workspaceRoot) {
323
491
 
324
492
  async function executeCodeTask(task, options = {}) {
325
493
  const workspaceRoot = path.resolve(options.cwd || process.cwd());
494
+ const history = options.history || [];
326
495
  const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
327
496
  const requestApproval = typeof options.requestApproval === 'function'
328
497
  ? options.requestApproval
329
498
  : 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
- });
499
+ const config = readConfig();
500
+ const provider = options.provider || selectSupportedCodeProvider(config);
501
+ const client = new UnifiedAgentClient(provider, config);
340
502
 
341
- let observation = await buildInitialObservation(task, workspaceRoot);
503
+ let observation = await buildInitialObservation(task, workspaceRoot, history);
504
+
505
+ let finalSummary = '';
506
+ let finalVerification = '';
507
+ let finalSessionSummary = '';
508
+ let executedSteps = 0;
342
509
 
343
510
  for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
511
+ executedSteps = step;
344
512
  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);
513
+ const decision = await getAgentDecision(client, observation, { onProgress, step });
348
514
  const action = decision.action;
349
515
  const input = decision.input || {};
350
516
 
351
517
  onProgress(`Step ${step}: ${action}${input.path ? ` ${input.path}` : input.command ? ` ${input.command}` : ''}`);
352
518
 
353
519
  if (action === 'finish') {
354
- const sessionSummary = input.sessionSummary || input.summary || task;
520
+ finalSessionSummary = input.sessionSummary || input.summary || task;
521
+ finalSummary = input.summary || 'Task complete.';
522
+ finalVerification = input.verification || 'Not specified.';
355
523
  writeWorkspaceSession(workspaceRoot, {
356
- summary: sessionSummary,
524
+ summary: finalSessionSummary,
357
525
  lastTask: task,
358
- lastVerification: input.verification || 'Not specified.'
526
+ lastVerification: finalVerification
359
527
  });
360
- return {
361
- summary: input.summary || 'Task complete.',
362
- verification: input.verification || 'Not specified.',
363
- steps: step
364
- };
528
+ break;
365
529
  }
366
530
 
367
531
  let toolResult = '';
@@ -375,6 +539,9 @@ async function executeCodeTask(task, options = {}) {
375
539
  case 'search_code':
376
540
  toolResult = await searchCode(workspaceRoot, input.query);
377
541
  break;
542
+ case 'find_path':
543
+ toolResult = await findPaths(workspaceRoot, input.query, input.type);
544
+ break;
378
545
  case 'run_shell': {
379
546
  const approved = await requestApproval({
380
547
  type: 'shell',
@@ -427,6 +594,45 @@ async function executeCodeTask(task, options = {}) {
427
594
  ].join('\n');
428
595
  }
429
596
 
597
+ // Check for Agent Collaboration (Review)
598
+ if (config.enableAgentCollaboration !== false) {
599
+ const availableProviders = getAvailableProviders(config);
600
+ // Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
601
+ const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
602
+
603
+ // Fallback to provider itself if no other good ones exist, or pick the best available
604
+ const reviewerProvider = altProviders.length > 0
605
+ ? altProviders[0]
606
+ : (availableProviders.includes('gemini') ? 'gemini' : availableProviders[0]);
607
+
608
+ if (reviewerProvider && finalSummary) {
609
+ onProgress(`Invoking Reviewer Agent (${reviewerProvider})...`);
610
+
611
+ const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
612
+ 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.";
613
+
614
+ 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'.`;
615
+
616
+ try {
617
+ const reviewResponse = await reviewerClient.sendMessage(reviewPrompt);
618
+ const reviewDecision = extractJson(reviewResponse);
619
+ const reviewInput = reviewDecision.input || {};
620
+
621
+ finalSummary += `\n\n[Review by ${reviewerProvider}]\n${reviewInput.summary || reviewDecision.thought || 'Looks good.'}`;
622
+ } catch (e) {
623
+ onProgress(`Reviewer Agent failed: ${e.message}`);
624
+ }
625
+ }
626
+ }
627
+
628
+ if (finalSummary) {
629
+ return {
630
+ summary: finalSummary,
631
+ verification: finalVerification,
632
+ steps: executedSteps
633
+ };
634
+ }
635
+
430
636
  writeWorkspaceSession(workspaceRoot, {
431
637
  summary: `Task stopped before completion: ${task}`,
432
638
  lastTask: task,
@@ -436,8 +642,15 @@ async function executeCodeTask(task, options = {}) {
436
642
  return {
437
643
  summary: 'Stopped after reaching the maximum number of agent steps.',
438
644
  verification: 'Agent limit reached before explicit completion.',
439
- steps: MAX_AGENT_STEPS
645
+ steps: executedSteps || MAX_AGENT_STEPS
440
646
  };
441
647
  }
442
648
 
443
- module.exports = { executeCodeTask };
649
+ module.exports = {
650
+ executeCodeTask,
651
+ _helpers: {
652
+ extractJson,
653
+ selectSupportedCodeProvider,
654
+ findPaths
655
+ }
656
+ };
@@ -18,28 +18,75 @@ async function runOnboarding(options = {}) {
18
18
  {
19
19
  type: 'input',
20
20
  name: 'apiKey',
21
- message: 'Please enter your Google Gemini API Key:',
21
+ message: 'Enter your Google Gemini API Key (Required for basic features):',
22
22
  default: config.apiKey || undefined,
23
- validate: (input) => input.length > 0 ? true : 'API Key is required.'
23
+ validate: (input) => input.trim().length > 0 ? true : 'API Key is required.'
24
24
  },
25
25
  {
26
26
  type: 'list',
27
- name: 'geminiModel',
28
- message: 'Select the Gemini model to use:',
27
+ name: 'geminiModelChoice',
28
+ message: 'Select the primary Gemini model to use:',
29
29
  choices: [
30
30
  'gemini-2.5-flash',
31
+ 'gemini-2.0-pro-exp-02-05',
31
32
  'gemini-3.1-flash-lite-preview',
32
33
  'gemini-3.1-flash-lite',
33
- 'gemini-2.0-pro-exp-02-05'
34
+ 'Custom model name'
34
35
  ],
35
36
  default: config.geminiModel || 'gemini-2.5-flash'
37
+ },
38
+ {
39
+ type: 'input',
40
+ name: 'customGeminiModel',
41
+ message: 'Enter your custom Gemini model name:',
42
+ when: (answers) => answers.geminiModelChoice === 'Custom model name',
43
+ validate: (input) => input.trim().length > 0 ? true : 'Please enter a valid model name.'
44
+ },
45
+ {
46
+ type: 'input',
47
+ name: 'anthropicApiKey',
48
+ message: 'Enter your Anthropic API Key (Optional, press Enter to skip):',
49
+ default: config.anthropicApiKey || ''
50
+ },
51
+ {
52
+ type: 'input',
53
+ name: 'openaiApiKey',
54
+ message: 'Enter your OpenAI API Key (Optional, press Enter to skip):',
55
+ default: config.openaiApiKey || ''
56
+ },
57
+ {
58
+ type: 'input',
59
+ name: 'hfApiKey',
60
+ message: 'Enter your Hugging Face API Key (Optional, press Enter to skip):',
61
+ default: config.hfApiKey || ''
62
+ },
63
+ {
64
+ type: 'input',
65
+ name: 'localApiBaseUrl',
66
+ message: 'Enter your Local AI (LM Studio/OpenAI Compatible) Base URL (Optional, press Enter to skip):',
67
+ default: config.localApiBaseUrl || ''
68
+ },
69
+ {
70
+ type: 'input',
71
+ name: 'localModelName',
72
+ message: 'Enter your Local Model Name (Optional, press Enter to skip):',
73
+ default: config.localModelName || ''
36
74
  }
37
75
  ];
38
76
 
39
77
  const answers = await inquirer.prompt(questions);
78
+
79
+ // Resolve custom gemini model if selected
80
+ const geminiModel = answers.geminiModelChoice === 'Custom model name'
81
+ ? answers.customGeminiModel
82
+ : answers.geminiModelChoice;
83
+
84
+ // Remove temporary choice fields before saving
85
+ delete answers.geminiModelChoice;
86
+ delete answers.customGeminiModel;
40
87
 
41
88
  // Save configuration
42
- const newConfig = { ...config, ...answers };
89
+ const newConfig = { ...config, ...answers, geminiModel };
43
90
  writeConfig(newConfig);
44
91
  console.log('\n✅ Configuration saved successfully!');
45
92
 
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Mint Workspace Manager
3
+ * -----------------------
4
+ * Manages project-specific contexts and persistent workspaces.
5
+ * Stores data in ~/.config/mint/workspaces.json
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ function getWorkspaceFile() {
13
+ return process.env.MINT_WORKSPACE_FILE || path.join(os.homedir(), '.config', 'mint', 'workspaces.json');
14
+ }
15
+
16
+ function ensureDir() {
17
+ const dir = path.dirname(getWorkspaceFile());
18
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+
21
+ function loadWorkspaces() {
22
+ const workspaceFile = getWorkspaceFile();
23
+ ensureDir();
24
+ if (!fs.existsSync(workspaceFile)) return {};
25
+ try {
26
+ return JSON.parse(fs.readFileSync(workspaceFile, 'utf8'));
27
+ } catch (e) {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ function saveWorkspaces(data) {
33
+ const workspaceFile = getWorkspaceFile();
34
+ ensureDir();
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));
41
+ }
42
+
43
+ function addWorkspace(name, rootPath, instructions = '') {
44
+ const workspaces = loadWorkspaces();
45
+ const absolutePath = path.resolve(rootPath);
46
+ workspaces[name] = {
47
+ name,
48
+ path: absolutePath,
49
+ instructions,
50
+ addedAt: new Date().toISOString(),
51
+ lastAccessed: new Date().toISOString()
52
+ };
53
+ saveWorkspaces(workspaces);
54
+ return workspaces[name];
55
+ }
56
+
57
+ function removeWorkspace(name) {
58
+ const workspaces = loadWorkspaces();
59
+ if (workspaces[name]) {
60
+ delete workspaces[name];
61
+ saveWorkspaces(workspaces);
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ function getWorkspaceByPath(currentPath) {
68
+ const workspaces = loadWorkspaces();
69
+ const absoluteCurrent = path.resolve(currentPath);
70
+
71
+ // Find workspace where current path is inside or equal to workspace path
72
+ for (const name in workspaces) {
73
+ const ws = workspaces[name];
74
+ if (isPathInsideWorkspace(absoluteCurrent, ws.path)) {
75
+ return ws;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function listWorkspaces() {
82
+ return loadWorkspaces();
83
+ }
84
+
85
+ module.exports = {
86
+ addWorkspace,
87
+ removeWorkspace,
88
+ getWorkspaceByPath,
89
+ listWorkspaces
90
+ };
@@ -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') {