agent-sory 1.0.0
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/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/commands/base.d.ts +26 -0
- package/dist/commands/base.d.ts.map +1 -0
- package/dist/commands/base.js +3 -0
- package/dist/commands/base.js.map +1 -0
- package/dist/commands/definitions/clear.d.ts +3 -0
- package/dist/commands/definitions/clear.d.ts.map +1 -0
- package/dist/commands/definitions/clear.js +12 -0
- package/dist/commands/definitions/clear.js.map +1 -0
- package/dist/commands/definitions/help.d.ts +3 -0
- package/dist/commands/definitions/help.d.ts.map +1 -0
- package/dist/commands/definitions/help.js +28 -0
- package/dist/commands/definitions/help.js.map +1 -0
- package/dist/commands/definitions/init.d.ts +3 -0
- package/dist/commands/definitions/init.d.ts.map +1 -0
- package/dist/commands/definitions/init.js +23 -0
- package/dist/commands/definitions/init.js.map +1 -0
- package/dist/commands/definitions/login.d.ts +3 -0
- package/dist/commands/definitions/login.d.ts.map +1 -0
- package/dist/commands/definitions/login.js +8 -0
- package/dist/commands/definitions/login.js.map +1 -0
- package/dist/commands/definitions/model.d.ts +3 -0
- package/dist/commands/definitions/model.d.ts.map +1 -0
- package/dist/commands/definitions/model.js +10 -0
- package/dist/commands/definitions/model.js.map +1 -0
- package/dist/commands/definitions/reasoning.d.ts +3 -0
- package/dist/commands/definitions/reasoning.d.ts.map +1 -0
- package/dist/commands/definitions/reasoning.js +21 -0
- package/dist/commands/definitions/reasoning.js.map +1 -0
- package/dist/commands/definitions/stats.d.ts +3 -0
- package/dist/commands/definitions/stats.d.ts.map +1 -0
- package/dist/commands/definitions/stats.js +22 -0
- package/dist/commands/definitions/stats.js.map +1 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +38 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/core/agent.d.ts +41 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +266 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/cli.d.ts +3 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +48 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/llm/factory.d.ts +12 -0
- package/dist/core/llm/factory.d.ts.map +1 -0
- package/dist/core/llm/factory.js +44 -0
- package/dist/core/llm/factory.js.map +1 -0
- package/dist/core/llm/providers/gemini.d.ts +22 -0
- package/dist/core/llm/providers/gemini.d.ts.map +1 -0
- package/dist/core/llm/providers/gemini.js +167 -0
- package/dist/core/llm/providers/gemini.js.map +1 -0
- package/dist/core/llm/providers/groq.d.ts +23 -0
- package/dist/core/llm/providers/groq.d.ts.map +1 -0
- package/dist/core/llm/providers/groq.js +96 -0
- package/dist/core/llm/providers/groq.js.map +1 -0
- package/dist/core/llm/providers/openai-compatible.d.ts +24 -0
- package/dist/core/llm/providers/openai-compatible.d.ts.map +1 -0
- package/dist/core/llm/providers/openai-compatible.js +86 -0
- package/dist/core/llm/providers/openai-compatible.js.map +1 -0
- package/dist/core/llm/types.d.ts +37 -0
- package/dist/core/llm/types.d.ts.map +1 -0
- package/dist/core/llm/types.js +2 -0
- package/dist/core/llm/types.js.map +1 -0
- package/dist/core/prompts/system.d.ts +6 -0
- package/dist/core/prompts/system.d.ts.map +1 -0
- package/dist/core/prompts/system.js +36 -0
- package/dist/core/prompts/system.js.map +1 -0
- package/dist/tests/proxy-config.test.d.ts +2 -0
- package/dist/tests/proxy-config.test.d.ts.map +1 -0
- package/dist/tests/proxy-config.test.js +169 -0
- package/dist/tests/proxy-config.test.js.map +1 -0
- package/dist/tools/tool-schemas.d.ts +36 -0
- package/dist/tools/tool-schemas.d.ts.map +1 -0
- package/dist/tools/tool-schemas.js +482 -0
- package/dist/tools/tool-schemas.js.map +1 -0
- package/dist/tools/tools.d.ts +114 -0
- package/dist/tools/tools.d.ts.map +1 -0
- package/dist/tools/tools.js +1015 -0
- package/dist/tools/tools.js.map +1 -0
- package/dist/tools/utils/edit-logic.d.ts +9 -0
- package/dist/tools/utils/edit-logic.d.ts.map +1 -0
- package/dist/tools/utils/edit-logic.js +149 -0
- package/dist/tools/utils/edit-logic.js.map +1 -0
- package/dist/tools/utils/ripgrep-runner.d.ts +15 -0
- package/dist/tools/utils/ripgrep-runner.d.ts.map +1 -0
- package/dist/tools/utils/ripgrep-runner.js +90 -0
- package/dist/tools/utils/ripgrep-runner.js.map +1 -0
- package/dist/tools/validators.d.ts +4 -0
- package/dist/tools/validators.d.ts.map +1 -0
- package/dist/tools/validators.js +18 -0
- package/dist/tools/validators.js.map +1 -0
- package/dist/ui/App.d.ts +7 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +12 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/components/core/Chat.d.ts +7 -0
- package/dist/ui/components/core/Chat.d.ts.map +1 -0
- package/dist/ui/components/core/Chat.js +60 -0
- package/dist/ui/components/core/Chat.js.map +1 -0
- package/dist/ui/components/core/MessageHistory.d.ts +7 -0
- package/dist/ui/components/core/MessageHistory.d.ts.map +1 -0
- package/dist/ui/components/core/MessageHistory.js +39 -0
- package/dist/ui/components/core/MessageHistory.js.map +1 -0
- package/dist/ui/components/core/MessageInput.d.ts +10 -0
- package/dist/ui/components/core/MessageInput.d.ts.map +1 -0
- package/dist/ui/components/core/MessageInput.js +130 -0
- package/dist/ui/components/core/MessageInput.js.map +1 -0
- package/dist/ui/components/display/DiffPreview.d.ts +15 -0
- package/dist/ui/components/display/DiffPreview.d.ts.map +1 -0
- package/dist/ui/components/display/DiffPreview.js +298 -0
- package/dist/ui/components/display/DiffPreview.js.map +1 -0
- package/dist/ui/components/display/Stats.d.ts +19 -0
- package/dist/ui/components/display/Stats.d.ts.map +1 -0
- package/dist/ui/components/display/Stats.js +31 -0
- package/dist/ui/components/display/Stats.js.map +1 -0
- package/dist/ui/components/display/TokenMetrics.d.ts +11 -0
- package/dist/ui/components/display/TokenMetrics.d.ts.map +1 -0
- package/dist/ui/components/display/TokenMetrics.js +16 -0
- package/dist/ui/components/display/TokenMetrics.js.map +1 -0
- package/dist/ui/components/display/ToolHistoryItem.d.ts +7 -0
- package/dist/ui/components/display/ToolHistoryItem.d.ts.map +1 -0
- package/dist/ui/components/display/ToolHistoryItem.js +102 -0
- package/dist/ui/components/display/ToolHistoryItem.js.map +1 -0
- package/dist/ui/components/input-overlays/ErrorRetry.d.ts +8 -0
- package/dist/ui/components/input-overlays/ErrorRetry.d.ts.map +1 -0
- package/dist/ui/components/input-overlays/ErrorRetry.js +6 -0
- package/dist/ui/components/input-overlays/ErrorRetry.js.map +1 -0
- package/dist/ui/components/input-overlays/Login.d.ts +7 -0
- package/dist/ui/components/input-overlays/Login.d.ts.map +1 -0
- package/dist/ui/components/input-overlays/Login.js +32 -0
- package/dist/ui/components/input-overlays/Login.js.map +1 -0
- package/dist/ui/components/input-overlays/MaxIterationsContinue.d.ts +8 -0
- package/dist/ui/components/input-overlays/MaxIterationsContinue.d.ts.map +1 -0
- package/dist/ui/components/input-overlays/MaxIterationsContinue.js +32 -0
- package/dist/ui/components/input-overlays/MaxIterationsContinue.js.map +1 -0
- package/dist/ui/components/input-overlays/ModelSelector.d.ts +12 -0
- package/dist/ui/components/input-overlays/ModelSelector.d.ts.map +1 -0
- package/dist/ui/components/input-overlays/ModelSelector.js +114 -0
- package/dist/ui/components/input-overlays/ModelSelector.js.map +1 -0
- package/dist/ui/components/input-overlays/PendingToolApproval.d.ts +10 -0
- package/dist/ui/components/input-overlays/PendingToolApproval.d.ts.map +1 -0
- package/dist/ui/components/input-overlays/PendingToolApproval.js +51 -0
- package/dist/ui/components/input-overlays/PendingToolApproval.js.map +1 -0
- package/dist/ui/components/input-overlays/SlashCommandSuggestions.d.ts +8 -0
- package/dist/ui/components/input-overlays/SlashCommandSuggestions.d.ts.map +1 -0
- package/dist/ui/components/input-overlays/SlashCommandSuggestions.js +13 -0
- package/dist/ui/components/input-overlays/SlashCommandSuggestions.js.map +1 -0
- package/dist/ui/hooks/useAgent.d.ts +41 -0
- package/dist/ui/hooks/useAgent.d.ts.map +1 -0
- package/dist/ui/hooks/useAgent.js +205 -0
- package/dist/ui/hooks/useAgent.js.map +1 -0
- package/dist/ui/hooks/useMouseScroll.d.ts +3 -0
- package/dist/ui/hooks/useMouseScroll.d.ts.map +1 -0
- package/dist/ui/hooks/useMouseScroll.js +32 -0
- package/dist/ui/hooks/useMouseScroll.js.map +1 -0
- package/dist/ui/hooks/useSessionStats.d.ts +20 -0
- package/dist/ui/hooks/useSessionStats.d.ts.map +1 -0
- package/dist/ui/hooks/useSessionStats.js +36 -0
- package/dist/ui/hooks/useSessionStats.js.map +1 -0
- package/dist/ui/hooks/useTokenMetrics.d.ts +21 -0
- package/dist/ui/hooks/useTokenMetrics.d.ts.map +1 -0
- package/dist/ui/hooks/useTokenMetrics.js +102 -0
- package/dist/ui/hooks/useTokenMetrics.js.map +1 -0
- package/dist/ui/utils/CodeColorizer.d.ts +5 -0
- package/dist/ui/utils/CodeColorizer.d.ts.map +1 -0
- package/dist/ui/utils/CodeColorizer.js +47 -0
- package/dist/ui/utils/CodeColorizer.js.map +1 -0
- package/dist/utils/constants.d.ts +8 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +104 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/context.d.ts +36 -0
- package/dist/utils/context.d.ts.map +1 -0
- package/dist/utils/context.js +239 -0
- package/dist/utils/context.js.map +1 -0
- package/dist/utils/file-ops.d.ts +21 -0
- package/dist/utils/file-ops.d.ts.map +1 -0
- package/dist/utils/file-ops.js +125 -0
- package/dist/utils/file-ops.js.map +1 -0
- package/dist/utils/local-settings.d.ts +18 -0
- package/dist/utils/local-settings.d.ts.map +1 -0
- package/dist/utils/local-settings.js +176 -0
- package/dist/utils/local-settings.js.map +1 -0
- package/dist/utils/markdown.d.ts +13 -0
- package/dist/utils/markdown.d.ts.map +1 -0
- package/dist/utils/markdown.js +122 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/proxy-config.d.ts +25 -0
- package/dist/utils/proxy-config.d.ts.map +1 -0
- package/dist/utils/proxy-config.js +145 -0
- package/dist/utils/proxy-config.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { writeFile, createDirectory, displayTree } from '../utils/file-ops.js';
|
|
7
|
+
import { setReadFilesTracker } from './validators.js';
|
|
8
|
+
import { calculateReplacement } from './utils/edit-logic.js';
|
|
9
|
+
import { runRipGrep } from './utils/ripgrep-runner.js';
|
|
10
|
+
import { convert } from 'html-to-text';
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
// Global task state
|
|
13
|
+
let currentTaskList = null;
|
|
14
|
+
// Track which files have been read in the current session
|
|
15
|
+
const readFiles = new Set();
|
|
16
|
+
// Export readFiles for validator access
|
|
17
|
+
export function getReadFilesTracker() {
|
|
18
|
+
return readFiles;
|
|
19
|
+
}
|
|
20
|
+
// Initialize validator with readFiles tracker
|
|
21
|
+
setReadFilesTracker(readFiles);
|
|
22
|
+
/**
|
|
23
|
+
* Format key parameters for tool call display
|
|
24
|
+
*/
|
|
25
|
+
export function formatToolParams(toolName, toolArgs, options = {}) {
|
|
26
|
+
const { includePrefix = true, separator = '=' } = options;
|
|
27
|
+
const paramMappings = {
|
|
28
|
+
read_file: ['file_path'],
|
|
29
|
+
create_file: ['file_path'],
|
|
30
|
+
edit_file: ['file_path'],
|
|
31
|
+
delete_file: ['file_path'],
|
|
32
|
+
list_files: ['directory'],
|
|
33
|
+
search_files: ['pattern'],
|
|
34
|
+
execute_command: ['command'],
|
|
35
|
+
create_tasks: [],
|
|
36
|
+
update_tasks: [],
|
|
37
|
+
};
|
|
38
|
+
const keyParams = paramMappings[toolName] || [];
|
|
39
|
+
const parts = [];
|
|
40
|
+
// First, add mapped key params
|
|
41
|
+
keyParams.forEach(param => {
|
|
42
|
+
if (param in toolArgs) {
|
|
43
|
+
let value = toolArgs[param];
|
|
44
|
+
if (typeof value === 'string' && value.length > 50)
|
|
45
|
+
value = value.substring(0, 47) + '...';
|
|
46
|
+
parts.push(`${param}${separator}${JSON.stringify(value)}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// Then, add any OTHER params that weren't mapped (except big ones like 'content')
|
|
50
|
+
Object.keys(toolArgs).forEach(key => {
|
|
51
|
+
if (!keyParams.includes(key) && key !== 'content' && key !== 'old_text' && key !== 'new_text') {
|
|
52
|
+
let value = toolArgs[key];
|
|
53
|
+
if (typeof value === 'string' && value.length > 30)
|
|
54
|
+
value = value.substring(0, 27) + '...';
|
|
55
|
+
parts.push(`${key}${separator}${JSON.stringify(value)}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (parts.length === 0) {
|
|
59
|
+
// If we have arguments but nothing was formatted (e.g. only 'content'), show a summary
|
|
60
|
+
if (Object.keys(toolArgs).length > 0) {
|
|
61
|
+
return includePrefix ? `Args: ${Object.keys(toolArgs).join(', ')}` : Object.keys(toolArgs).join(', ');
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
const formatted = parts.join(', ');
|
|
66
|
+
return includePrefix ? `Params: ${formatted}` : formatted;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create a standardized tool response format
|
|
70
|
+
*/
|
|
71
|
+
export function createToolResponse(success, data, message = '', error = '') {
|
|
72
|
+
const response = { success };
|
|
73
|
+
if (success) {
|
|
74
|
+
if (data !== undefined) {
|
|
75
|
+
response.content = data;
|
|
76
|
+
}
|
|
77
|
+
if (message) {
|
|
78
|
+
response.message = message;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
response.error = error;
|
|
83
|
+
if (message) {
|
|
84
|
+
response.message = message;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return response;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read the contents of a file, optionally specifying line range
|
|
91
|
+
*/
|
|
92
|
+
export async function readFile(filePath, startLine, endLine) {
|
|
93
|
+
try {
|
|
94
|
+
const resolvedPath = path.resolve(filePath);
|
|
95
|
+
// Check if file exists
|
|
96
|
+
try {
|
|
97
|
+
await fs.promises.access(resolvedPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
const parentDir = path.dirname(filePath);
|
|
101
|
+
const suggestions = await displayTree(parentDir, '*', false, false);
|
|
102
|
+
return createToolResponse(false, undefined, '', `Error: File '${filePath}' not found. Directory '${parentDir}' contains:\n${suggestions}`);
|
|
103
|
+
}
|
|
104
|
+
const stats = await fs.promises.stat(resolvedPath);
|
|
105
|
+
if (!stats.isFile()) {
|
|
106
|
+
return createToolResponse(false, undefined, '', `Error: '${filePath}' is not a file. If it is a directory, use list_files.`);
|
|
107
|
+
}
|
|
108
|
+
// Check file size (50MB limit)
|
|
109
|
+
if (stats.size > 50 * 1024 * 1024) {
|
|
110
|
+
return createToolResponse(false, undefined, '', 'Error: File too large (max 50MB)');
|
|
111
|
+
}
|
|
112
|
+
const content = await fs.promises.readFile(resolvedPath, 'utf-8');
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
// Handle line range if specified
|
|
115
|
+
if (startLine !== undefined) {
|
|
116
|
+
const startIdx = Math.max(0, startLine - 1); // Convert to 0-indexed
|
|
117
|
+
let endIdx = lines.length;
|
|
118
|
+
if (endLine !== undefined) {
|
|
119
|
+
endIdx = Math.min(lines.length, endLine);
|
|
120
|
+
}
|
|
121
|
+
if (startIdx >= lines.length) {
|
|
122
|
+
return createToolResponse(false, undefined, '', 'Error: Start line exceeds file length');
|
|
123
|
+
}
|
|
124
|
+
const selectedLines = lines.slice(startIdx, endIdx);
|
|
125
|
+
const selectedContent = selectedLines.join('\n');
|
|
126
|
+
// Add file to read tracking for partial reads too
|
|
127
|
+
readFiles.add(resolvedPath);
|
|
128
|
+
const message = `Read lines ${startLine}-${endIdx} from ${filePath}`;
|
|
129
|
+
return createToolResponse(true, selectedContent, message);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Add file to read tracking
|
|
133
|
+
readFiles.add(resolvedPath);
|
|
134
|
+
const message = `Read ${lines.length} lines from ${filePath}`;
|
|
135
|
+
return createToolResponse(true, content, message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.code === 'ENOENT') {
|
|
140
|
+
return createToolResponse(false, undefined, '', 'Error: File not found');
|
|
141
|
+
}
|
|
142
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to read file');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read multiple files at once
|
|
147
|
+
*/
|
|
148
|
+
export async function readManyFiles(filePaths) {
|
|
149
|
+
try {
|
|
150
|
+
const results = await Promise.all(filePaths.map(async (filePath) => {
|
|
151
|
+
try {
|
|
152
|
+
const resolvedPath = path.resolve(filePath);
|
|
153
|
+
const content = await fs.promises.readFile(resolvedPath, 'utf-8');
|
|
154
|
+
readFiles.add(resolvedPath);
|
|
155
|
+
return { filePath, content, success: true };
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return { filePath, error: String(error), success: false };
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
const successfulCount = results.filter(r => r.success).length;
|
|
162
|
+
return createToolResponse(true, results, `Successfully read ${successfulCount}/${filePaths.length} files`);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return createToolResponse(false, undefined, '', `Error: Failed to read multiple files - ${error}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Create a new file or directory with specified content
|
|
170
|
+
*/
|
|
171
|
+
export async function createFile(filePath, content, fileType = 'file', overwrite = false) {
|
|
172
|
+
try {
|
|
173
|
+
const targetPath = path.resolve(filePath);
|
|
174
|
+
// Check if file exists and handle overwrite
|
|
175
|
+
const exists = await fs.promises.access(targetPath).then(() => true).catch(() => false);
|
|
176
|
+
if (exists && !overwrite) {
|
|
177
|
+
return createToolResponse(false, undefined, '', 'Error: File already exists, use overwrite=true');
|
|
178
|
+
}
|
|
179
|
+
if (fileType === 'directory') {
|
|
180
|
+
const result = await createDirectory(targetPath);
|
|
181
|
+
if (result) {
|
|
182
|
+
return createToolResponse(true, { path: targetPath, type: 'directory' }, `Directory created: ${filePath}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to create directory');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (fileType === 'file') {
|
|
189
|
+
const result = await writeFile(targetPath, content, overwrite, true);
|
|
190
|
+
if (result) {
|
|
191
|
+
return createToolResponse(true, undefined, `File created: ${filePath}`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to create file');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
return createToolResponse(false, undefined, '', "Error: Invalid file_type, must be 'file' or 'directory'");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to create file or directory');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Edit a file by replacing text strings using multiple strategies (exact, flexible, regex)
|
|
207
|
+
*/
|
|
208
|
+
export async function editFile(filePath, oldString, newString, expectedReplacements = 1) {
|
|
209
|
+
try {
|
|
210
|
+
const resolvedPath = path.resolve(filePath);
|
|
211
|
+
// Read current content
|
|
212
|
+
const originalContent = await fs.promises.readFile(resolvedPath, 'utf-8');
|
|
213
|
+
// Detect line endings
|
|
214
|
+
const isCRLF = originalContent.includes('\r\n');
|
|
215
|
+
const normalizedContent = originalContent.replace(/\r\n/g, '\n');
|
|
216
|
+
// Perform the replacement using advanced logic
|
|
217
|
+
const result = await calculateReplacement(normalizedContent, oldString, newString);
|
|
218
|
+
if (result.occurrences === 0) {
|
|
219
|
+
return createToolResponse(false, undefined, '', `Error: Could not find the string to replace in ${filePath}. Check whitespace and indentation.`);
|
|
220
|
+
}
|
|
221
|
+
if (expectedReplacements > 0 && result.occurrences !== expectedReplacements) {
|
|
222
|
+
return createToolResponse(false, undefined, '', `Error: Expected ${expectedReplacements} replacement(s) but found ${result.occurrences} in ${filePath}.`);
|
|
223
|
+
}
|
|
224
|
+
// Restore line endings if necessary
|
|
225
|
+
let finalContent = result.newContent;
|
|
226
|
+
if (isCRLF) {
|
|
227
|
+
finalContent = finalContent.replace(/\n/g, '\r\n');
|
|
228
|
+
}
|
|
229
|
+
// Write the updated content
|
|
230
|
+
const writeResult = await writeFile(filePath, finalContent, true, true);
|
|
231
|
+
if (writeResult) {
|
|
232
|
+
return createToolResponse(true, undefined, `Successfully modified ${filePath} (${result.occurrences} replacement(s)).`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to write changes to file');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
return createToolResponse(false, undefined, '', `Error: Failed to edit file - ${error}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Delete a file or directory with safety checks
|
|
244
|
+
*/
|
|
245
|
+
export async function deleteFile(filePath, recursive = false) {
|
|
246
|
+
try {
|
|
247
|
+
const targetPath = path.resolve(filePath);
|
|
248
|
+
const currentWorkingDir = path.resolve(process.cwd());
|
|
249
|
+
// Safety check 1: Never delete the root directory itself
|
|
250
|
+
if (targetPath === currentWorkingDir) {
|
|
251
|
+
return createToolResponse(false, undefined, '', 'Error: Cannot delete the root project directory');
|
|
252
|
+
}
|
|
253
|
+
// Safety check 2: Never delete anything outside the current working directory
|
|
254
|
+
if (!targetPath.startsWith(currentWorkingDir)) {
|
|
255
|
+
return createToolResponse(false, undefined, '', 'Error: Cannot delete files outside the project directory');
|
|
256
|
+
}
|
|
257
|
+
const exists = await fs.promises.access(targetPath).then(() => true).catch(() => false);
|
|
258
|
+
if (!exists) {
|
|
259
|
+
return createToolResponse(false, undefined, '', 'Error: Path not found');
|
|
260
|
+
}
|
|
261
|
+
const stats = await fs.promises.stat(targetPath);
|
|
262
|
+
if (stats.isDirectory() && !recursive) {
|
|
263
|
+
// Check if directory is empty
|
|
264
|
+
const items = await fs.promises.readdir(targetPath);
|
|
265
|
+
if (items.length > 0) {
|
|
266
|
+
return createToolResponse(false, undefined, '', 'Error: Directory not empty, use recursive=true');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Perform deletion
|
|
270
|
+
if (stats.isDirectory()) {
|
|
271
|
+
await fs.promises.rmdir(targetPath, { recursive });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
await fs.promises.unlink(targetPath);
|
|
275
|
+
}
|
|
276
|
+
const fileType = stats.isDirectory() ? 'directory' : 'file';
|
|
277
|
+
return createToolResponse(true, undefined, `Deleted ${fileType}: ${filePath}`);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to delete');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* List files and directories in a path with tree-style display
|
|
285
|
+
*/
|
|
286
|
+
export async function listFiles(directory = '.', pattern = '*', recursive = false, showHidden = false) {
|
|
287
|
+
try {
|
|
288
|
+
const dirPath = path.resolve(directory);
|
|
289
|
+
const exists = await fs.promises.access(dirPath).then(() => true).catch(() => false);
|
|
290
|
+
if (!exists) {
|
|
291
|
+
const parentDir = path.dirname(directory);
|
|
292
|
+
const suggestions = await displayTree(parentDir === '.' ? '.' : parentDir, '*', false, false);
|
|
293
|
+
return createToolResponse(false, undefined, '', `Error: Directory '${directory}' not found. Current location contains:\n${suggestions}`);
|
|
294
|
+
}
|
|
295
|
+
const stats = await fs.promises.stat(dirPath);
|
|
296
|
+
if (!stats.isDirectory()) {
|
|
297
|
+
return createToolResponse(false, undefined, '', `Error: '${directory}' is a file, not a directory. Use read_file instead.`);
|
|
298
|
+
}
|
|
299
|
+
// Get tree display output
|
|
300
|
+
const treeOutput = await displayTree(directory, pattern, recursive, showHidden);
|
|
301
|
+
return createToolResponse(true, treeOutput, `Listed ${directory}`);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return createToolResponse(false, undefined, '', `Failed to list files: ${error}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Search for text patterns in files with advanced filtering and matching options
|
|
309
|
+
*/
|
|
310
|
+
export async function searchFiles(pattern, filePattern = '*', directory = '.', caseSensitive = false, patternType = 'substring', fileTypes, excludeDirs, excludeFiles, maxResults = 100, contextLines = 0, groupByFile = false) {
|
|
311
|
+
try {
|
|
312
|
+
const searchDir = path.resolve(directory);
|
|
313
|
+
// Check if directory exists
|
|
314
|
+
const exists = await fs.promises.access(searchDir).then(() => true).catch(() => false);
|
|
315
|
+
if (!exists) {
|
|
316
|
+
return createToolResponse(false, undefined, '', 'Error: Directory not found');
|
|
317
|
+
}
|
|
318
|
+
const stats = await fs.promises.stat(searchDir);
|
|
319
|
+
if (!stats.isDirectory()) {
|
|
320
|
+
return createToolResponse(false, undefined, '', 'Error: Path is not a directory');
|
|
321
|
+
}
|
|
322
|
+
// Default exclusions
|
|
323
|
+
const defaultExcludeDirs = ['.git', 'node_modules', '.next', 'dist', 'build', '.cache'];
|
|
324
|
+
const defaultExcludeFiles = ['*.log', '*.tmp', '*.cache', '*.lock'];
|
|
325
|
+
const finalExcludeDirs = [...defaultExcludeDirs, ...(excludeDirs || [])];
|
|
326
|
+
const finalExcludeFiles = [...defaultExcludeFiles, ...(excludeFiles || [])];
|
|
327
|
+
// Prepare search regex
|
|
328
|
+
let searchRegex;
|
|
329
|
+
try {
|
|
330
|
+
switch (patternType) {
|
|
331
|
+
case 'exact':
|
|
332
|
+
searchRegex = new RegExp(escapeRegex(pattern), caseSensitive ? 'g' : 'gi');
|
|
333
|
+
break;
|
|
334
|
+
case 'regex':
|
|
335
|
+
searchRegex = new RegExp(pattern, caseSensitive ? 'g' : 'gi');
|
|
336
|
+
break;
|
|
337
|
+
case 'fuzzy':
|
|
338
|
+
// Simple fuzzy search, insert .* between characters
|
|
339
|
+
const fuzzyPattern = pattern.split('').map(escapeRegex).join('.*');
|
|
340
|
+
searchRegex = new RegExp(fuzzyPattern, caseSensitive ? 'g' : 'gi');
|
|
341
|
+
break;
|
|
342
|
+
case 'substring':
|
|
343
|
+
default:
|
|
344
|
+
searchRegex = new RegExp(escapeRegex(pattern), caseSensitive ? 'g' : 'gi');
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
return createToolResponse(false, undefined, '', 'Error: Invalid regex pattern');
|
|
350
|
+
}
|
|
351
|
+
// Collect all files to search
|
|
352
|
+
const filesToSearch = await collectFiles(searchDir, filePattern, fileTypes, finalExcludeDirs, finalExcludeFiles);
|
|
353
|
+
if (filesToSearch.length === 0) {
|
|
354
|
+
return createToolResponse(true, [], 'No files found matching criteria');
|
|
355
|
+
}
|
|
356
|
+
// Search through files
|
|
357
|
+
const results = [];
|
|
358
|
+
let totalMatches = 0;
|
|
359
|
+
for (const filePath of filesToSearch) {
|
|
360
|
+
if (totalMatches >= maxResults) {
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
365
|
+
const lines = content.split('\n');
|
|
366
|
+
const fileMatches = [];
|
|
367
|
+
for (let i = 0; i < lines.length && totalMatches < maxResults; i++) {
|
|
368
|
+
const line = lines[i];
|
|
369
|
+
const matches = Array.from(line.matchAll(searchRegex));
|
|
370
|
+
if (matches.length > 0) {
|
|
371
|
+
const contextStart = Math.max(0, i - contextLines);
|
|
372
|
+
const contextEnd = Math.min(lines.length - 1, i + contextLines);
|
|
373
|
+
const contextLinesArray = [];
|
|
374
|
+
for (let j = contextStart; j <= contextEnd; j++) {
|
|
375
|
+
contextLinesArray.push(lines[j]);
|
|
376
|
+
}
|
|
377
|
+
fileMatches.push({
|
|
378
|
+
lineNumber: i + 1,
|
|
379
|
+
lineContent: line,
|
|
380
|
+
contextLines: contextLines > 0 ? contextLinesArray : undefined,
|
|
381
|
+
matchPositions: matches.map(match => ({
|
|
382
|
+
start: match.index || 0,
|
|
383
|
+
end: (match.index || 0) + match[0].length,
|
|
384
|
+
text: match[0]
|
|
385
|
+
}))
|
|
386
|
+
});
|
|
387
|
+
totalMatches++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (fileMatches.length > 0) {
|
|
391
|
+
results.push({
|
|
392
|
+
filePath: path.relative(process.cwd(), filePath),
|
|
393
|
+
matches: fileMatches,
|
|
394
|
+
totalMatches: fileMatches.length
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
// Skip files that can't be read (binary files, permission issues, etc.)
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Format results
|
|
404
|
+
let formattedResults;
|
|
405
|
+
if (groupByFile) {
|
|
406
|
+
formattedResults = results;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
// Flatten results
|
|
410
|
+
formattedResults = results.flatMap(fileResult => fileResult.matches.map(match => ({
|
|
411
|
+
filePath: fileResult.filePath,
|
|
412
|
+
lineNumber: match.lineNumber,
|
|
413
|
+
lineContent: match.lineContent,
|
|
414
|
+
contextLines: match.contextLines,
|
|
415
|
+
matchPositions: match.matchPositions
|
|
416
|
+
})));
|
|
417
|
+
}
|
|
418
|
+
const message = `Found ${totalMatches} match(es) in ${results.length} file(s)`;
|
|
419
|
+
return createToolResponse(true, formattedResults, message);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to search files');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Helper function to escape regex special characters
|
|
426
|
+
function escapeRegex(string) {
|
|
427
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
428
|
+
}
|
|
429
|
+
// Helper function to collect files based on patterns and filters
|
|
430
|
+
async function collectFiles(directory, filePattern, fileTypes, excludeDirs, excludeFiles) {
|
|
431
|
+
const files = [];
|
|
432
|
+
async function walkDirectory(dir) {
|
|
433
|
+
try {
|
|
434
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
435
|
+
for (const entry of entries) {
|
|
436
|
+
const fullPath = path.join(dir, entry.name);
|
|
437
|
+
if (entry.isDirectory()) {
|
|
438
|
+
// Check if directory should be excluded
|
|
439
|
+
if (excludeDirs && excludeDirs.some(pattern => matchesPattern(entry.name, pattern))) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// Skip hidden directories unless explicitly included
|
|
443
|
+
if (entry.name.startsWith('.') && !entry.name.match(/^\.(config|env)$/)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
await walkDirectory(fullPath);
|
|
447
|
+
}
|
|
448
|
+
else if (entry.isFile()) {
|
|
449
|
+
// Check file type filters
|
|
450
|
+
if (fileTypes && fileTypes.length > 0) {
|
|
451
|
+
const ext = path.extname(entry.name).slice(1);
|
|
452
|
+
if (!fileTypes.includes(ext)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Check file pattern
|
|
457
|
+
if (!matchesPattern(entry.name, filePattern)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
// Check exclusions
|
|
461
|
+
if (excludeFiles && excludeFiles.some(pattern => matchesPattern(entry.name, pattern))) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
// Skip obviously binary files
|
|
465
|
+
if (isBinaryFile(entry.name)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
files.push(fullPath);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
// Skip directories we can't read
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
await walkDirectory(directory);
|
|
477
|
+
return files;
|
|
478
|
+
}
|
|
479
|
+
// Helper function to match glob-like patterns
|
|
480
|
+
function matchesPattern(filename, pattern) {
|
|
481
|
+
if (pattern === '*')
|
|
482
|
+
return true;
|
|
483
|
+
// Simple glob matching, convert * to .* and ? to .
|
|
484
|
+
const regexPattern = pattern
|
|
485
|
+
.replace(/\./g, '\\.')
|
|
486
|
+
.replace(/\*/g, '.*')
|
|
487
|
+
.replace(/\?/g, '.');
|
|
488
|
+
return new RegExp(`^${regexPattern}$`, 'i').test(filename);
|
|
489
|
+
}
|
|
490
|
+
// Helper function to detect binary files
|
|
491
|
+
function isBinaryFile(filename) {
|
|
492
|
+
const binaryExtensions = [
|
|
493
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.obj', '.o', '.a', '.lib',
|
|
494
|
+
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg', '.webp',
|
|
495
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm',
|
|
496
|
+
'.zip', '.tar', '.gz', '.bz2', '.rar', '.7z',
|
|
497
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'
|
|
498
|
+
];
|
|
499
|
+
const ext = path.extname(filename).toLowerCase();
|
|
500
|
+
return binaryExtensions.includes(ext);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Execute a shell command or run code
|
|
504
|
+
*/
|
|
505
|
+
export async function executeCommand(command, commandType, workingDirectory, timeout = 30000) {
|
|
506
|
+
try {
|
|
507
|
+
// Validate command type
|
|
508
|
+
if (!['bash', 'python', 'setup', 'run'].includes(commandType)) {
|
|
509
|
+
return createToolResponse(false, undefined, '', 'Error: Invalid command_type');
|
|
510
|
+
}
|
|
511
|
+
let originalCwd;
|
|
512
|
+
if (workingDirectory) {
|
|
513
|
+
const wdPath = path.resolve(workingDirectory);
|
|
514
|
+
const exists = await fs.promises.access(wdPath).then(() => true).catch(() => false);
|
|
515
|
+
if (!exists) {
|
|
516
|
+
return createToolResponse(false, undefined, '', 'Error: Working directory not found');
|
|
517
|
+
}
|
|
518
|
+
originalCwd = process.cwd();
|
|
519
|
+
process.chdir(workingDirectory);
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
let execCommand;
|
|
523
|
+
if (commandType === 'python') {
|
|
524
|
+
execCommand = `python -c "${command.replace(/"/g, '\\"')}"`;
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
execCommand = command;
|
|
528
|
+
}
|
|
529
|
+
const { stdout, stderr } = await execAsync(execCommand, { timeout });
|
|
530
|
+
const success = true; // If no error was thrown, consider it successful
|
|
531
|
+
return createToolResponse(success, `stdout: ${stdout}\nstderr: ${stderr}`, `Command executed successfully`);
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
// Restore original working directory
|
|
535
|
+
if (originalCwd) {
|
|
536
|
+
process.chdir(originalCwd);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
const isTimeout = error.killed && error.signal === 'SIGTERM';
|
|
542
|
+
if (isTimeout) {
|
|
543
|
+
return createToolResponse(false, undefined, '', 'Error: Command timed out');
|
|
544
|
+
}
|
|
545
|
+
return createToolResponse(false, undefined, '', 'Error: Failed to execute command');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Create a task list of subtasks to complete the user's request
|
|
550
|
+
*/
|
|
551
|
+
export async function createTasks(userQuery, tasks) {
|
|
552
|
+
try {
|
|
553
|
+
// Validate task structure
|
|
554
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
555
|
+
const task = tasks[i];
|
|
556
|
+
if (!task.id || !task.description) {
|
|
557
|
+
return createToolResponse(false, undefined, '', `Error: Task ${i} missing required fields (id, description)`);
|
|
558
|
+
}
|
|
559
|
+
// Set default status if not provided
|
|
560
|
+
if (!task.status) {
|
|
561
|
+
task.status = 'pending';
|
|
562
|
+
}
|
|
563
|
+
// Validate status
|
|
564
|
+
if (!['pending', 'in_progress', 'completed'].includes(task.status)) {
|
|
565
|
+
return createToolResponse(false, undefined, '', `Error: Invalid status '${task.status}' for task ${task.id}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Store the task list globally
|
|
569
|
+
currentTaskList = {
|
|
570
|
+
user_query: userQuery,
|
|
571
|
+
tasks: tasks,
|
|
572
|
+
created_at: new Date().toISOString()
|
|
573
|
+
};
|
|
574
|
+
// Return a deep copy to prevent mutation of historical displays
|
|
575
|
+
const snapshot = {
|
|
576
|
+
user_query: currentTaskList.user_query,
|
|
577
|
+
tasks: currentTaskList.tasks.map(task => ({ ...task })),
|
|
578
|
+
created_at: currentTaskList.created_at
|
|
579
|
+
};
|
|
580
|
+
return createToolResponse(true, snapshot, `Created task list with ${tasks.length} tasks for: ${userQuery}`);
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
return createToolResponse(false, undefined, '', `Error: Failed to create tasks - ${error}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Update the status of one or more tasks in the task list
|
|
588
|
+
*/
|
|
589
|
+
export async function updateTasks(taskUpdates) {
|
|
590
|
+
try {
|
|
591
|
+
if (!currentTaskList) {
|
|
592
|
+
return createToolResponse(false, undefined, '', 'Error: No task list exists. Create tasks first.');
|
|
593
|
+
}
|
|
594
|
+
// Track updates made
|
|
595
|
+
const updatesMade = [];
|
|
596
|
+
for (const update of taskUpdates) {
|
|
597
|
+
if (!update.id || !update.status) {
|
|
598
|
+
return createToolResponse(false, undefined, '', 'Error: Task update missing required fields (id, status)');
|
|
599
|
+
}
|
|
600
|
+
// Validate status
|
|
601
|
+
if (!['pending', 'in_progress', 'completed'].includes(update.status)) {
|
|
602
|
+
return createToolResponse(false, undefined, '', `Error: Invalid status '${update.status}'`);
|
|
603
|
+
}
|
|
604
|
+
// Find and update the task
|
|
605
|
+
let taskFound = false;
|
|
606
|
+
for (const task of currentTaskList.tasks) {
|
|
607
|
+
if (task.id === update.id) {
|
|
608
|
+
const oldStatus = task.status;
|
|
609
|
+
task.status = update.status;
|
|
610
|
+
// Add notes if provided
|
|
611
|
+
if (update.notes) {
|
|
612
|
+
task.notes = update.notes;
|
|
613
|
+
}
|
|
614
|
+
// Add update timestamp
|
|
615
|
+
task.updated_at = new Date().toISOString();
|
|
616
|
+
updatesMade.push({
|
|
617
|
+
id: update.id,
|
|
618
|
+
description: task.description,
|
|
619
|
+
old_status: oldStatus,
|
|
620
|
+
new_status: update.status
|
|
621
|
+
});
|
|
622
|
+
taskFound = true;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (!taskFound) {
|
|
627
|
+
return createToolResponse(false, undefined, '', `Error: Task '${update.id}' not found`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Return a deep copy to prevent mutation of historical displays
|
|
631
|
+
const snapshot = {
|
|
632
|
+
user_query: currentTaskList.user_query,
|
|
633
|
+
tasks: currentTaskList.tasks.map(task => ({ ...task })),
|
|
634
|
+
created_at: currentTaskList.created_at
|
|
635
|
+
};
|
|
636
|
+
return createToolResponse(true, snapshot, `Updated ${updatesMade.length} task(s)`);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
return createToolResponse(false, undefined, '', `Error: Failed to update tasks - ${error}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Find files matching a glob pattern
|
|
644
|
+
*/
|
|
645
|
+
export async function glob(pattern) {
|
|
646
|
+
try {
|
|
647
|
+
const files = await fg(pattern, {
|
|
648
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
|
649
|
+
dot: true
|
|
650
|
+
});
|
|
651
|
+
return createToolResponse(true, files, `Found ${files.length} file(s) matching ${pattern}`);
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
return createToolResponse(false, undefined, '', `Glob error: ${error}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Search text in files using ripgrep
|
|
659
|
+
*/
|
|
660
|
+
export async function grep(pattern, include, context = 2, caseSensitive = false, directory = '.', fixedStrings = false) {
|
|
661
|
+
try {
|
|
662
|
+
const matches = await runRipGrep({
|
|
663
|
+
pattern,
|
|
664
|
+
directory,
|
|
665
|
+
include,
|
|
666
|
+
caseSensitive,
|
|
667
|
+
fixedStrings,
|
|
668
|
+
context
|
|
669
|
+
});
|
|
670
|
+
if (matches.length === 0) {
|
|
671
|
+
return createToolResponse(true, [], `No matches found for "${pattern}"`);
|
|
672
|
+
}
|
|
673
|
+
const wasTruncated = matches.length >= 1000;
|
|
674
|
+
const results = wasTruncated ? matches.slice(0, 1000) : matches;
|
|
675
|
+
const message = `Found ${matches.length} match(es)${wasTruncated ? ' (truncated to 1000)' : ''}`;
|
|
676
|
+
return createToolResponse(true, results, message);
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
return createToolResponse(false, undefined, '', `Grep error: ${error}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Save a fact to the project's long-term memory (SORY.md)
|
|
684
|
+
*/
|
|
685
|
+
export async function saveMemory(fact) {
|
|
686
|
+
try {
|
|
687
|
+
const memoryFilePath = path.join(process.cwd(), 'SORY.md');
|
|
688
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
689
|
+
const entry = `\n- [${timestamp}] ${fact}`;
|
|
690
|
+
let content = '';
|
|
691
|
+
if (fs.existsSync(memoryFilePath)) {
|
|
692
|
+
content = await fs.promises.readFile(memoryFilePath, 'utf-8');
|
|
693
|
+
}
|
|
694
|
+
if (!content.includes('# SORY MEMORY')) {
|
|
695
|
+
content = `# SORY MEMORY\nCette liste contient des faits importants mémorisés par l'IA pour ce projet.\n${content}`;
|
|
696
|
+
}
|
|
697
|
+
await fs.promises.writeFile(memoryFilePath, content + entry);
|
|
698
|
+
return createToolResponse(true, undefined, `J'ai mémorisé : "${fact}" dans SORY.md`);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
return createToolResponse(false, undefined, '', `Erreur lors de la mémorisation : ${error}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Search Wikipedia (Free API)
|
|
706
|
+
*/
|
|
707
|
+
async function searchWikipedia(query) {
|
|
708
|
+
try {
|
|
709
|
+
const url = `https://fr.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&origin=*`;
|
|
710
|
+
const response = await fetch(url);
|
|
711
|
+
const data = await response.json();
|
|
712
|
+
return (data.query?.search || []).map((res) => ({
|
|
713
|
+
title: `[Wikipedia] ${res.title}`,
|
|
714
|
+
url: `https://fr.wikipedia.org/wiki/${encodeURIComponent(res.title)}`,
|
|
715
|
+
description: res.snippet.replace(/<[^>]*>/g, ''),
|
|
716
|
+
source: 'wikipedia'
|
|
717
|
+
}));
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Search NPM Packages (Public API)
|
|
725
|
+
*/
|
|
726
|
+
async function searchNPM(query) {
|
|
727
|
+
try {
|
|
728
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=5`;
|
|
729
|
+
const response = await fetch(url);
|
|
730
|
+
const data = await response.json();
|
|
731
|
+
return (data.objects || []).map((res) => ({
|
|
732
|
+
title: `[NPM Package] ${res.package.name} (v${res.package.version})`,
|
|
733
|
+
url: `https://www.npmjs.com/package/${res.package.name}`,
|
|
734
|
+
description: res.package.description,
|
|
735
|
+
source: 'npm'
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Search PyPI Packages (Python)
|
|
744
|
+
*/
|
|
745
|
+
async function searchPyPI(query) {
|
|
746
|
+
try {
|
|
747
|
+
const url = `https://pypi.org/pypi/${encodeURIComponent(query)}/json`;
|
|
748
|
+
const response = await fetch(url);
|
|
749
|
+
if (!response.ok)
|
|
750
|
+
return [];
|
|
751
|
+
const data = await response.json();
|
|
752
|
+
return [{
|
|
753
|
+
title: `[PyPI Package] ${data.info.name} (v${data.info.version})`,
|
|
754
|
+
url: data.info.package_url,
|
|
755
|
+
description: data.info.summary,
|
|
756
|
+
source: 'pypi'
|
|
757
|
+
}];
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Search GitHub with high inclusivity and version detection
|
|
765
|
+
*/
|
|
766
|
+
/**
|
|
767
|
+
* Search GitHub with high inclusivity and version detection
|
|
768
|
+
*/
|
|
769
|
+
async function searchGitHub(query) {
|
|
770
|
+
const results = [];
|
|
771
|
+
try {
|
|
772
|
+
// Nettoyage léger pour ne pas casser la recherche
|
|
773
|
+
const q = query.replace(/cli|version|dernier|latest/gi, '').trim();
|
|
774
|
+
// Recherche de dépôts par nom, description ou readme
|
|
775
|
+
const url = `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}+in:name,description&sort=stars&order=desc&per_page=10`;
|
|
776
|
+
const response = await fetch(url, {
|
|
777
|
+
headers: {
|
|
778
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
779
|
+
'User-Agent': 'Mozilla/5.0 (Sory-CLI)'
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
if (!response.ok)
|
|
783
|
+
return [];
|
|
784
|
+
const data = await response.json();
|
|
785
|
+
if (data.items) {
|
|
786
|
+
for (const repo of data.items) {
|
|
787
|
+
results.push({
|
|
788
|
+
title: `[GitHub Repo] ${repo.full_name}`,
|
|
789
|
+
url: repo.html_url,
|
|
790
|
+
description: repo.description || "Pas de description disponible.",
|
|
791
|
+
source: 'github'
|
|
792
|
+
});
|
|
793
|
+
// Tentative de récupération de la version si mentionnée
|
|
794
|
+
if (query.toLowerCase().match(/version|dernier|latest/)) {
|
|
795
|
+
try {
|
|
796
|
+
const relUrl = `https://api.github.com/repos/${repo.full_name}/releases/latest`;
|
|
797
|
+
const relRes = await fetch(relUrl, { headers: { 'User-Agent': 'Sory-CLI' } });
|
|
798
|
+
if (relRes.ok) {
|
|
799
|
+
const relData = await relRes.json();
|
|
800
|
+
results.push({
|
|
801
|
+
title: `[GitHub Release] ${repo.full_name} ${relData.tag_name}`,
|
|
802
|
+
url: relData.html_url,
|
|
803
|
+
description: `Version : ${relData.name || relData.tag_name}. Publiée le ${new Date(relData.published_at).toLocaleDateString('fr-FR')}`,
|
|
804
|
+
source: 'github'
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch { }
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch { /* ignore error */ }
|
|
814
|
+
return results;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Search the web using multiple sources with technical prioritization and manual override
|
|
818
|
+
*/
|
|
819
|
+
export async function googleWebSearch(query, source = 'auto') {
|
|
820
|
+
try {
|
|
821
|
+
const isTechnical = /code|api|repo|git|version|framework|lib|package|software|install|dernier|cli|opencode/i.test(query);
|
|
822
|
+
let github = [], npm = [], pypi = [], wiki = [], ddg = [];
|
|
823
|
+
// Lancer toutes les sources spécialisées + web en parallèle
|
|
824
|
+
const tasks = [];
|
|
825
|
+
if (source === 'github' || source === 'auto')
|
|
826
|
+
tasks.push(searchGitHub(query).then(res => github = res));
|
|
827
|
+
if (source === 'npm' || source === 'auto')
|
|
828
|
+
tasks.push(searchNPM(query).then(res => npm = res));
|
|
829
|
+
if (source === 'pypi' || source === 'auto')
|
|
830
|
+
tasks.push(searchPyPI(query).then(res => pypi = res));
|
|
831
|
+
if (source === 'wikipedia' || source === 'auto')
|
|
832
|
+
tasks.push(searchWikipedia(query).then(res => wiki = res));
|
|
833
|
+
if (source === 'web' || source === 'auto')
|
|
834
|
+
tasks.push(fetchDuckDuckGo(query, isTechnical).then(res => ddg = res));
|
|
835
|
+
await Promise.all(tasks);
|
|
836
|
+
// Fusion intelligente : Priorité absolue aux paquets et repos
|
|
837
|
+
let combined = [...npm, ...github, ...pypi, ...ddg, ...wiki];
|
|
838
|
+
// Supprimer les doublons et filtrage drastique du bruit
|
|
839
|
+
const seenUrls = new Set();
|
|
840
|
+
combined = combined.filter(res => {
|
|
841
|
+
if (!res.url || seenUrls.has(res.url))
|
|
842
|
+
return false;
|
|
843
|
+
seenUrls.add(res.url);
|
|
844
|
+
// Si c'est une recherche technique et qu'on a des résultats tech, on vire le Wikipedia générique
|
|
845
|
+
if (res.source === 'wikipedia' && isTechnical && (github.length > 0 || npm.length > 0 || ddg.some(d => d.url.includes('github') || d.url.includes('npm')))) {
|
|
846
|
+
const queryLower = query.toLowerCase();
|
|
847
|
+
const firstWord = queryLower.split(' ')[0];
|
|
848
|
+
// On ne garde Wikipedia que si le titre correspond vraiment à l'outil
|
|
849
|
+
if (!res.title.toLowerCase().includes(firstWord))
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
return true;
|
|
853
|
+
});
|
|
854
|
+
if (combined.length === 0) {
|
|
855
|
+
return createToolResponse(true, [], `Désolé, aucune information trouvée pour "${query}".`);
|
|
856
|
+
}
|
|
857
|
+
const sourceMsg = source === 'auto' ? "Analyse multi-sources" : `Recherche ciblée ${source.toUpperCase()}`;
|
|
858
|
+
return createToolResponse(true, combined.slice(0, 10), `${sourceMsg} pour "${query}"`);
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
return createToolResponse(false, undefined, '', `Erreur de recherche technique : ${error}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Enhanced DuckDuckGo Scraper with site-specific capabilities
|
|
866
|
+
*/
|
|
867
|
+
async function fetchDuckDuckGo(query, prioritizeTech) {
|
|
868
|
+
try {
|
|
869
|
+
// Si c'est technique, on ajoute des sites de confiance à la recherche
|
|
870
|
+
const enhancedQuery = prioritizeTech
|
|
871
|
+
? `${query} (site:github.com OR site:npmjs.com OR site:pypi.org OR site:stackoverflow.com)`
|
|
872
|
+
: query;
|
|
873
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(enhancedQuery)}`;
|
|
874
|
+
const response = await fetch(url, {
|
|
875
|
+
headers: {
|
|
876
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
877
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
if (!response.ok)
|
|
881
|
+
return [];
|
|
882
|
+
const html = await response.text();
|
|
883
|
+
const results = [];
|
|
884
|
+
const resultRegex = /<a class="result__a" href="([^"]+)">([^<]+)<\/a>/g;
|
|
885
|
+
let match;
|
|
886
|
+
while ((match = resultRegex.exec(html)) !== null && results.length < 8) {
|
|
887
|
+
let link = match[1];
|
|
888
|
+
if (link.includes('uddg=')) {
|
|
889
|
+
const parts = link.split('uddg=');
|
|
890
|
+
if (parts[1])
|
|
891
|
+
link = decodeURIComponent(parts[1].split('&')[0]);
|
|
892
|
+
}
|
|
893
|
+
if (!link.includes('w3.org') && !link.includes('dtd')) {
|
|
894
|
+
results.push({
|
|
895
|
+
title: match[2].trim(),
|
|
896
|
+
url: link,
|
|
897
|
+
source: 'web'
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return results;
|
|
902
|
+
}
|
|
903
|
+
catch {
|
|
904
|
+
return [];
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Fetch and extract text from a website
|
|
909
|
+
*/
|
|
910
|
+
export async function webFetch(url) {
|
|
911
|
+
try {
|
|
912
|
+
const response = await fetch(url, {
|
|
913
|
+
headers: {
|
|
914
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
915
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
|
916
|
+
},
|
|
917
|
+
redirect: 'follow'
|
|
918
|
+
});
|
|
919
|
+
if (!response.ok)
|
|
920
|
+
throw new Error(`HTTP ${response.status}`);
|
|
921
|
+
const html = await response.text();
|
|
922
|
+
const text = convert(html, {
|
|
923
|
+
wordwrap: 130,
|
|
924
|
+
selectors: [
|
|
925
|
+
{ selector: 'a', options: { ignoreHref: true } },
|
|
926
|
+
{ selector: 'img', format: 'skip' },
|
|
927
|
+
{ selector: 'nav', format: 'skip' },
|
|
928
|
+
{ selector: 'footer', format: 'skip' },
|
|
929
|
+
{ selector: 'script', format: 'skip' },
|
|
930
|
+
{ selector: 'style', format: 'skip' }
|
|
931
|
+
]
|
|
932
|
+
});
|
|
933
|
+
const cleanText = text.substring(0, 15000); // Augmentation de la limite à 15k
|
|
934
|
+
return createToolResponse(true, cleanText, `Contenu extrait de ${url}`);
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
return createToolResponse(false, undefined, '', `Erreur de lecture web : ${error}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Tool Registry: maps tool names to functions
|
|
941
|
+
export const TOOL_REGISTRY = {
|
|
942
|
+
read_file: readFile,
|
|
943
|
+
create_file: createFile,
|
|
944
|
+
edit_file: editFile,
|
|
945
|
+
delete_file: deleteFile,
|
|
946
|
+
list_files: listFiles,
|
|
947
|
+
search_files: searchFiles,
|
|
948
|
+
execute_command: executeCommand,
|
|
949
|
+
create_tasks: createTasks,
|
|
950
|
+
update_tasks: updateTasks,
|
|
951
|
+
grep: grep,
|
|
952
|
+
glob: glob,
|
|
953
|
+
read_many_files: readManyFiles,
|
|
954
|
+
save_memory: saveMemory,
|
|
955
|
+
google_web_search: googleWebSearch,
|
|
956
|
+
web_fetch: webFetch,
|
|
957
|
+
};
|
|
958
|
+
/**
|
|
959
|
+
* Execute a tool by name with given arguments
|
|
960
|
+
*/
|
|
961
|
+
export async function executeTool(toolName, toolArgs) {
|
|
962
|
+
if (!(toolName in TOOL_REGISTRY)) {
|
|
963
|
+
return createToolResponse(false, undefined, '', 'Error: Unknown tool');
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
const toolFunction = TOOL_REGISTRY[toolName];
|
|
967
|
+
// Call the function with the appropriate arguments based on the tool
|
|
968
|
+
switch (toolName) {
|
|
969
|
+
case 'read_file':
|
|
970
|
+
return await toolFunction(toolArgs.file_path, toolArgs.start_line, toolArgs.end_line);
|
|
971
|
+
case 'read_many_files':
|
|
972
|
+
return await toolFunction(toolArgs.file_paths);
|
|
973
|
+
case 'create_file':
|
|
974
|
+
return await toolFunction(toolArgs.file_path, toolArgs.content, toolArgs.file_type, toolArgs.overwrite);
|
|
975
|
+
case 'edit_file':
|
|
976
|
+
const oldText = toolArgs.old_string || toolArgs.old_text;
|
|
977
|
+
const newText = toolArgs.new_string || toolArgs.new_text;
|
|
978
|
+
const expected = toolArgs.expected_replacements !== undefined
|
|
979
|
+
? toolArgs.expected_replacements
|
|
980
|
+
: (toolArgs.replace_all ? 0 : 1);
|
|
981
|
+
return await toolFunction(toolArgs.file_path, oldText, newText, expected);
|
|
982
|
+
case 'delete_file':
|
|
983
|
+
return await toolFunction(toolArgs.file_path, toolArgs.recursive);
|
|
984
|
+
case 'list_files':
|
|
985
|
+
return await toolFunction(toolArgs.directory, toolArgs.pattern, toolArgs.recursive, toolArgs.show_hidden);
|
|
986
|
+
case 'search_files':
|
|
987
|
+
return await toolFunction(toolArgs.pattern, toolArgs.file_pattern, toolArgs.directory, toolArgs.case_sensitive, toolArgs.pattern_type, toolArgs.file_types, toolArgs.exclude_dirs, toolArgs.exclude_files, toolArgs.max_results, toolArgs.context_lines, toolArgs.group_by_file);
|
|
988
|
+
case 'execute_command':
|
|
989
|
+
return await toolFunction(toolArgs.command, toolArgs.command_type, toolArgs.working_directory, toolArgs.timeout);
|
|
990
|
+
case 'create_tasks':
|
|
991
|
+
return await toolFunction(toolArgs.user_query, toolArgs.tasks);
|
|
992
|
+
case 'update_tasks':
|
|
993
|
+
return await toolFunction(toolArgs.task_updates);
|
|
994
|
+
case 'grep':
|
|
995
|
+
return await toolFunction(toolArgs.pattern, toolArgs.include, toolArgs.context, toolArgs.case_sensitive, toolArgs.dir_path || toolArgs.directory || '.', toolArgs.fixed_strings);
|
|
996
|
+
case 'glob':
|
|
997
|
+
return await toolFunction(toolArgs.pattern);
|
|
998
|
+
case 'save_memory':
|
|
999
|
+
return await toolFunction(toolArgs.fact);
|
|
1000
|
+
case 'google_web_search':
|
|
1001
|
+
return await toolFunction(toolArgs.query, toolArgs.source);
|
|
1002
|
+
case 'web_fetch':
|
|
1003
|
+
return await toolFunction(toolArgs.url);
|
|
1004
|
+
default:
|
|
1005
|
+
return createToolResponse(false, undefined, '', 'Error: Tool not implemented');
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
if (error instanceof TypeError) {
|
|
1010
|
+
return createToolResponse(false, undefined, '', 'Error: Invalid tool arguments');
|
|
1011
|
+
}
|
|
1012
|
+
return createToolResponse(false, undefined, '', 'Error: Unexpected tool error');
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
//# sourceMappingURL=tools.js.map
|