@kirosnn/mosaic 0.0.91 → 0.73.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 +1 -1
- package/README.md +2 -6
- package/package.json +55 -48
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +209 -70
- package/src/agent/prompts/toolsPrompt.ts +285 -138
- package/src/agent/provider/anthropic.ts +109 -105
- package/src/agent/provider/google.ts +111 -107
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +73 -17
- package/src/agent/provider/openai.ts +146 -102
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +108 -104
- package/src/agent/tools/definitions.ts +15 -1
- package/src/agent/tools/executor.ts +717 -98
- package/src/agent/tools/exploreExecutor.ts +20 -22
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +64 -9
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +15 -14
- package/src/components/App.tsx +50 -8
- package/src/components/CustomInput.tsx +461 -77
- package/src/components/Main.tsx +1459 -1112
- package/src/components/Setup.tsx +1 -1
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -516
- package/src/components/main/HomePage.tsx +58 -39
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +13 -2
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +53 -25
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +45 -12
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +9 -7
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +13 -16
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -16
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +428 -48
- package/src/web/app.tsx +65 -5
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +3 -3
- package/src/web/components/MessageItem.tsx +80 -81
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -3
- package/src/web/components/ThinkingIndicator.tsx +41 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +894 -662
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -1,15 +1,185 @@
|
|
|
1
|
-
import { readFile, writeFile, readdir, appendFile, stat, mkdir } from 'fs/promises';
|
|
1
|
+
import { readFile, writeFile, readdir, appendFile, stat, mkdir, realpath } from 'fs/promises';
|
|
2
2
|
import { join, resolve, dirname, extname, sep } from 'path';
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { requestApproval } from '../../utils/approvalBridge';
|
|
6
6
|
import { shouldRequireApprovals } from '../../utils/config';
|
|
7
7
|
import { generateDiff, formatDiffForDisplay } from '../../utils/diff';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
8
|
+
import { trackFileChange, trackFileCreated } from '../../utils/fileChangeTracker';
|
|
9
|
+
import TurndownService from 'turndown';
|
|
10
|
+
import { Readability } from '@mozilla/readability';
|
|
11
|
+
import { parseHTML } from 'linkedom';
|
|
10
12
|
|
|
11
13
|
const execAsync = promisify(exec);
|
|
12
14
|
|
|
15
|
+
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0';
|
|
16
|
+
const DEFAULT_FETCH_MAX_LENGTH = 10000;
|
|
17
|
+
const DEFAULT_FETCH_TIMEOUT = 30000;
|
|
18
|
+
|
|
19
|
+
function extractContentFromHtml(html: string, url: string): { content: string; title: string | null; isSPA: boolean } {
|
|
20
|
+
const { document } = parseHTML(html);
|
|
21
|
+
|
|
22
|
+
const turndown = new TurndownService({
|
|
23
|
+
headingStyle: 'atx',
|
|
24
|
+
codeBlockStyle: 'fenced',
|
|
25
|
+
emDelimiter: '*',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
turndown.addRule('removeScripts', {
|
|
29
|
+
filter: ['script', 'style', 'noscript'],
|
|
30
|
+
replacement: () => '',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
turndown.addRule('preserveLinks', {
|
|
34
|
+
filter: 'a',
|
|
35
|
+
replacement: (content, node) => {
|
|
36
|
+
const element = node as HTMLAnchorElement;
|
|
37
|
+
const href = element.getAttribute('href');
|
|
38
|
+
if (!href || href.startsWith('#')) return content;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const absoluteUrl = new URL(href, url).toString();
|
|
42
|
+
return `[${content}](${absoluteUrl})`;
|
|
43
|
+
} catch {
|
|
44
|
+
return `[${content}](${href})`;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
turndown.addRule('preserveImages', {
|
|
50
|
+
filter: 'img',
|
|
51
|
+
replacement: (_content, node) => {
|
|
52
|
+
const element = node as HTMLImageElement;
|
|
53
|
+
const src = element.getAttribute('src');
|
|
54
|
+
const alt = element.getAttribute('alt') || '';
|
|
55
|
+
if (!src) return '';
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const absoluteUrl = new URL(src, url).toString();
|
|
59
|
+
return ``;
|
|
60
|
+
} catch {
|
|
61
|
+
return ``;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const reader = new Readability(document as unknown as Document, {
|
|
67
|
+
charThreshold: 0,
|
|
68
|
+
});
|
|
69
|
+
const article = reader.parse();
|
|
70
|
+
|
|
71
|
+
if (article && article.content) {
|
|
72
|
+
const content = turndown.turndown(article.content).trim();
|
|
73
|
+
if (content.length > 50) {
|
|
74
|
+
return {
|
|
75
|
+
content,
|
|
76
|
+
title: article.title || document.title || null,
|
|
77
|
+
isSPA: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
83
|
+
const bodyContent = bodyMatch ? bodyMatch[1] : html;
|
|
84
|
+
const markdownContent = turndown.turndown(bodyContent || '').trim();
|
|
85
|
+
|
|
86
|
+
if (markdownContent.length > 50) {
|
|
87
|
+
return {
|
|
88
|
+
content: markdownContent,
|
|
89
|
+
title: document.title || null,
|
|
90
|
+
isSPA: false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isSPA = html.includes('id="root"') ||
|
|
95
|
+
html.includes('id="app"') ||
|
|
96
|
+
html.includes('id="__next"') ||
|
|
97
|
+
html.includes('data-reactroot') ||
|
|
98
|
+
html.includes('ng-app');
|
|
99
|
+
|
|
100
|
+
const metaTags: string[] = [];
|
|
101
|
+
const metaDescription = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
|
|
102
|
+
const metaOgTitle = html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["']/i);
|
|
103
|
+
const metaOgDescription = html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']+)["']/i);
|
|
104
|
+
|
|
105
|
+
if (metaOgTitle) metaTags.push(`**Title:** ${metaOgTitle[1]}`);
|
|
106
|
+
if (metaDescription) metaTags.push(`**Description:** ${metaDescription[1]}`);
|
|
107
|
+
if (metaOgDescription && metaOgDescription[1] !== metaDescription?.[1]) {
|
|
108
|
+
metaTags.push(`**OG Description:** ${metaOgDescription[1]}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let content = '';
|
|
112
|
+
if (isSPA) {
|
|
113
|
+
content = `*This appears to be a Single Page Application (SPA/React/Vue/Angular). The content is rendered client-side with JavaScript and cannot be extracted via simple HTTP fetch.*\n\n`;
|
|
114
|
+
if (metaTags.length > 0) {
|
|
115
|
+
content += `**Available metadata:**\n${metaTags.join('\n')}\n\n`;
|
|
116
|
+
}
|
|
117
|
+
content += `*To see the actual content, you would need a headless browser. Try using raw=true to see the HTML source.*`;
|
|
118
|
+
} else if (markdownContent) {
|
|
119
|
+
content = markdownContent;
|
|
120
|
+
} else {
|
|
121
|
+
content = `*No readable content could be extracted from this page.*\n\n`;
|
|
122
|
+
if (metaTags.length > 0) {
|
|
123
|
+
content += `**Available metadata:**\n${metaTags.join('\n')}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
content,
|
|
129
|
+
title: document.title || null,
|
|
130
|
+
isSPA,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fetchUrlContent(
|
|
135
|
+
url: string,
|
|
136
|
+
options: {
|
|
137
|
+
raw?: boolean;
|
|
138
|
+
timeout?: number;
|
|
139
|
+
userAgent?: string;
|
|
140
|
+
} = {}
|
|
141
|
+
): Promise<{ content: string; contentType: string; title: string | null; status: number; statusText: string; isSPA?: boolean }> {
|
|
142
|
+
const { raw = false, timeout = DEFAULT_FETCH_TIMEOUT, userAgent = DEFAULT_USER_AGENT } = options;
|
|
143
|
+
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const response = await globalThis.fetch(url, {
|
|
149
|
+
headers: {
|
|
150
|
+
'User-Agent': userAgent,
|
|
151
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
152
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
153
|
+
},
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
redirect: 'follow',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const status = response.status;
|
|
159
|
+
const statusText = response.statusText;
|
|
160
|
+
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(`HTTP ${status} ${statusText}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const contentType = response.headers.get('content-type') || '';
|
|
166
|
+
const text = await response.text();
|
|
167
|
+
|
|
168
|
+
const isHtml = contentType.includes('text/html') ||
|
|
169
|
+
text.slice(0, 500).toLowerCase().includes('<html') ||
|
|
170
|
+
text.slice(0, 500).toLowerCase().includes('<!doctype html');
|
|
171
|
+
|
|
172
|
+
if (isHtml && !raw) {
|
|
173
|
+
const { content, title, isSPA } = extractContentFromHtml(text, url);
|
|
174
|
+
return { content, contentType, title, isSPA, status, statusText };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { content: text, contentType, title: null, status, statusText };
|
|
178
|
+
} finally {
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
13
183
|
export interface ToolResult {
|
|
14
184
|
success: boolean;
|
|
15
185
|
result?: string;
|
|
@@ -18,23 +188,23 @@ export interface ToolResult {
|
|
|
18
188
|
diff?: string[];
|
|
19
189
|
}
|
|
20
190
|
|
|
21
|
-
const pathValidationCache = new Map<string, boolean>();
|
|
22
191
|
const globPatternCache = new Map<string, RegExp>();
|
|
23
192
|
|
|
24
|
-
function validatePath(fullPath: string, workspace: string): boolean {
|
|
25
|
-
const
|
|
26
|
-
const cached = pathValidationCache.get(cacheKey);
|
|
27
|
-
if (cached !== undefined) return cached;
|
|
193
|
+
async function validatePath(fullPath: string, workspace: string): Promise<boolean> {
|
|
194
|
+
const normalizedWorkspace = workspace.endsWith(sep) ? workspace : workspace + sep;
|
|
28
195
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
196
|
+
try {
|
|
197
|
+
const resolved = await realpath(fullPath);
|
|
198
|
+
return resolved === workspace || resolved.startsWith(normalizedWorkspace);
|
|
199
|
+
} catch {
|
|
200
|
+
const parent = dirname(fullPath);
|
|
201
|
+
try {
|
|
202
|
+
const resolvedParent = await realpath(parent);
|
|
203
|
+
return resolvedParent === workspace || resolvedParent.startsWith(normalizedWorkspace);
|
|
204
|
+
} catch {
|
|
205
|
+
return fullPath === workspace || fullPath.startsWith(normalizedWorkspace);
|
|
206
|
+
}
|
|
35
207
|
}
|
|
36
|
-
|
|
37
|
-
return result;
|
|
38
208
|
}
|
|
39
209
|
|
|
40
210
|
const EXCLUDED_DIRECTORIES = new Set([
|
|
@@ -69,14 +239,14 @@ function matchGlob(filename: string, pattern: string): boolean {
|
|
|
69
239
|
if (!regex) {
|
|
70
240
|
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
71
241
|
|
|
72
|
-
let regexPattern = normalizedPattern.replace(/[.+^${}()|[\]
|
|
242
|
+
let regexPattern = normalizedPattern.replace(/[.+^${}()|[\]\\*?]/g, '\\$&');
|
|
73
243
|
|
|
74
244
|
regexPattern = regexPattern
|
|
75
|
-
.replace(
|
|
76
|
-
.replace(
|
|
77
|
-
.replace(
|
|
78
|
-
.replace(
|
|
79
|
-
.replace(
|
|
245
|
+
.replace(/\\\*\\\*\\\//g, '(?:(?:[^/]+/)*)')
|
|
246
|
+
.replace(/\\\/\*\\\*$/g, '(?:/.*)?')
|
|
247
|
+
.replace(/\\\*\\\*/g, '.*')
|
|
248
|
+
.replace(/\\\*/g, '[^/]*')
|
|
249
|
+
.replace(/\\\?/g, '[^/]');
|
|
80
250
|
|
|
81
251
|
regex = new RegExp(`^${regexPattern}$`, 'i');
|
|
82
252
|
globPatternCache.set(pattern, regex);
|
|
@@ -91,32 +261,190 @@ function matchGlob(filename: string, pattern: string): boolean {
|
|
|
91
261
|
return regex.test(normalizedFilename);
|
|
92
262
|
}
|
|
93
263
|
|
|
94
|
-
|
|
264
|
+
interface SearchResult {
|
|
265
|
+
matches: Array<{ line: number; content: string; context?: { before: string[]; after: string[] } }>;
|
|
266
|
+
error?: string;
|
|
267
|
+
matchCount?: number;
|
|
268
|
+
skipped?: boolean;
|
|
269
|
+
skipReason?: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface SearchOptions {
|
|
273
|
+
caseSensitive: boolean;
|
|
274
|
+
isRegex: boolean;
|
|
275
|
+
wholeWord: boolean;
|
|
276
|
+
multiline: boolean;
|
|
277
|
+
contextBefore: number;
|
|
278
|
+
contextAfter: number;
|
|
279
|
+
maxFileSize: number;
|
|
280
|
+
invertMatch: boolean;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
|
|
284
|
+
|
|
285
|
+
function isBinaryFile(buffer: Buffer, bytesToCheck = 8000): boolean {
|
|
286
|
+
const checkLength = Math.min(buffer.length, bytesToCheck);
|
|
287
|
+
let nullCount = 0;
|
|
288
|
+
let controlCount = 0;
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < checkLength; i++) {
|
|
291
|
+
const byte = buffer[i];
|
|
292
|
+
if (byte === 0) {
|
|
293
|
+
nullCount++;
|
|
294
|
+
if (nullCount > 1) return true;
|
|
295
|
+
}
|
|
296
|
+
if (byte !== undefined && byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
|
|
297
|
+
controlCount++;
|
|
298
|
+
if (controlCount > checkLength * 0.1) return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function escapeRegexForLiteral(str: string): string {
|
|
306
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildSearchRegex(query: string, options: SearchOptions): { regex: RegExp; error?: undefined } | { regex?: undefined; error: string } {
|
|
310
|
+
try {
|
|
311
|
+
let pattern = query;
|
|
312
|
+
|
|
313
|
+
if (!options.isRegex) {
|
|
314
|
+
pattern = escapeRegexForLiteral(query);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (options.wholeWord) {
|
|
318
|
+
if (options.isRegex) {
|
|
319
|
+
pattern = `(?:^|\\b)${pattern}(?:\\b|$)`;
|
|
320
|
+
} else {
|
|
321
|
+
pattern = `\\b${pattern}\\b`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let flags = 'g';
|
|
326
|
+
if (!options.caseSensitive) flags += 'i';
|
|
327
|
+
if (options.multiline) flags += 'm';
|
|
328
|
+
|
|
329
|
+
return { regex: new RegExp(pattern, flags) };
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return { error: e instanceof Error ? e.message : 'Invalid pattern' };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function searchInFile(filePath: string, query: string, options: SearchOptions): Promise<SearchResult> {
|
|
95
336
|
try {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
337
|
+
const stats = await stat(filePath);
|
|
338
|
+
|
|
339
|
+
if (stats.size > options.maxFileSize) {
|
|
340
|
+
return {
|
|
341
|
+
matches: [],
|
|
342
|
+
skipped: true,
|
|
343
|
+
skipReason: `File too large (${Math.round(stats.size / 1024)}KB > ${Math.round(options.maxFileSize / 1024)}KB)`
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const buffer = await readFile(filePath);
|
|
348
|
+
|
|
349
|
+
if (isBinaryFile(buffer)) {
|
|
350
|
+
return {
|
|
351
|
+
matches: [],
|
|
352
|
+
skipped: true,
|
|
353
|
+
skipReason: 'Binary file'
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const content = buffer.toString('utf-8');
|
|
358
|
+
const lines = content.split('\n');
|
|
99
359
|
|
|
100
|
-
|
|
101
|
-
|
|
360
|
+
const regexResult = buildSearchRegex(query, options);
|
|
361
|
+
if (regexResult.error || !regexResult.regex) {
|
|
362
|
+
return { matches: [], error: regexResult.error ?? 'Failed to build search pattern' };
|
|
363
|
+
}
|
|
364
|
+
const regex: RegExp = regexResult.regex;
|
|
365
|
+
|
|
366
|
+
if (options.invertMatch) {
|
|
367
|
+
const hasMatch = lines.some(line => regex.test(line));
|
|
368
|
+
return {
|
|
369
|
+
matches: [],
|
|
370
|
+
matchCount: hasMatch ? 0 : 1,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
102
373
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
374
|
+
if (options.multiline && options.isRegex) {
|
|
375
|
+
const multilineMatches: Array<{ line: number; content: string }> = [];
|
|
376
|
+
let match;
|
|
377
|
+
regex.lastIndex = 0;
|
|
107
378
|
|
|
108
|
-
|
|
109
|
-
|
|
379
|
+
while ((match = regex.exec(content)) !== null) {
|
|
380
|
+
const matchStart = match.index;
|
|
381
|
+
let lineNumber = 1;
|
|
382
|
+
for (let i = 0; i < matchStart; i++) {
|
|
383
|
+
if (content[i] === '\n') lineNumber++;
|
|
110
384
|
}
|
|
111
385
|
|
|
112
|
-
|
|
113
|
-
|
|
386
|
+
const matchedText = match[0];
|
|
387
|
+
const matchLines = matchedText.split('\n');
|
|
388
|
+
|
|
389
|
+
multilineMatches.push({
|
|
390
|
+
line: lineNumber,
|
|
391
|
+
content: matchLines.length > 1
|
|
392
|
+
? `${matchLines[0]}... (+${matchLines.length - 1} lines)`
|
|
393
|
+
: matchedText.slice(0, 200)
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (regex.lastIndex === match.index) {
|
|
397
|
+
regex.lastIndex++;
|
|
398
|
+
}
|
|
114
399
|
}
|
|
400
|
+
|
|
401
|
+
return { matches: multilineMatches, matchCount: multilineMatches.length };
|
|
115
402
|
}
|
|
116
403
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
404
|
+
const matches: Array<{ line: number; content: string; context?: { before: string[]; after: string[] } }> = [];
|
|
405
|
+
let matchCount = 0;
|
|
406
|
+
|
|
407
|
+
for (let i = 0; i < lines.length; i++) {
|
|
408
|
+
const line = lines[i];
|
|
409
|
+
if (line === undefined) continue;
|
|
410
|
+
|
|
411
|
+
regex.lastIndex = 0;
|
|
412
|
+
if (regex.test(line)) {
|
|
413
|
+
matchCount++;
|
|
414
|
+
|
|
415
|
+
const contextBefore: string[] = [];
|
|
416
|
+
const contextAfter: string[] = [];
|
|
417
|
+
|
|
418
|
+
if (options.contextBefore > 0) {
|
|
419
|
+
for (let j = Math.max(0, i - options.contextBefore); j < i; j++) {
|
|
420
|
+
const ctxLine = lines[j];
|
|
421
|
+
if (ctxLine !== undefined) contextBefore.push(ctxLine);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (options.contextAfter > 0) {
|
|
426
|
+
for (let j = i + 1; j <= Math.min(lines.length - 1, i + options.contextAfter); j++) {
|
|
427
|
+
const ctxLine = lines[j];
|
|
428
|
+
if (ctxLine !== undefined) contextAfter.push(ctxLine);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const hasContext = contextBefore.length > 0 || contextAfter.length > 0;
|
|
433
|
+
|
|
434
|
+
matches.push({
|
|
435
|
+
line: i + 1,
|
|
436
|
+
content: line,
|
|
437
|
+
...(hasContext && { context: { before: contextBefore, after: contextAfter } })
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { matches, matchCount };
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return {
|
|
445
|
+
matches: [],
|
|
446
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
447
|
+
};
|
|
120
448
|
}
|
|
121
449
|
}
|
|
122
450
|
|
|
@@ -126,12 +454,18 @@ interface WalkResult {
|
|
|
126
454
|
excluded?: boolean;
|
|
127
455
|
}
|
|
128
456
|
|
|
129
|
-
|
|
457
|
+
interface WalkOutput {
|
|
458
|
+
results: WalkResult[];
|
|
459
|
+
errors: string[];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function walkDirectory(dir: string, filePattern?: string, includeHidden = false): Promise<WalkOutput> {
|
|
130
463
|
const results: WalkResult[] = [];
|
|
464
|
+
const errors: string[] = [];
|
|
131
465
|
|
|
132
466
|
try {
|
|
133
467
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
134
|
-
const subDirPromises: Promise<
|
|
468
|
+
const subDirPromises: Promise<WalkOutput>[] = [];
|
|
135
469
|
|
|
136
470
|
for (const entry of entries) {
|
|
137
471
|
if (!includeHidden && entry.name.startsWith('.')) continue;
|
|
@@ -152,27 +486,31 @@ async function walkDirectory(dir: string, filePattern?: string, includeHidden =
|
|
|
152
486
|
}
|
|
153
487
|
|
|
154
488
|
if (subDirPromises.length > 0) {
|
|
155
|
-
const
|
|
156
|
-
for (const
|
|
157
|
-
results.push(...
|
|
489
|
+
const subOutputs = await Promise.all(subDirPromises);
|
|
490
|
+
for (const sub of subOutputs) {
|
|
491
|
+
results.push(...sub.results);
|
|
492
|
+
errors.push(...sub.errors);
|
|
158
493
|
}
|
|
159
494
|
}
|
|
160
|
-
} catch {
|
|
161
|
-
|
|
495
|
+
} catch (e) {
|
|
496
|
+
errors.push(`${dir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
162
497
|
}
|
|
163
498
|
|
|
164
|
-
return results;
|
|
499
|
+
return { results, errors };
|
|
165
500
|
}
|
|
166
501
|
|
|
167
|
-
async function listFilesRecursive(dirPath: string, workspace: string, filterPattern?: string, includeHidden = false): Promise<
|
|
502
|
+
async function listFilesRecursive(dirPath: string, workspace: string, filterPattern?: string, includeHidden = false): Promise<WalkOutput> {
|
|
168
503
|
const fullPath = resolve(workspace, dirPath);
|
|
169
|
-
const
|
|
504
|
+
const { results, errors } = await walkDirectory(fullPath, filterPattern, includeHidden);
|
|
170
505
|
const separator = workspace.endsWith(sep) ? '' : sep;
|
|
171
506
|
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
507
|
+
return {
|
|
508
|
+
results: results.map(file => ({
|
|
509
|
+
...file,
|
|
510
|
+
path: file.path.replace(workspace + separator, '')
|
|
511
|
+
})),
|
|
512
|
+
errors,
|
|
513
|
+
};
|
|
176
514
|
}
|
|
177
515
|
|
|
178
516
|
async function findFilesByPattern(pattern: string, searchPath: string): Promise<string[]> {
|
|
@@ -181,7 +519,7 @@ async function findFilesByPattern(pattern: string, searchPath: string): Promise<
|
|
|
181
519
|
const hasDoubleStar = pattern.includes('**');
|
|
182
520
|
|
|
183
521
|
if (hasDoubleStar) {
|
|
184
|
-
const files = await walkDirectory(searchPath, undefined, false);
|
|
522
|
+
const { results: files } = await walkDirectory(searchPath, undefined, false);
|
|
185
523
|
const separator = searchPath.endsWith(sep) ? '' : sep;
|
|
186
524
|
const root = searchPath + separator;
|
|
187
525
|
|
|
@@ -380,9 +718,11 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
380
718
|
switch (toolName) {
|
|
381
719
|
case 'read': {
|
|
382
720
|
const path = args.path as string;
|
|
721
|
+
const startLine = args.start_line as number | undefined;
|
|
722
|
+
const endLine = args.end_line as number | undefined;
|
|
383
723
|
const fullPath = resolve(workspace, path);
|
|
384
724
|
|
|
385
|
-
if (!validatePath(fullPath, workspace)) {
|
|
725
|
+
if (!await validatePath(fullPath, workspace)) {
|
|
386
726
|
return {
|
|
387
727
|
success: false,
|
|
388
728
|
error: 'Access denied: path is outside workspace'
|
|
@@ -390,6 +730,26 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
390
730
|
}
|
|
391
731
|
|
|
392
732
|
const content = await readFile(fullPath, 'utf-8');
|
|
733
|
+
|
|
734
|
+
if (startLine !== undefined || endLine !== undefined) {
|
|
735
|
+
const lines = content.split('\n');
|
|
736
|
+
const start = (startLine ?? 1) - 1;
|
|
737
|
+
const end = endLine ?? lines.length;
|
|
738
|
+
|
|
739
|
+
if (start < 0 || start >= lines.length) {
|
|
740
|
+
return {
|
|
741
|
+
success: false,
|
|
742
|
+
error: `Start line ${startLine} is out of bounds (1-${lines.length})`
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const selectedLines = lines.slice(start, end);
|
|
747
|
+
return {
|
|
748
|
+
success: true,
|
|
749
|
+
result: selectedLines.join('\n')
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
393
753
|
return {
|
|
394
754
|
success: true,
|
|
395
755
|
result: content
|
|
@@ -398,19 +758,18 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
398
758
|
|
|
399
759
|
case 'write': {
|
|
400
760
|
const path = args.path as string;
|
|
401
|
-
|
|
761
|
+
let content = typeof args.content === 'string' ? args.content : '';
|
|
762
|
+
if (content) content = content.trimEnd();
|
|
402
763
|
const append = args.append === true;
|
|
403
764
|
const fullPath = resolve(workspace, path);
|
|
404
765
|
|
|
405
|
-
if (!validatePath(fullPath, workspace)) {
|
|
766
|
+
if (!await validatePath(fullPath, workspace)) {
|
|
406
767
|
return {
|
|
407
768
|
success: false,
|
|
408
769
|
error: 'Access denied: path is outside workspace'
|
|
409
770
|
};
|
|
410
771
|
}
|
|
411
772
|
|
|
412
|
-
captureFileSnapshot(path);
|
|
413
|
-
|
|
414
773
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
415
774
|
|
|
416
775
|
let oldContent = '';
|
|
@@ -455,7 +814,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
455
814
|
const includeHidden = args.include_hidden === null ? undefined : (args.include_hidden as boolean | undefined);
|
|
456
815
|
const fullPath = resolve(workspace, path);
|
|
457
816
|
|
|
458
|
-
if (!validatePath(fullPath, workspace)) {
|
|
817
|
+
if (!await validatePath(fullPath, workspace)) {
|
|
459
818
|
return {
|
|
460
819
|
success: false,
|
|
461
820
|
error: 'Access denied: path is outside workspace'
|
|
@@ -463,7 +822,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
463
822
|
}
|
|
464
823
|
|
|
465
824
|
if (recursive) {
|
|
466
|
-
const files = await listFilesRecursive(path, workspace, filter, includeHidden);
|
|
825
|
+
const { results: files, errors: walkErrors } = await listFilesRecursive(path, workspace, filter, includeHidden);
|
|
467
826
|
const fileStats = await Promise.all(
|
|
468
827
|
files.map(async (file) => {
|
|
469
828
|
if (file.excluded) {
|
|
@@ -474,17 +833,29 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
474
833
|
};
|
|
475
834
|
}
|
|
476
835
|
const filePath = resolve(workspace, file.path);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
836
|
+
try {
|
|
837
|
+
const stats = await stat(filePath);
|
|
838
|
+
return {
|
|
839
|
+
path: file.path,
|
|
840
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
841
|
+
size: stats.size,
|
|
842
|
+
};
|
|
843
|
+
} catch {
|
|
844
|
+
return {
|
|
845
|
+
path: file.path,
|
|
846
|
+
type: 'unknown',
|
|
847
|
+
error: 'access denied',
|
|
848
|
+
};
|
|
849
|
+
}
|
|
483
850
|
})
|
|
484
851
|
);
|
|
852
|
+
const output: Record<string, unknown> = { files: fileStats };
|
|
853
|
+
if (walkErrors.length > 0) {
|
|
854
|
+
output.errors = walkErrors.slice(0, 10);
|
|
855
|
+
}
|
|
485
856
|
return {
|
|
486
857
|
success: true,
|
|
487
|
-
result: JSON.stringify(
|
|
858
|
+
result: JSON.stringify(output, null, 2)
|
|
488
859
|
};
|
|
489
860
|
} else {
|
|
490
861
|
const entries = await readdir(fullPath, { withFileTypes: true });
|
|
@@ -495,7 +866,11 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
495
866
|
}
|
|
496
867
|
|
|
497
868
|
if (filter) {
|
|
498
|
-
const
|
|
869
|
+
const escapedFilter = filter
|
|
870
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
871
|
+
.replace(/\*/g, '.*')
|
|
872
|
+
.replace(/\?/g, '.');
|
|
873
|
+
const regex = new RegExp(`^${escapedFilter}$`, 'i');
|
|
499
874
|
filteredEntries = filteredEntries.filter(entry => regex.test(entry.name));
|
|
500
875
|
}
|
|
501
876
|
|
|
@@ -544,7 +919,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
544
919
|
: `Command timed out after ${timeout}ms and produced no output.\n\n[Process may be running in background]`;
|
|
545
920
|
|
|
546
921
|
return {
|
|
547
|
-
success:
|
|
922
|
+
success: false,
|
|
548
923
|
result: output
|
|
549
924
|
};
|
|
550
925
|
}
|
|
@@ -556,7 +931,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
556
931
|
: `Command failed: ${errorMessage}`;
|
|
557
932
|
|
|
558
933
|
return {
|
|
559
|
-
success:
|
|
934
|
+
success: false,
|
|
560
935
|
result: fullOutput
|
|
561
936
|
};
|
|
562
937
|
}
|
|
@@ -567,7 +942,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
567
942
|
const searchPath = (args.path === null ? undefined : (args.path as string | undefined)) || '.';
|
|
568
943
|
const fullPath = resolve(workspace, searchPath);
|
|
569
944
|
|
|
570
|
-
if (!validatePath(fullPath, workspace)) {
|
|
945
|
+
if (!await validatePath(fullPath, workspace)) {
|
|
571
946
|
return {
|
|
572
947
|
success: false,
|
|
573
948
|
error: 'Access denied: path is outside workspace'
|
|
@@ -583,72 +958,227 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
583
958
|
}
|
|
584
959
|
|
|
585
960
|
case 'grep': {
|
|
586
|
-
const
|
|
961
|
+
const { FILE_TYPE_EXTENSIONS } = await import('./grep.ts');
|
|
962
|
+
|
|
963
|
+
const pattern = args.pattern === null ? undefined : (args.pattern as string | undefined);
|
|
964
|
+
const fileType = args.file_type === null ? undefined : (args.file_type as string | undefined);
|
|
587
965
|
const query = args.query as string;
|
|
588
966
|
const searchPath = (args.path === null ? undefined : (args.path as string | undefined)) || '.';
|
|
589
967
|
const caseSensitive = ((args.case_sensitive === null ? undefined : (args.case_sensitive as boolean | undefined)) ?? false);
|
|
590
|
-
const
|
|
968
|
+
const isRegex = ((args.regex === null ? undefined : (args.regex as boolean | undefined)) ?? false);
|
|
969
|
+
const wholeWord = ((args.whole_word === null ? undefined : (args.whole_word as boolean | undefined)) ?? false);
|
|
970
|
+
const multiline = ((args.multiline === null ? undefined : (args.multiline as boolean | undefined)) ?? false);
|
|
971
|
+
const context = ((args.context === null ? undefined : (args.context as number | undefined)) ?? 0);
|
|
972
|
+
const contextBefore = ((args.context_before === null ? undefined : (args.context_before as number | undefined)) ?? context);
|
|
973
|
+
const contextAfter = ((args.context_after === null ? undefined : (args.context_after as number | undefined)) ?? context);
|
|
974
|
+
const maxResults = ((args.max_results === null ? undefined : (args.max_results as number | undefined)) ?? 500);
|
|
975
|
+
const maxFileSize = ((args.max_file_size === null ? undefined : (args.max_file_size as number | undefined)) ?? DEFAULT_MAX_FILE_SIZE);
|
|
976
|
+
const includeHidden = ((args.include_hidden === null ? undefined : (args.include_hidden as boolean | undefined)) ?? false);
|
|
977
|
+
const excludePattern = args.exclude_pattern === null ? undefined : (args.exclude_pattern as string | undefined);
|
|
978
|
+
const outputMode = ((args.output_mode === null ? undefined : (args.output_mode as string | undefined)) ?? 'matches') as 'matches' | 'files' | 'count';
|
|
979
|
+
const invertMatch = ((args.invert_match === null ? undefined : (args.invert_match as boolean | undefined)) ?? false);
|
|
980
|
+
|
|
591
981
|
const fullPath = resolve(workspace, searchPath);
|
|
592
982
|
|
|
593
|
-
if (!validatePath(fullPath, workspace)) {
|
|
983
|
+
if (!await validatePath(fullPath, workspace)) {
|
|
594
984
|
return {
|
|
595
985
|
success: false,
|
|
596
986
|
error: 'Access denied: path is outside workspace'
|
|
597
987
|
};
|
|
598
988
|
}
|
|
599
989
|
|
|
600
|
-
const
|
|
990
|
+
const testSearchOptions: SearchOptions = {
|
|
991
|
+
caseSensitive,
|
|
992
|
+
isRegex,
|
|
993
|
+
wholeWord,
|
|
994
|
+
multiline,
|
|
995
|
+
contextBefore: 0,
|
|
996
|
+
contextAfter: 0,
|
|
997
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
998
|
+
invertMatch: false,
|
|
999
|
+
};
|
|
1000
|
+
const regexTest = buildSearchRegex(query, testSearchOptions);
|
|
1001
|
+
if (regexTest.error) {
|
|
1002
|
+
return {
|
|
1003
|
+
success: false,
|
|
1004
|
+
error: `Invalid search pattern: ${regexTest.error}`
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const normalizedFileType = typeof fileType === 'string' ? fileType.trim().toLowerCase() : undefined;
|
|
1009
|
+
const fileTypeParts = normalizedFileType
|
|
1010
|
+
? normalizedFileType.split(',').map(p => p.trim()).filter(Boolean)
|
|
1011
|
+
: [];
|
|
1012
|
+
const resolvedExtensions = fileTypeParts.length > 0
|
|
1013
|
+
? Array.from(new Set(fileTypeParts.flatMap((part) => {
|
|
1014
|
+
const mapped = FILE_TYPE_EXTENSIONS[part];
|
|
1015
|
+
if (mapped && mapped.length > 0) return mapped;
|
|
1016
|
+
if (part.startsWith('.')) return [part];
|
|
1017
|
+
return [`.${part}`];
|
|
1018
|
+
})))
|
|
1019
|
+
: undefined;
|
|
1020
|
+
|
|
1021
|
+
let finalPattern: string;
|
|
1022
|
+
if (pattern) {
|
|
1023
|
+
finalPattern = pattern.includes('**') ? pattern : `**/${pattern}`;
|
|
1024
|
+
} else if (resolvedExtensions && resolvedExtensions.length === 1) {
|
|
1025
|
+
finalPattern = `**/*${resolvedExtensions[0]}`;
|
|
1026
|
+
} else {
|
|
1027
|
+
finalPattern = '**/*';
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
let allFiles = await findFilesByPattern(finalPattern, fullPath);
|
|
1031
|
+
|
|
1032
|
+
if (!includeHidden) {
|
|
1033
|
+
allFiles = allFiles.filter(f => !f.split('/').some(part => part.startsWith('.')));
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (resolvedExtensions && !pattern) {
|
|
1037
|
+
allFiles = allFiles.filter(f => resolvedExtensions.some(ext => f.toLowerCase().endsWith(ext)));
|
|
1038
|
+
}
|
|
601
1039
|
|
|
602
|
-
|
|
1040
|
+
if (excludePattern) {
|
|
1041
|
+
allFiles = allFiles.filter(f => !matchGlob(f, excludePattern));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const searchOptions: SearchOptions = {
|
|
1045
|
+
caseSensitive,
|
|
1046
|
+
isRegex,
|
|
1047
|
+
wholeWord,
|
|
1048
|
+
multiline,
|
|
1049
|
+
contextBefore,
|
|
1050
|
+
contextAfter,
|
|
1051
|
+
maxFileSize,
|
|
1052
|
+
invertMatch,
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
type MatchType = { line: number; content: string; context?: { before: string[]; after: string[] } };
|
|
1056
|
+
const results: Array<{ file: string; matches: MatchType[]; count?: number }> = [];
|
|
1057
|
+
const skippedFiles: Array<{ file: string; reason: string }> = [];
|
|
603
1058
|
let totalResults = 0;
|
|
1059
|
+
let totalMatchCount = 0;
|
|
604
1060
|
|
|
605
|
-
const BATCH_SIZE =
|
|
606
|
-
for (let i = 0; i <
|
|
607
|
-
if (totalResults >= maxResults) break;
|
|
1061
|
+
const BATCH_SIZE = 15;
|
|
1062
|
+
for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
|
|
1063
|
+
if (!invertMatch && totalResults >= maxResults) break;
|
|
608
1064
|
|
|
609
|
-
const batch =
|
|
1065
|
+
const batch = allFiles.slice(i, i + BATCH_SIZE);
|
|
610
1066
|
const batchResults = await Promise.all(
|
|
611
1067
|
batch.map(async (file) => {
|
|
612
1068
|
const filePath = resolve(fullPath, file);
|
|
613
|
-
const
|
|
614
|
-
return {
|
|
1069
|
+
const searchResult = await searchInFile(filePath, query, searchOptions);
|
|
1070
|
+
return {
|
|
1071
|
+
file: join(searchPath, file),
|
|
1072
|
+
matches: searchResult.matches,
|
|
1073
|
+
matchCount: searchResult.matchCount ?? searchResult.matches.length,
|
|
1074
|
+
skipped: searchResult.skipped,
|
|
1075
|
+
skipReason: searchResult.skipReason,
|
|
1076
|
+
};
|
|
615
1077
|
})
|
|
616
1078
|
);
|
|
617
1079
|
|
|
618
|
-
for (const { file, matches } of batchResults) {
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1080
|
+
for (const { file, matches, matchCount, skipped, skipReason } of batchResults) {
|
|
1081
|
+
if (skipped && skipReason) {
|
|
1082
|
+
skippedFiles.push({ file, reason: skipReason });
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (invertMatch) {
|
|
1087
|
+
if (matchCount === 1) {
|
|
1088
|
+
results.push({ file, matches: [], count: 1 });
|
|
1089
|
+
totalResults++;
|
|
1090
|
+
}
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (matches.length > 0 || matchCount > 0) {
|
|
1095
|
+
totalMatchCount += matchCount;
|
|
1096
|
+
|
|
1097
|
+
if (outputMode === 'files') {
|
|
1098
|
+
results.push({ file, matches: [] });
|
|
1099
|
+
totalResults++;
|
|
1100
|
+
} else if (outputMode === 'count') {
|
|
1101
|
+
results.push({ file, matches: [], count: matchCount });
|
|
1102
|
+
totalResults++;
|
|
1103
|
+
} else {
|
|
1104
|
+
const remainingSlots = maxResults - totalResults;
|
|
1105
|
+
const matchesToInclude = matches.slice(0, remainingSlots);
|
|
1106
|
+
results.push({ file, matches: matchesToInclude, count: matchCount });
|
|
1107
|
+
totalResults += matchesToInclude.length;
|
|
1108
|
+
}
|
|
626
1109
|
}
|
|
627
1110
|
}
|
|
628
1111
|
}
|
|
629
1112
|
|
|
1113
|
+
let formattedResult: string;
|
|
1114
|
+
|
|
1115
|
+
const skippedDetails = skippedFiles.length > 0
|
|
1116
|
+
? skippedFiles.slice(0, 5).map(s => ({ file: s.file, reason: s.reason }))
|
|
1117
|
+
: undefined;
|
|
1118
|
+
|
|
1119
|
+
if (outputMode === 'files') {
|
|
1120
|
+
const filesOnly = results.map(r => r.file);
|
|
1121
|
+
const summary = {
|
|
1122
|
+
files_found: filesOnly.length,
|
|
1123
|
+
files: filesOnly,
|
|
1124
|
+
...(skippedFiles.length > 0 && { skipped: skippedFiles.length, skipped_details: skippedDetails })
|
|
1125
|
+
};
|
|
1126
|
+
formattedResult = JSON.stringify(summary, null, 2);
|
|
1127
|
+
} else if (outputMode === 'count') {
|
|
1128
|
+
const counts = results.map(r => ({ file: r.file, count: r.count ?? 0 }));
|
|
1129
|
+
const summary = {
|
|
1130
|
+
total_matches: totalMatchCount,
|
|
1131
|
+
files_with_matches: counts.length,
|
|
1132
|
+
counts,
|
|
1133
|
+
...(skippedFiles.length > 0 && { skipped: skippedFiles.length, skipped_details: skippedDetails })
|
|
1134
|
+
};
|
|
1135
|
+
formattedResult = JSON.stringify(summary, null, 2);
|
|
1136
|
+
} else {
|
|
1137
|
+
const summary = {
|
|
1138
|
+
total_matches: totalMatchCount,
|
|
1139
|
+
files_searched: allFiles.length,
|
|
1140
|
+
files_with_matches: results.length,
|
|
1141
|
+
...(skippedFiles.length > 0 && { skipped_files: skippedFiles.length, skipped_details: skippedDetails }),
|
|
1142
|
+
...(totalResults >= maxResults && { truncated: true, max_results: maxResults }),
|
|
1143
|
+
results: results.map(r => ({
|
|
1144
|
+
file: r.file,
|
|
1145
|
+
match_count: r.count ?? r.matches.length,
|
|
1146
|
+
matches: r.matches.map(m => {
|
|
1147
|
+
if (m.context && (m.context.before.length > 0 || m.context.after.length > 0)) {
|
|
1148
|
+
return {
|
|
1149
|
+
line: m.line,
|
|
1150
|
+
content: m.content,
|
|
1151
|
+
context: m.context
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
return { line: m.line, content: m.content };
|
|
1155
|
+
})
|
|
1156
|
+
}))
|
|
1157
|
+
};
|
|
1158
|
+
formattedResult = JSON.stringify(summary, null, 2);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
630
1161
|
return {
|
|
631
1162
|
success: true,
|
|
632
|
-
result:
|
|
1163
|
+
result: formattedResult
|
|
633
1164
|
};
|
|
634
1165
|
}
|
|
635
1166
|
|
|
636
1167
|
case 'edit': {
|
|
637
1168
|
const path = args.path as string;
|
|
638
1169
|
const oldContent = args.old_content as string;
|
|
639
|
-
|
|
1170
|
+
let newContent = args.new_content as string;
|
|
1171
|
+
if (newContent) newContent = newContent.trimEnd();
|
|
640
1172
|
const occurrence = ((args.occurrence === null ? undefined : (args.occurrence as number | undefined)) ?? 1);
|
|
641
1173
|
const fullPath = resolve(workspace, path);
|
|
642
1174
|
|
|
643
|
-
if (!validatePath(fullPath, workspace)) {
|
|
1175
|
+
if (!await validatePath(fullPath, workspace)) {
|
|
644
1176
|
return {
|
|
645
1177
|
success: false,
|
|
646
1178
|
error: 'Access denied: path is outside workspace'
|
|
647
1179
|
};
|
|
648
1180
|
}
|
|
649
1181
|
|
|
650
|
-
captureFileSnapshot(path);
|
|
651
|
-
|
|
652
1182
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
653
1183
|
|
|
654
1184
|
let content = '';
|
|
@@ -720,7 +1250,7 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
720
1250
|
}
|
|
721
1251
|
const fullPath = resolve(workspace, path);
|
|
722
1252
|
|
|
723
|
-
if (!validatePath(fullPath, workspace)) {
|
|
1253
|
+
if (!await validatePath(fullPath, workspace)) {
|
|
724
1254
|
return {
|
|
725
1255
|
success: false,
|
|
726
1256
|
error: 'Access denied: path is outside workspace'
|
|
@@ -735,6 +1265,95 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
735
1265
|
};
|
|
736
1266
|
}
|
|
737
1267
|
|
|
1268
|
+
case 'fetch': {
|
|
1269
|
+
const url = args.url as string;
|
|
1270
|
+
const maxLength = (args.max_length as number | undefined) ?? DEFAULT_FETCH_MAX_LENGTH;
|
|
1271
|
+
const startIndex = (args.start_index as number | undefined) ?? 0;
|
|
1272
|
+
const raw = (args.raw as boolean | undefined) ?? false;
|
|
1273
|
+
const timeout = (args.timeout as number | undefined) ?? DEFAULT_FETCH_TIMEOUT;
|
|
1274
|
+
|
|
1275
|
+
try {
|
|
1276
|
+
new URL(url);
|
|
1277
|
+
} catch {
|
|
1278
|
+
return {
|
|
1279
|
+
success: false,
|
|
1280
|
+
error: `Invalid URL: ${url}`,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
let fetchResult = await fetchUrlContent(url, { raw, timeout });
|
|
1286
|
+
let { content, contentType, title, isSPA, status, statusText } = fetchResult;
|
|
1287
|
+
|
|
1288
|
+
if (isSPA && !raw) {
|
|
1289
|
+
const rawResult = await fetchUrlContent(url, { raw: true, timeout });
|
|
1290
|
+
content = rawResult.content;
|
|
1291
|
+
contentType = rawResult.contentType;
|
|
1292
|
+
title = rawResult.title;
|
|
1293
|
+
status = rawResult.status;
|
|
1294
|
+
statusText = rawResult.statusText;
|
|
1295
|
+
isSPA = false;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const totalLength = content.length;
|
|
1299
|
+
|
|
1300
|
+
if (startIndex >= totalLength) {
|
|
1301
|
+
return {
|
|
1302
|
+
success: false,
|
|
1303
|
+
error: `Start index ${startIndex} exceeds content length ${totalLength}`,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const extractedContent = content.slice(startIndex, startIndex + maxLength);
|
|
1308
|
+
const truncated = startIndex + maxLength < totalLength;
|
|
1309
|
+
const nextStartIndex = truncated ? startIndex + maxLength : undefined;
|
|
1310
|
+
|
|
1311
|
+
const parts: string[] = [];
|
|
1312
|
+
|
|
1313
|
+
if (title) {
|
|
1314
|
+
parts.push(`# ${title}\n`);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
parts.push(`**URL:** ${url}`);
|
|
1318
|
+
parts.push(`**Status:** ${status} ${statusText}`);
|
|
1319
|
+
parts.push(`**Content-Type:** ${contentType}`);
|
|
1320
|
+
parts.push(`**Length:** ${extractedContent.length} / ${totalLength} characters`);
|
|
1321
|
+
|
|
1322
|
+
if (fetchResult.isSPA) {
|
|
1323
|
+
parts.push(`**Note:** SPA detected (React/Vue/Angular). Showing raw HTML source.`);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (truncated && nextStartIndex !== undefined) {
|
|
1327
|
+
parts.push(`**Status:** Content truncated. Use start_index=${nextStartIndex} to continue reading.`);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
parts.push('\n---\n');
|
|
1331
|
+
parts.push(extractedContent);
|
|
1332
|
+
|
|
1333
|
+
if (truncated && nextStartIndex !== undefined) {
|
|
1334
|
+
parts.push(`\n\n---\n*Content truncated at ${extractedContent.length} characters. Call fetch again with start_index=${nextStartIndex} to continue reading.*`);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
return {
|
|
1338
|
+
success: true,
|
|
1339
|
+
result: parts.join('\n'),
|
|
1340
|
+
};
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1343
|
+
|
|
1344
|
+
if (message.includes('abort')) {
|
|
1345
|
+
return {
|
|
1346
|
+
success: false,
|
|
1347
|
+
error: `Request timed out after ${timeout}ms`,
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
return {
|
|
1352
|
+
success: false,
|
|
1353
|
+
error: `Failed to fetch ${url}: ${message}`,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
738
1357
|
|
|
739
1358
|
default:
|
|
740
1359
|
return {
|
|
@@ -748,4 +1367,4 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
748
1367
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
749
1368
|
};
|
|
750
1369
|
}
|
|
751
|
-
}
|
|
1370
|
+
}
|