@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.
- package/.claude/settings.local.json +8 -0
- package/.mentis/session.json +15 -0
- package/.mentis/sessions/1769189035730.json +23 -0
- package/.mentis/sessions/1769189569160.json +23 -0
- package/.mentis/sessions/1769767538672.json +23 -0
- package/.mentis/sessions/1769767785155.json +23 -0
- package/.mentis/sessions/1769768745802.json +23 -0
- package/.mentis/sessions/1769769600884.json +31 -0
- package/.mentis/sessions/1769770030160.json +31 -0
- package/.mentis/sessions/1769770606004.json +78 -0
- package/.mentis/sessions/1769771084515.json +141 -0
- package/.mentis/sessions/1769881926630.json +57 -0
- package/README.md +17 -0
- package/dist/checkpoint/CheckpointManager.js +92 -0
- package/dist/debug_google.js +61 -0
- package/dist/debug_lite.js +49 -0
- package/dist/debug_lite_headers.js +57 -0
- package/dist/debug_search.js +16 -0
- package/dist/index.js +10 -0
- package/dist/mcp/JsonRpcClient.js +16 -0
- package/dist/mcp/McpConfig.js +132 -0
- package/dist/mcp/McpManager.js +189 -0
- package/dist/repl/PersistentShell.js +20 -1
- package/dist/repl/ReplManager.js +410 -138
- package/dist/tools/AskQuestionTool.js +172 -0
- package/dist/tools/EditFileTool.js +141 -0
- package/dist/tools/FileTools.js +7 -1
- package/dist/tools/PlanModeTool.js +53 -0
- package/dist/tools/WebSearchTool.js +190 -27
- package/dist/ui/DiffViewer.js +110 -0
- package/dist/ui/InputBox.js +16 -2
- package/dist/ui/MultiFileSelector.js +123 -0
- package/dist/ui/PlanModeUI.js +105 -0
- package/dist/ui/ToolExecutor.js +154 -0
- package/dist/ui/UIManager.js +12 -2
- package/docs/MCP_INTEGRATION.md +290 -0
- package/google_dump.html +18 -0
- package/lite_dump.html +176 -0
- package/lite_headers_dump.html +176 -0
- package/package.json +16 -5
- package/scripts/test_exa_mcp.ts +90 -0
- package/src/checkpoint/CheckpointManager.ts +102 -0
- package/src/debug_google.ts +30 -0
- package/src/debug_lite.ts +18 -0
- package/src/debug_lite_headers.ts +25 -0
- package/src/debug_search.ts +18 -0
- package/src/index.ts +12 -0
- package/src/mcp/JsonRpcClient.ts +19 -0
- package/src/mcp/McpConfig.ts +153 -0
- package/src/mcp/McpManager.ts +224 -0
- package/src/repl/PersistentShell.ts +24 -1
- package/src/repl/ReplManager.ts +1521 -1204
- package/src/tools/AskQuestionTool.ts +197 -0
- package/src/tools/EditFileTool.ts +172 -0
- package/src/tools/FileTools.ts +3 -0
- package/src/tools/PlanModeTool.ts +50 -0
- package/src/tools/WebSearchTool.ts +235 -63
- package/src/ui/DiffViewer.ts +117 -0
- package/src/ui/InputBox.ts +17 -2
- package/src/ui/MultiFileSelector.ts +135 -0
- package/src/ui/PlanModeUI.ts +121 -0
- package/src/ui/ToolExecutor.ts +182 -0
- package/src/ui/UIManager.ts +15 -2
- 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;
|
package/dist/tools/FileTools.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
26
|
-
|
|
34
|
+
// 1. Try API Key (Most Reliable)
|
|
35
|
+
if (process.env.TAVILY_API_KEY) {
|
|
27
36
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 (
|
|
41
|
-
|
|
52
|
+
catch (e) {
|
|
53
|
+
// Ignore npm error
|
|
42
54
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 `
|
|
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=([^&]+)&[^"]+">[^<]*<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(/&/g, '&')
|
|
217
|
+
.replace(/</g, '<')
|
|
218
|
+
.replace(/>/g, '>')
|
|
219
|
+
.replace(/"/g, '"')
|
|
220
|
+
.replace(/'/g, "'");
|
|
221
|
+
}
|
|
59
222
|
}
|
|
60
223
|
exports.WebSearchTool = WebSearchTool;
|