@indiccoder/mentis-cli 1.1.4 → 1.1.5

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 (64) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.mentis/session.json +15 -0
  3. package/.mentis/sessions/1769189035730.json +23 -0
  4. package/.mentis/sessions/1769189569160.json +23 -0
  5. package/.mentis/sessions/1769767538672.json +23 -0
  6. package/.mentis/sessions/1769767785155.json +23 -0
  7. package/.mentis/sessions/1769768745802.json +23 -0
  8. package/.mentis/sessions/1769769600884.json +31 -0
  9. package/.mentis/sessions/1769770030160.json +31 -0
  10. package/.mentis/sessions/1769770606004.json +78 -0
  11. package/.mentis/sessions/1769771084515.json +141 -0
  12. package/.mentis/sessions/1769881926630.json +57 -0
  13. package/README.md +17 -0
  14. package/dist/checkpoint/CheckpointManager.js +92 -0
  15. package/dist/debug_google.js +61 -0
  16. package/dist/debug_lite.js +49 -0
  17. package/dist/debug_lite_headers.js +57 -0
  18. package/dist/debug_search.js +16 -0
  19. package/dist/index.js +10 -0
  20. package/dist/mcp/JsonRpcClient.js +16 -0
  21. package/dist/mcp/McpConfig.js +132 -0
  22. package/dist/mcp/McpManager.js +189 -0
  23. package/dist/repl/PersistentShell.js +20 -1
  24. package/dist/repl/ReplManager.js +410 -138
  25. package/dist/tools/AskQuestionTool.js +172 -0
  26. package/dist/tools/EditFileTool.js +141 -0
  27. package/dist/tools/FileTools.js +7 -1
  28. package/dist/tools/PlanModeTool.js +53 -0
  29. package/dist/tools/WebSearchTool.js +190 -27
  30. package/dist/ui/DiffViewer.js +110 -0
  31. package/dist/ui/InputBox.js +16 -2
  32. package/dist/ui/MultiFileSelector.js +123 -0
  33. package/dist/ui/PlanModeUI.js +105 -0
  34. package/dist/ui/ToolExecutor.js +154 -0
  35. package/dist/ui/UIManager.js +12 -2
  36. package/docs/MCP_INTEGRATION.md +290 -0
  37. package/google_dump.html +18 -0
  38. package/lite_dump.html +176 -0
  39. package/lite_headers_dump.html +176 -0
  40. package/package.json +16 -5
  41. package/scripts/test_exa_mcp.ts +90 -0
  42. package/src/checkpoint/CheckpointManager.ts +102 -0
  43. package/src/debug_google.ts +30 -0
  44. package/src/debug_lite.ts +18 -0
  45. package/src/debug_lite_headers.ts +25 -0
  46. package/src/debug_search.ts +18 -0
  47. package/src/index.ts +12 -0
  48. package/src/mcp/JsonRpcClient.ts +19 -0
  49. package/src/mcp/McpConfig.ts +153 -0
  50. package/src/mcp/McpManager.ts +224 -0
  51. package/src/repl/PersistentShell.ts +24 -1
  52. package/src/repl/ReplManager.ts +1521 -1204
  53. package/src/tools/AskQuestionTool.ts +197 -0
  54. package/src/tools/EditFileTool.ts +172 -0
  55. package/src/tools/FileTools.ts +3 -0
  56. package/src/tools/PlanModeTool.ts +50 -0
  57. package/src/tools/WebSearchTool.ts +235 -63
  58. package/src/ui/DiffViewer.ts +117 -0
  59. package/src/ui/InputBox.ts +17 -2
  60. package/src/ui/MultiFileSelector.ts +135 -0
  61. package/src/ui/PlanModeUI.ts +121 -0
  62. package/src/ui/ToolExecutor.ts +182 -0
  63. package/src/ui/UIManager.ts +15 -2
  64. package/console.log(tick) +0 -0
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AskQuestionTool = void 0;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ /**
10
+ * AskQuestionTool - Allows the AI to ask clarifying questions
11
+ * This enables interactive plan mode where AI can gather requirements
12
+ */
13
+ class AskQuestionTool {
14
+ constructor() {
15
+ this.name = 'ask_question';
16
+ this.description = 'Ask the user a clarifying question. Use this in plan mode to gather requirements before implementation. Supports: text, confirm (yes/no), list (single choice), checkbox (multi-select).';
17
+ this.parameters = {
18
+ type: 'object',
19
+ properties: {
20
+ question: {
21
+ type: 'string',
22
+ description: 'The question to ask the user'
23
+ },
24
+ type: {
25
+ type: 'string',
26
+ enum: ['text', 'confirm', 'list', 'checkbox'],
27
+ description: 'Type of question: text (free input), confirm (yes/no), list (single choice), checkbox (multi-select)'
28
+ },
29
+ options: {
30
+ type: 'array',
31
+ items: {
32
+ type: 'object',
33
+ properties: {
34
+ label: { type: 'string', description: 'Display text for the option' },
35
+ description: { type: 'string', description: 'Additional context (optional)' }
36
+ }
37
+ },
38
+ description: 'Options for list/checkbox questions. Required for list/checkbox types.'
39
+ },
40
+ default: {
41
+ oneOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'array' }],
42
+ description: 'Default value (optional)'
43
+ }
44
+ },
45
+ required: ['question', 'type']
46
+ };
47
+ }
48
+ /**
49
+ * Execute the question and return the user's answer
50
+ */
51
+ async execute(args) {
52
+ const questionType = args.type || 'text';
53
+ // Show question header
54
+ console.log('');
55
+ console.log(chalk_1.default.cyan('🤔 Question from AI:'));
56
+ console.log(chalk_1.default.gray('─'.repeat(60)));
57
+ try {
58
+ let result;
59
+ switch (questionType) {
60
+ case 'text':
61
+ result = await this.askText(args.question, args.default);
62
+ break;
63
+ case 'confirm':
64
+ result = await this.askConfirm(args.question, args.default);
65
+ break;
66
+ case 'list':
67
+ if (!args.options || args.options.length === 0) {
68
+ return 'Error: list questions require options';
69
+ }
70
+ result = await this.askList(args.question, args.options, args.default);
71
+ break;
72
+ case 'checkbox':
73
+ if (!args.options || args.options.length === 0) {
74
+ return 'Error: checkbox questions require options';
75
+ }
76
+ result = await this.askCheckbox(args.question, args.options, args.default);
77
+ break;
78
+ default:
79
+ return `Error: Unknown question type: ${questionType}`;
80
+ }
81
+ console.log(chalk_1.default.gray('─'.repeat(60)));
82
+ // Format result as string for return to LLM
83
+ return this.formatResult(result, questionType);
84
+ }
85
+ catch (error) {
86
+ return `Error asking question: ${error.message}`;
87
+ }
88
+ }
89
+ /**
90
+ * Ask a free-text question
91
+ */
92
+ async askText(question, defaultValue) {
93
+ const { answer } = await inquirer_1.default.prompt([
94
+ {
95
+ type: 'input',
96
+ name: 'answer',
97
+ message: question,
98
+ default: defaultValue
99
+ }
100
+ ]);
101
+ return answer;
102
+ }
103
+ /**
104
+ * Ask a yes/no confirmation
105
+ */
106
+ async askConfirm(question, defaultValue) {
107
+ const { answer } = await inquirer_1.default.prompt([
108
+ {
109
+ type: 'confirm',
110
+ name: 'answer',
111
+ message: question,
112
+ default: defaultValue ?? true
113
+ }
114
+ ]);
115
+ return answer;
116
+ }
117
+ /**
118
+ * Ask a single-choice list question
119
+ */
120
+ async askList(question, options, defaultValue) {
121
+ const { answer } = await inquirer_1.default.prompt([
122
+ {
123
+ type: 'list',
124
+ name: 'answer',
125
+ message: question,
126
+ choices: options.map((opt, i) => ({
127
+ name: opt.label,
128
+ value: opt.label,
129
+ short: opt.label
130
+ })),
131
+ default: defaultValue
132
+ }
133
+ ]);
134
+ return answer;
135
+ }
136
+ /**
137
+ * Ask a multi-select checkbox question
138
+ */
139
+ async askCheckbox(question, options, defaultValue) {
140
+ const { answer } = await inquirer_1.default.prompt([
141
+ {
142
+ type: 'checkbox',
143
+ name: 'answer',
144
+ message: question,
145
+ choices: options.map((opt, i) => ({
146
+ name: opt.label,
147
+ value: opt.label,
148
+ checked: defaultValue?.includes(opt.label),
149
+ short: opt.label
150
+ }))
151
+ }
152
+ ]);
153
+ return answer;
154
+ }
155
+ /**
156
+ * Format the result for return to the LLM
157
+ */
158
+ formatResult(result, questionType) {
159
+ switch (questionType) {
160
+ case 'confirm':
161
+ return result === true ? 'Yes' : 'No';
162
+ case 'checkbox':
163
+ if (Array.isArray(result) && result.length > 0) {
164
+ return `Selected: ${result.join(', ')}`;
165
+ }
166
+ return 'None selected';
167
+ default:
168
+ return String(result);
169
+ }
170
+ }
171
+ }
172
+ exports.AskQuestionTool = AskQuestionTool;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EditFileTool = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ /**
7
+ * EditFileTool - Performs string replacement in files (like Claude's Edit tool)
8
+ * Returns a unified diff preview instead of writing immediately
9
+ */
10
+ class EditFileTool {
11
+ constructor() {
12
+ this.name = 'edit_file';
13
+ this.description = 'Make targeted edits to files using string replacement. Returns diff preview. Requires approval before writing.';
14
+ this.parameters = {
15
+ type: 'object',
16
+ properties: {
17
+ file_path: {
18
+ type: 'string',
19
+ description: 'The path to the file to edit'
20
+ },
21
+ old_string: {
22
+ type: 'string',
23
+ description: 'The exact string to replace. Must match exactly (including whitespace).'
24
+ },
25
+ new_string: {
26
+ type: 'string',
27
+ description: 'The new string to replace old_string with.'
28
+ },
29
+ auto_format: {
30
+ type: 'boolean',
31
+ description: 'Auto-format code after edit (default: false)'
32
+ }
33
+ },
34
+ required: ['file_path', 'old_string', 'new_string']
35
+ };
36
+ }
37
+ /**
38
+ * Execute the edit and return a diff preview
39
+ * Note: This does NOT write the file - it returns what WOULD change
40
+ * The caller (ReplManager) should handle approval before calling applyEdit()
41
+ */
42
+ async execute(args) {
43
+ const filePath = (0, path_1.resolve)(process.cwd(), args.file_path);
44
+ if (!(0, fs_1.existsSync)(filePath)) {
45
+ return `Error: File not found: ${args.file_path}`;
46
+ }
47
+ const originalContent = (0, fs_1.readFileSync)(filePath, 'utf-8');
48
+ if (!originalContent.includes(args.old_string)) {
49
+ return `Error: old_string not found in file. The string must match exactly (including whitespace and indentation).`;
50
+ }
51
+ // Count occurrences
52
+ const occurrences = (originalContent.match(new RegExp(this.escapeRegex(args.old_string), 'g')) || []).length;
53
+ if (occurrences > 1) {
54
+ return `Warning: old_string found ${occurrences} times. All occurrences will be replaced.\n\n${this.generateDiff(originalContent, args.old_string, args.new_string, args.file_path)}`;
55
+ }
56
+ // Generate and return diff
57
+ return this.generateDiff(originalContent, args.old_string, args.new_string, args.file_path);
58
+ }
59
+ /**
60
+ * Apply the edit after approval
61
+ * This should be called after user approves the diff
62
+ */
63
+ applyEdit(args) {
64
+ const filePath = (0, path_1.resolve)(process.cwd(), args.file_path);
65
+ if (!(0, fs_1.existsSync)(filePath)) {
66
+ return { success: false, message: `File not found: ${args.file_path}` };
67
+ }
68
+ const originalContent = (0, fs_1.readFileSync)(filePath, 'utf-8');
69
+ if (!originalContent.includes(args.old_string)) {
70
+ return { success: false, message: 'old_string not found in file' };
71
+ }
72
+ const newContent = originalContent.replace(args.old_string, args.new_string);
73
+ (0, fs_1.writeFileSync)(filePath, newContent, 'utf-8');
74
+ return {
75
+ success: true,
76
+ message: `Successfully edited ${args.file_path}`
77
+ };
78
+ }
79
+ /**
80
+ * Generate a unified diff preview
81
+ */
82
+ generateDiff(content, oldString, newString, filePath) {
83
+ const lines = content.split('\n');
84
+ const oldLines = oldString.split('\n');
85
+ const newLines = newString.split('\n');
86
+ // Find the line number where old_string starts
87
+ let startLine = -1;
88
+ for (let i = 0; i <= lines.length - oldLines.length; i++) {
89
+ let match = true;
90
+ for (let j = 0; j < oldLines.length; j++) {
91
+ if (lines[i + j] !== oldLines[j]) {
92
+ match = false;
93
+ break;
94
+ }
95
+ }
96
+ if (match) {
97
+ startLine = i;
98
+ break;
99
+ }
100
+ }
101
+ if (startLine === -1) {
102
+ return 'Error: Could not locate old_string in file';
103
+ }
104
+ // Build unified diff
105
+ let diff = `\n${'─'.repeat(60)}\n`;
106
+ diff += `📝 Edit Preview: ${filePath}\n`;
107
+ diff += `${'─'.repeat(60)}\n`;
108
+ diff += `Line ${startLine + 1}:\n\n`;
109
+ // Show context (2 lines before)
110
+ const contextStart = Math.max(0, startLine - 2);
111
+ if (contextStart < startLine) {
112
+ for (let i = contextStart; i < startLine; i++) {
113
+ diff += ` ${lines[i]}\n`;
114
+ }
115
+ }
116
+ // Show removed lines (red)
117
+ for (const line of oldLines) {
118
+ diff += `\x1b[31m- ${line}\x1b[0m\n`;
119
+ }
120
+ // Show added lines (green)
121
+ for (const line of newLines) {
122
+ diff += `\x1b[32m+ ${line}\x1b[0m\n`;
123
+ }
124
+ // Show context (2 lines after)
125
+ const contextEnd = Math.min(lines.length, startLine + oldLines.length + 2);
126
+ if (startLine + oldLines.length < contextEnd) {
127
+ for (let i = startLine + oldLines.length; i < contextEnd; i++) {
128
+ diff += ` ${lines[i]}\n`;
129
+ }
130
+ }
131
+ diff += `${'─'.repeat(60)}\n`;
132
+ return diff;
133
+ }
134
+ /**
135
+ * Escape special regex characters
136
+ */
137
+ escapeRegex(str) {
138
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
139
+ }
140
+ }
141
+ exports.EditFileTool = EditFileTool;
@@ -3,9 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ListDirTool = exports.ReadFileTool = exports.WriteFileTool = void 0;
6
+ exports.ListDirTool = exports.ReadFileTool = exports.WriteFileTool = exports.PlanModeTool = exports.AskQuestionTool = exports.EditFileTool = void 0;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ var EditFileTool_1 = require("./EditFileTool");
10
+ Object.defineProperty(exports, "EditFileTool", { enumerable: true, get: function () { return EditFileTool_1.EditFileTool; } });
11
+ var AskQuestionTool_1 = require("./AskQuestionTool");
12
+ Object.defineProperty(exports, "AskQuestionTool", { enumerable: true, get: function () { return AskQuestionTool_1.AskQuestionTool; } });
13
+ var PlanModeTool_1 = require("./PlanModeTool");
14
+ Object.defineProperty(exports, "PlanModeTool", { enumerable: true, get: function () { return PlanModeTool_1.PlanModeTool; } });
9
15
  class WriteFileTool {
10
16
  constructor() {
11
17
  this.name = 'write_file';
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PlanModeTool = void 0;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const PlanModeUI_1 = require("../ui/PlanModeUI");
10
+ /**
11
+ * PlanModeTool - Allows AI to suggest switching to plan mode
12
+ * Use this when the task is complex, requires architecture design, or needs requirements gathering
13
+ */
14
+ class PlanModeTool {
15
+ constructor() {
16
+ this.name = 'enter_plan_mode';
17
+ this.description = 'Suggest switching to plan mode for complex tasks. Call this when you need to gather requirements, design architecture, or break down a complex implementation before coding.';
18
+ this.parameters = {
19
+ type: 'object',
20
+ properties: {
21
+ reason: {
22
+ type: 'string',
23
+ description: 'Explain why plan mode is recommended for this task'
24
+ }
25
+ },
26
+ required: ['reason']
27
+ };
28
+ }
29
+ /**
30
+ * Execute - Ask user if they want to switch to plan mode
31
+ */
32
+ async execute(args) {
33
+ console.log('');
34
+ console.log(chalk_1.default.cyan('🎯 AI suggests entering PLAN MODE'));
35
+ console.log(chalk_1.default.gray('─'.repeat(60)));
36
+ console.log(chalk_1.default.dim(`Reason: ${args.reason}`));
37
+ console.log(chalk_1.default.gray('─'.repeat(60)));
38
+ const { confirm } = await inquirer_1.default.prompt([
39
+ {
40
+ type: 'confirm',
41
+ name: 'confirm',
42
+ message: 'Switch to plan mode?',
43
+ default: true
44
+ }
45
+ ]);
46
+ if (confirm) {
47
+ PlanModeUI_1.PlanModeUI.showPlanHeader();
48
+ return 'User approved. Switching to plan mode for requirements gathering and planning.';
49
+ }
50
+ return 'User declined. Continuing in current mode.';
51
+ }
52
+ }
53
+ exports.PlanModeTool = PlanModeTool;
@@ -4,12 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.WebSearchTool = void 0;
7
- const duck_duck_scrape_1 = require("duck-duck-scrape");
8
7
  const chalk_1 = __importDefault(require("chalk"));
8
+ const child_process_1 = require("child_process");
9
+ const os_1 = __importDefault(require("os"));
9
10
  class WebSearchTool {
10
11
  constructor() {
11
12
  this.name = 'search_web';
12
- this.description = 'Search the internet for documentation, libraries, or solutions to errors. Returns snippets of top results.';
13
+ this.description = 'Search internet for documentation, libraries, or solutions to errors. Returns snippets of top results.';
13
14
  this.parameters = {
14
15
  type: 'object',
15
16
  properties: {
@@ -21,40 +22,202 @@ class WebSearchTool {
21
22
  required: ['query']
22
23
  };
23
24
  }
25
+ /**
26
+ * Execute search using a hybrid strategy:
27
+ * 1. Tavily API (if configured) - Most Reliable
28
+ * 2. NPM/Expo Registry (if applicable) - Bypasses search engines
29
+ * 3. Google Curl - Mimics browser request
30
+ * 4. DuckDuckGo Lite - Low bandwidth fallback
31
+ * 5. DuckDuckScrape Library - Last resort
32
+ */
24
33
  async execute(args) {
25
- try {
26
- // Priority 1: Google Search
34
+ // 1. Try API Key (Most Reliable)
35
+ if (process.env.TAVILY_API_KEY) {
27
36
  try {
28
- // Dynamic import to avoid build issues if types are missing
29
- const { search: googleSearch } = require('google-sr');
30
- console.log(chalk_1.default.dim(` Searching Google for: "${args.query}"...`));
31
- const googleResults = await googleSearch({
32
- query: args.query,
33
- limit: 5,
34
- });
35
- if (googleResults && googleResults.length > 0) {
36
- const formatted = googleResults.map(r => `[${r.title}](${r.link})\n${r.description || 'No description.'}`).join('\n\n');
37
- return `Top Google Results:\n\n${formatted}`;
37
+ return await this.searchTavily(args.query, process.env.TAVILY_API_KEY);
38
+ }
39
+ catch (e) {
40
+ console.error(chalk_1.default.red('Tavily search failed, falling back to scraper.'));
41
+ }
42
+ }
43
+ // 2. Specific Fallback: NPM/Expo Queries (Bypass Search Engines)
44
+ // If user asks for versions, use npm directly
45
+ if (args.query.toLowerCase().includes('expo') || args.query.toLowerCase().includes('react native')) {
46
+ try {
47
+ const npmInfo = await this.checkNpmVersion('expo');
48
+ if (npmInfo) {
49
+ return `NPM Registry Info:\n${npmInfo}\n\n(Web search was blocked, but I checked NPM directly.)`;
38
50
  }
39
51
  }
40
- catch (googleError) {
41
- console.log(chalk_1.default.dim(` Google search failed (${googleError.message}), failing over to DuckDuckGo...`));
52
+ catch (e) {
53
+ // Ignore npm error
42
54
  }
43
- // Priority 2: DuckDuckGo Fallback
44
- console.log(chalk_1.default.dim(` Searching DuckDuckGo for: "${args.query}"...`));
45
- const ddgResults = await (0, duck_duck_scrape_1.search)(args.query, {
46
- safeSearch: 0
47
- });
48
- if (!ddgResults.results || ddgResults.results.length === 0) {
49
- return 'No results found.';
55
+ }
56
+ // Rate limit protection
57
+ await new Promise(resolve => setTimeout(resolve, 2000));
58
+ try {
59
+ // Strategy 1: Google Curl (Specific Browser Header)
60
+ try {
61
+ const googleResults = await this.searchGoogleCurl(args.query);
62
+ if (googleResults.length > 0)
63
+ return this.formatResults(googleResults, 'Google');
50
64
  }
51
- // Return top 5 results
52
- const topResults = ddgResults.results.slice(0, 5).map(r => `[${r.title}](${r.url})\n${r.description || 'No description found.'}`).join('\n\n');
53
- return `Top Search Results (via DDG):\n\n${topResults}`;
65
+ catch (e) {
66
+ // Ignore
67
+ }
68
+ // Strategy 2: DDG Lite
69
+ try {
70
+ const liteResults = await this.searchDuckDuckGoLite(args.query);
71
+ if (liteResults.length > 0)
72
+ return this.formatResults(liteResults, 'DDG Lite');
73
+ }
74
+ catch (e) {
75
+ // Ignore
76
+ }
77
+ // Strategy 3: Library Fallback
78
+ console.log(chalk_1.default.dim(` Direct scraping failed, falling back to library...`));
79
+ const { search } = require('duck-duck-scrape');
80
+ const ddgResults = await search(args.query, { safeSearch: 0 });
81
+ if (!ddgResults.results?.length)
82
+ throw new Error('No results from library');
83
+ return `Top Search Results (Library):\n\n` +
84
+ ddgResults.results.slice(0, 5).map((r) => `[${r.title}](${r.url})\n${r.description || 'No description found.'}`).join('\n\n');
54
85
  }
55
86
  catch (error) {
56
- return `Error searching web: ${error.message}`;
87
+ return `Web Search Failed (CAPTCHA Blocked).
88
+
89
+ Search engines are blocking automated requests from your network.
90
+
91
+ To enable web search, get a free Tavily API key:
92
+ 1. Go to https://tavily.com
93
+ 2. Sign up for free
94
+ 3. Add to your .env: TAVILY_API_KEY=your_key_here
95
+
96
+ Alternatively, use Exa via MCP:
97
+ 1. Get key at https://exa.ai
98
+ 2. Add EXA_API_KEY to .env
99
+ 3. Run: /mcp connect "Exa Search"`;
100
+ }
101
+ }
102
+ formatResults(results, source) {
103
+ const formatted = results.slice(0, 5).map(r => `[${r.title}](${r.url})\n${r.snippet || 'No description.'}`).join('\n\n');
104
+ return `Top Search Results (${source}):\n\n${formatted}`;
105
+ }
106
+ async searchTavily(query, apiKey) {
107
+ console.log(chalk_1.default.dim(` Searching Tavily for: "${query}"...`));
108
+ try {
109
+ const response = await fetch("https://api.tavily.com/search", {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({ api_key: apiKey, query, search_depth: "basic", include_answer: true })
113
+ });
114
+ const data = await response.json();
115
+ const results = data.results.map((r) => `[${r.title}](${r.url})\n${r.content}`).join('\n\n');
116
+ return `Tavily Results:\n\n${results}`;
117
+ }
118
+ catch (e) {
119
+ throw new Error(`Tavily Error: ${e.message}`);
57
120
  }
58
121
  }
122
+ async checkNpmVersion(pkg) {
123
+ return new Promise((resolve) => {
124
+ (0, child_process_1.exec)(`npm view ${pkg} name version dist-tags --json`, { maxBuffer: 1024 * 1024 }, (error, stdout) => {
125
+ if (error || !stdout)
126
+ resolve('');
127
+ try {
128
+ const info = JSON.parse(stdout);
129
+ resolve(`Package: ${info.name}\nLatest Version: ${info.version}\nTags: ${JSON.stringify(info['dist-tags'])}`);
130
+ }
131
+ catch {
132
+ resolve('');
133
+ }
134
+ });
135
+ });
136
+ }
137
+ async searchGoogleCurl(query) {
138
+ console.log(chalk_1.default.dim(` Searching Google (Curl) for: "${query}"...`));
139
+ const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&hl=en`;
140
+ const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
141
+ // Use curl.exe explicitly on Windows
142
+ const curlCmd = os_1.default.platform() === 'win32' ? 'curl.exe' : 'curl';
143
+ return new Promise((resolve, reject) => {
144
+ (0, child_process_1.exec)(`${curlCmd} -s -L -A "${userAgent}" "${url}"`, { maxBuffer: 1024 * 1024 * 2 }, (error, stdout) => {
145
+ if (error) {
146
+ reject(error);
147
+ return;
148
+ }
149
+ const html = stdout;
150
+ const results = [];
151
+ // Matches standard google result anchors: <a href="/url?q=..." ...><h3 ...>Title</h3>...
152
+ const linkRegex = /<a href="\/url\?q=([^&]+)&amp;[^"]+">[^<]*<h3[^>]*><div[^>]*>([^<]+)<\/div><\/h3>/g;
153
+ let match;
154
+ while ((match = linkRegex.exec(html)) !== null) {
155
+ results.push({
156
+ url: decodeURIComponent(match[1]),
157
+ title: this.decodeHtml(match[2]),
158
+ snippet: ''
159
+ });
160
+ }
161
+ resolve(results);
162
+ });
163
+ });
164
+ }
165
+ async curl(url) {
166
+ const curl = os_1.default.platform() === 'win32' ? 'curl.exe' : 'curl';
167
+ const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
168
+ return new Promise((resolve, reject) => {
169
+ (0, child_process_1.exec)(`${curl} -s -L -A "${userAgent}" "${url}"`, { maxBuffer: 1024 * 1024 * 2 }, (error, stdout, stderr) => {
170
+ if (error) {
171
+ reject(error);
172
+ return;
173
+ }
174
+ resolve(stdout);
175
+ });
176
+ });
177
+ }
178
+ async searchDuckDuckGoHtml(query) {
179
+ console.log(chalk_1.default.dim(` Searching DuckDuckGo (HTML) for: "${query}"...`));
180
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
181
+ const html = await this.curl(url);
182
+ const results = [];
183
+ const chunks = html.split('class="result__body"');
184
+ for (let i = 1; i < chunks.length; i++) {
185
+ const chunk = chunks[i];
186
+ const titleMatch = chunk.match(/class="result__a" href="([^"]+)">(.*?)<\/a>/);
187
+ const snippetMatch = chunk.match(/class="result__snippet"[^>]*>(.*?)<\/a>/);
188
+ if (titleMatch) {
189
+ results.push({
190
+ url: titleMatch[1],
191
+ title: this.decodeHtml(titleMatch[2].replace(/<[^>]+>/g, '').trim()),
192
+ snippet: snippetMatch ? this.decodeHtml(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''
193
+ });
194
+ }
195
+ }
196
+ return results;
197
+ }
198
+ async searchDuckDuckGoLite(query) {
199
+ console.log(chalk_1.default.dim(` Searching DuckDuckGo (Lite) for: "${query}"...`));
200
+ const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
201
+ const html = await this.curl(url);
202
+ const results = [];
203
+ const regex = /<a[^>]+class="result-link"[^>]+href="(.*?)"[^>]*>(.*?)<\/a>/g;
204
+ let match;
205
+ while ((match = regex.exec(html)) !== null) {
206
+ results.push({
207
+ url: match[1],
208
+ title: this.decodeHtml(match[2].replace(/<[^>]+>/g, '').trim()),
209
+ snippet: 'Click to view.'
210
+ });
211
+ }
212
+ return results;
213
+ }
214
+ decodeHtml(str) {
215
+ return str
216
+ .replace(/&amp;/g, '&')
217
+ .replace(/&lt;/g, '<')
218
+ .replace(/&gt;/g, '>')
219
+ .replace(/&quot;/g, '"')
220
+ .replace(/&#39;/g, "'");
221
+ }
59
222
  }
60
223
  exports.WebSearchTool = WebSearchTool;