@kirosnn/mosaic 0.0.9 → 0.71.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 +83 -19
- package/package.json +52 -47
- package/src/agent/prompts/systemPrompt.ts +198 -68
- package/src/agent/prompts/toolsPrompt.ts +217 -135
- package/src/agent/provider/anthropic.ts +19 -15
- package/src/agent/provider/google.ts +21 -17
- package/src/agent/provider/ollama.ts +80 -41
- package/src/agent/provider/openai.ts +107 -67
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +19 -15
- package/src/agent/tools/definitions.ts +9 -5
- package/src/agent/tools/executor.ts +655 -46
- package/src/agent/tools/exploreExecutor.ts +12 -12
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +62 -8
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +6 -6
- package/src/components/App.tsx +67 -25
- package/src/components/CustomInput.tsx +274 -68
- package/src/components/Main.tsx +323 -168
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/main/ChatPage.tsx +217 -58
- package/src/components/main/HomePage.tsx +5 -1
- package/src/components/main/ThinkingIndicator.tsx +11 -1
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +3 -5
- package/src/utils/approvalBridge.ts +29 -8
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +5 -1
- package/src/utils/diffRendering.tsx +13 -14
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/models.ts +0 -7
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/toolFormatting.ts +162 -43
- package/src/web/app.tsx +94 -34
- 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 +6 -6
- package/src/web/components/MessageItem.tsx +88 -89
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -1
- package/src/web/components/ThinkingIndicator.tsx +40 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +187 -39
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
|
@@ -7,9 +7,180 @@ import { shouldRequireApprovals } from '../../utils/config';
|
|
|
7
7
|
import { generateDiff, formatDiffForDisplay } from '../../utils/diff';
|
|
8
8
|
import { captureFileSnapshot } from '../../utils/undoRedo';
|
|
9
9
|
import { trackFileChange, trackFileCreated, trackFileDeleted } from '../../utils/fileChangeTracker';
|
|
10
|
+
import TurndownService from 'turndown';
|
|
11
|
+
import { Readability } from '@mozilla/readability';
|
|
12
|
+
import { parseHTML } from 'linkedom';
|
|
10
13
|
|
|
11
14
|
const execAsync = promisify(exec);
|
|
12
15
|
|
|
16
|
+
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0';
|
|
17
|
+
const DEFAULT_FETCH_MAX_LENGTH = 10000;
|
|
18
|
+
const DEFAULT_FETCH_TIMEOUT = 30000;
|
|
19
|
+
|
|
20
|
+
function extractContentFromHtml(html: string, url: string): { content: string; title: string | null; isSPA: boolean } {
|
|
21
|
+
const { document } = parseHTML(html);
|
|
22
|
+
|
|
23
|
+
const turndown = new TurndownService({
|
|
24
|
+
headingStyle: 'atx',
|
|
25
|
+
codeBlockStyle: 'fenced',
|
|
26
|
+
emDelimiter: '*',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
turndown.addRule('removeScripts', {
|
|
30
|
+
filter: ['script', 'style', 'noscript'],
|
|
31
|
+
replacement: () => '',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
turndown.addRule('preserveLinks', {
|
|
35
|
+
filter: 'a',
|
|
36
|
+
replacement: (content, node) => {
|
|
37
|
+
const element = node as HTMLAnchorElement;
|
|
38
|
+
const href = element.getAttribute('href');
|
|
39
|
+
if (!href || href.startsWith('#')) return content;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const absoluteUrl = new URL(href, url).toString();
|
|
43
|
+
return `[${content}](${absoluteUrl})`;
|
|
44
|
+
} catch {
|
|
45
|
+
return `[${content}](${href})`;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
turndown.addRule('preserveImages', {
|
|
51
|
+
filter: 'img',
|
|
52
|
+
replacement: (_content, node) => {
|
|
53
|
+
const element = node as HTMLImageElement;
|
|
54
|
+
const src = element.getAttribute('src');
|
|
55
|
+
const alt = element.getAttribute('alt') || '';
|
|
56
|
+
if (!src) return '';
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const absoluteUrl = new URL(src, url).toString();
|
|
60
|
+
return ``;
|
|
61
|
+
} catch {
|
|
62
|
+
return ``;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const reader = new Readability(document as unknown as Document, {
|
|
68
|
+
charThreshold: 0,
|
|
69
|
+
});
|
|
70
|
+
const article = reader.parse();
|
|
71
|
+
|
|
72
|
+
if (article && article.content) {
|
|
73
|
+
const content = turndown.turndown(article.content).trim();
|
|
74
|
+
if (content.length > 50) {
|
|
75
|
+
return {
|
|
76
|
+
content,
|
|
77
|
+
title: article.title || document.title || null,
|
|
78
|
+
isSPA: false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
84
|
+
const bodyContent = bodyMatch ? bodyMatch[1] : html;
|
|
85
|
+
const markdownContent = turndown.turndown(bodyContent || '').trim();
|
|
86
|
+
|
|
87
|
+
if (markdownContent.length > 50) {
|
|
88
|
+
return {
|
|
89
|
+
content: markdownContent,
|
|
90
|
+
title: document.title || null,
|
|
91
|
+
isSPA: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isSPA = html.includes('id="root"') ||
|
|
96
|
+
html.includes('id="app"') ||
|
|
97
|
+
html.includes('id="__next"') ||
|
|
98
|
+
html.includes('data-reactroot') ||
|
|
99
|
+
html.includes('ng-app');
|
|
100
|
+
|
|
101
|
+
const metaTags: string[] = [];
|
|
102
|
+
const metaDescription = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
|
|
103
|
+
const metaOgTitle = html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["']/i);
|
|
104
|
+
const metaOgDescription = html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']+)["']/i);
|
|
105
|
+
|
|
106
|
+
if (metaOgTitle) metaTags.push(`**Title:** ${metaOgTitle[1]}`);
|
|
107
|
+
if (metaDescription) metaTags.push(`**Description:** ${metaDescription[1]}`);
|
|
108
|
+
if (metaOgDescription && metaOgDescription[1] !== metaDescription?.[1]) {
|
|
109
|
+
metaTags.push(`**OG Description:** ${metaOgDescription[1]}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let content = '';
|
|
113
|
+
if (isSPA) {
|
|
114
|
+
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`;
|
|
115
|
+
if (metaTags.length > 0) {
|
|
116
|
+
content += `**Available metadata:**\n${metaTags.join('\n')}\n\n`;
|
|
117
|
+
}
|
|
118
|
+
content += `*To see the actual content, you would need a headless browser. Try using raw=true to see the HTML source.*`;
|
|
119
|
+
} else if (markdownContent) {
|
|
120
|
+
content = markdownContent;
|
|
121
|
+
} else {
|
|
122
|
+
content = `*No readable content could be extracted from this page.*\n\n`;
|
|
123
|
+
if (metaTags.length > 0) {
|
|
124
|
+
content += `**Available metadata:**\n${metaTags.join('\n')}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
content,
|
|
130
|
+
title: document.title || null,
|
|
131
|
+
isSPA,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function fetchUrlContent(
|
|
136
|
+
url: string,
|
|
137
|
+
options: {
|
|
138
|
+
raw?: boolean;
|
|
139
|
+
timeout?: number;
|
|
140
|
+
userAgent?: string;
|
|
141
|
+
} = {}
|
|
142
|
+
): Promise<{ content: string; contentType: string; title: string | null; status: number; statusText: string; isSPA?: boolean }> {
|
|
143
|
+
const { raw = false, timeout = DEFAULT_FETCH_TIMEOUT, userAgent = DEFAULT_USER_AGENT } = options;
|
|
144
|
+
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await globalThis.fetch(url, {
|
|
150
|
+
headers: {
|
|
151
|
+
'User-Agent': userAgent,
|
|
152
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
153
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
154
|
+
},
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
redirect: 'follow',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const status = response.status;
|
|
160
|
+
const statusText = response.statusText;
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`HTTP ${status} ${statusText}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const contentType = response.headers.get('content-type') || '';
|
|
167
|
+
const text = await response.text();
|
|
168
|
+
|
|
169
|
+
const isHtml = contentType.includes('text/html') ||
|
|
170
|
+
text.slice(0, 500).toLowerCase().includes('<html') ||
|
|
171
|
+
text.slice(0, 500).toLowerCase().includes('<!doctype html');
|
|
172
|
+
|
|
173
|
+
if (isHtml && !raw) {
|
|
174
|
+
const { content, title, isSPA } = extractContentFromHtml(text, url);
|
|
175
|
+
return { content, contentType, title, isSPA, status, statusText };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { content: text, contentType, title: null, status, statusText };
|
|
179
|
+
} finally {
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
13
184
|
export interface ToolResult {
|
|
14
185
|
success: boolean;
|
|
15
186
|
result?: string;
|
|
@@ -21,6 +192,7 @@ export interface ToolResult {
|
|
|
21
192
|
const pathValidationCache = new Map<string, boolean>();
|
|
22
193
|
const globPatternCache = new Map<string, RegExp>();
|
|
23
194
|
|
|
195
|
+
|
|
24
196
|
function validatePath(fullPath: string, workspace: string): boolean {
|
|
25
197
|
const cacheKey = `${fullPath}|${workspace}`;
|
|
26
198
|
const cached = pathValidationCache.get(cacheKey);
|
|
@@ -69,14 +241,14 @@ function matchGlob(filename: string, pattern: string): boolean {
|
|
|
69
241
|
if (!regex) {
|
|
70
242
|
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
71
243
|
|
|
72
|
-
let regexPattern = normalizedPattern.replace(/[.+^${}()|[\]
|
|
244
|
+
let regexPattern = normalizedPattern.replace(/[.+^${}()|[\]\\*?]/g, '\\$&');
|
|
73
245
|
|
|
74
246
|
regexPattern = regexPattern
|
|
75
|
-
.replace(
|
|
76
|
-
.replace(
|
|
77
|
-
.replace(
|
|
78
|
-
.replace(
|
|
79
|
-
.replace(
|
|
247
|
+
.replace(/\\\*\\\*\\\//g, '(?:(?:[^/]+/)*)')
|
|
248
|
+
.replace(/\\\/\*\\\*$/g, '(?:/.*)?')
|
|
249
|
+
.replace(/\\\*\\\*/g, '.*')
|
|
250
|
+
.replace(/\\\*/g, '[^/]*')
|
|
251
|
+
.replace(/\\\?/g, '[^/]');
|
|
80
252
|
|
|
81
253
|
regex = new RegExp(`^${regexPattern}$`, 'i');
|
|
82
254
|
globPatternCache.set(pattern, regex);
|
|
@@ -91,32 +263,199 @@ function matchGlob(filename: string, pattern: string): boolean {
|
|
|
91
263
|
return regex.test(normalizedFilename);
|
|
92
264
|
}
|
|
93
265
|
|
|
94
|
-
|
|
266
|
+
interface SearchResult {
|
|
267
|
+
matches: Array<{ line: number; content: string; context?: { before: string[]; after: string[] } }>;
|
|
268
|
+
error?: string;
|
|
269
|
+
matchCount?: number;
|
|
270
|
+
skipped?: boolean;
|
|
271
|
+
skipReason?: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
interface SearchOptions {
|
|
275
|
+
caseSensitive: boolean;
|
|
276
|
+
isRegex: boolean;
|
|
277
|
+
wholeWord: boolean;
|
|
278
|
+
multiline: boolean;
|
|
279
|
+
contextBefore: number;
|
|
280
|
+
contextAfter: number;
|
|
281
|
+
maxFileSize: number;
|
|
282
|
+
invertMatch: boolean;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
|
|
286
|
+
|
|
287
|
+
function isValidRegex(pattern: string): { valid: boolean; error?: string } {
|
|
95
288
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
289
|
+
new RegExp(pattern);
|
|
290
|
+
return { valid: true };
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return { valid: false, error: e instanceof Error ? e.message : 'Invalid regular expression' };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isBinaryFile(buffer: Buffer, bytesToCheck = 8000): boolean {
|
|
297
|
+
const checkLength = Math.min(buffer.length, bytesToCheck);
|
|
298
|
+
let nullCount = 0;
|
|
299
|
+
let controlCount = 0;
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i < checkLength; i++) {
|
|
302
|
+
const byte = buffer[i];
|
|
303
|
+
if (byte === 0) {
|
|
304
|
+
nullCount++;
|
|
305
|
+
if (nullCount > 1) return true;
|
|
306
|
+
}
|
|
307
|
+
if (byte !== undefined && byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
|
|
308
|
+
controlCount++;
|
|
309
|
+
if (controlCount > checkLength * 0.1) return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function escapeRegexForLiteral(str: string): string {
|
|
317
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildSearchRegex(query: string, options: SearchOptions): { regex: RegExp; error?: undefined } | { regex?: undefined; error: string } {
|
|
321
|
+
try {
|
|
322
|
+
let pattern = query;
|
|
323
|
+
|
|
324
|
+
if (!options.isRegex) {
|
|
325
|
+
pattern = escapeRegexForLiteral(query);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options.wholeWord) {
|
|
329
|
+
if (options.isRegex) {
|
|
330
|
+
pattern = `(?:^|\\b)${pattern}(?:\\b|$)`;
|
|
331
|
+
} else {
|
|
332
|
+
pattern = `\\b${pattern}\\b`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let flags = 'g';
|
|
337
|
+
if (!options.caseSensitive) flags += 'i';
|
|
338
|
+
if (options.multiline) flags += 'm';
|
|
339
|
+
|
|
340
|
+
return { regex: new RegExp(pattern, flags) };
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return { error: e instanceof Error ? e.message : 'Invalid pattern' };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function searchInFile(filePath: string, query: string, options: SearchOptions): Promise<SearchResult> {
|
|
347
|
+
try {
|
|
348
|
+
const stats = await stat(filePath);
|
|
349
|
+
|
|
350
|
+
if (stats.size > options.maxFileSize) {
|
|
351
|
+
return {
|
|
352
|
+
matches: [],
|
|
353
|
+
skipped: true,
|
|
354
|
+
skipReason: `File too large (${Math.round(stats.size / 1024)}KB > ${Math.round(options.maxFileSize / 1024)}KB)`
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const buffer = await readFile(filePath);
|
|
99
359
|
|
|
100
|
-
|
|
101
|
-
|
|
360
|
+
if (isBinaryFile(buffer)) {
|
|
361
|
+
return {
|
|
362
|
+
matches: [],
|
|
363
|
+
skipped: true,
|
|
364
|
+
skipReason: 'Binary file'
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const content = buffer.toString('utf-8');
|
|
369
|
+
const lines = content.split('\n');
|
|
370
|
+
|
|
371
|
+
const regexResult = buildSearchRegex(query, options);
|
|
372
|
+
if (regexResult.error || !regexResult.regex) {
|
|
373
|
+
return { matches: [], error: regexResult.error ?? 'Failed to build search pattern' };
|
|
374
|
+
}
|
|
375
|
+
const regex: RegExp = regexResult.regex;
|
|
376
|
+
|
|
377
|
+
if (options.invertMatch) {
|
|
378
|
+
const hasMatch = lines.some(line => regex.test(line));
|
|
379
|
+
return {
|
|
380
|
+
matches: [],
|
|
381
|
+
matchCount: hasMatch ? 0 : 1,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
102
384
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
385
|
+
if (options.multiline && options.isRegex) {
|
|
386
|
+
const multilineMatches: Array<{ line: number; content: string }> = [];
|
|
387
|
+
let match;
|
|
388
|
+
regex.lastIndex = 0;
|
|
107
389
|
|
|
108
|
-
|
|
109
|
-
|
|
390
|
+
while ((match = regex.exec(content)) !== null) {
|
|
391
|
+
const matchStart = match.index;
|
|
392
|
+
let lineNumber = 1;
|
|
393
|
+
for (let i = 0; i < matchStart; i++) {
|
|
394
|
+
if (content[i] === '\n') lineNumber++;
|
|
110
395
|
}
|
|
111
396
|
|
|
112
|
-
|
|
113
|
-
|
|
397
|
+
const matchedText = match[0];
|
|
398
|
+
const matchLines = matchedText.split('\n');
|
|
399
|
+
|
|
400
|
+
multilineMatches.push({
|
|
401
|
+
line: lineNumber,
|
|
402
|
+
content: matchLines.length > 1
|
|
403
|
+
? `${matchLines[0]}... (+${matchLines.length - 1} lines)`
|
|
404
|
+
: matchedText.slice(0, 200)
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (regex.lastIndex === match.index) {
|
|
408
|
+
regex.lastIndex++;
|
|
409
|
+
}
|
|
114
410
|
}
|
|
411
|
+
|
|
412
|
+
return { matches: multilineMatches, matchCount: multilineMatches.length };
|
|
115
413
|
}
|
|
116
414
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
415
|
+
const matches: Array<{ line: number; content: string; context?: { before: string[]; after: string[] } }> = [];
|
|
416
|
+
let matchCount = 0;
|
|
417
|
+
|
|
418
|
+
for (let i = 0; i < lines.length; i++) {
|
|
419
|
+
const line = lines[i];
|
|
420
|
+
if (line === undefined) continue;
|
|
421
|
+
|
|
422
|
+
regex.lastIndex = 0;
|
|
423
|
+
if (regex.test(line)) {
|
|
424
|
+
matchCount++;
|
|
425
|
+
|
|
426
|
+
const contextBefore: string[] = [];
|
|
427
|
+
const contextAfter: string[] = [];
|
|
428
|
+
|
|
429
|
+
if (options.contextBefore > 0) {
|
|
430
|
+
for (let j = Math.max(0, i - options.contextBefore); j < i; j++) {
|
|
431
|
+
const ctxLine = lines[j];
|
|
432
|
+
if (ctxLine !== undefined) contextBefore.push(ctxLine);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (options.contextAfter > 0) {
|
|
437
|
+
for (let j = i + 1; j <= Math.min(lines.length - 1, i + options.contextAfter); j++) {
|
|
438
|
+
const ctxLine = lines[j];
|
|
439
|
+
if (ctxLine !== undefined) contextAfter.push(ctxLine);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const hasContext = contextBefore.length > 0 || contextAfter.length > 0;
|
|
444
|
+
|
|
445
|
+
matches.push({
|
|
446
|
+
line: i + 1,
|
|
447
|
+
content: line,
|
|
448
|
+
...(hasContext && { context: { before: contextBefore, after: contextAfter } })
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { matches, matchCount };
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
matches: [],
|
|
457
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
458
|
+
};
|
|
120
459
|
}
|
|
121
460
|
}
|
|
122
461
|
|
|
@@ -380,6 +719,8 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
380
719
|
switch (toolName) {
|
|
381
720
|
case 'read': {
|
|
382
721
|
const path = args.path as string;
|
|
722
|
+
const startLine = args.start_line as number | undefined;
|
|
723
|
+
const endLine = args.end_line as number | undefined;
|
|
383
724
|
const fullPath = resolve(workspace, path);
|
|
384
725
|
|
|
385
726
|
if (!validatePath(fullPath, workspace)) {
|
|
@@ -390,6 +731,26 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
390
731
|
}
|
|
391
732
|
|
|
392
733
|
const content = await readFile(fullPath, 'utf-8');
|
|
734
|
+
|
|
735
|
+
if (startLine !== undefined || endLine !== undefined) {
|
|
736
|
+
const lines = content.split('\n');
|
|
737
|
+
const start = (startLine ?? 1) - 1;
|
|
738
|
+
const end = endLine ?? lines.length;
|
|
739
|
+
|
|
740
|
+
if (start < 0 || start >= lines.length) {
|
|
741
|
+
return {
|
|
742
|
+
success: false,
|
|
743
|
+
error: `Start line ${startLine} is out of bounds (1-${lines.length})`
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const selectedLines = lines.slice(start, end);
|
|
748
|
+
return {
|
|
749
|
+
success: true,
|
|
750
|
+
result: selectedLines.join('\n')
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
393
754
|
return {
|
|
394
755
|
success: true,
|
|
395
756
|
result: content
|
|
@@ -398,7 +759,8 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
398
759
|
|
|
399
760
|
case 'write': {
|
|
400
761
|
const path = args.path as string;
|
|
401
|
-
|
|
762
|
+
let content = typeof args.content === 'string' ? args.content : '';
|
|
763
|
+
if (content) content = content.trimEnd(); // Ensure no trailing empty lines
|
|
402
764
|
const append = args.append === true;
|
|
403
765
|
const fullPath = resolve(workspace, path);
|
|
404
766
|
|
|
@@ -495,7 +857,11 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
495
857
|
}
|
|
496
858
|
|
|
497
859
|
if (filter) {
|
|
498
|
-
const
|
|
860
|
+
const escapedFilter = filter
|
|
861
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
862
|
+
.replace(/\*/g, '.*')
|
|
863
|
+
.replace(/\?/g, '.');
|
|
864
|
+
const regex = new RegExp(`^${escapedFilter}$`, 'i');
|
|
499
865
|
filteredEntries = filteredEntries.filter(entry => regex.test(entry.name));
|
|
500
866
|
}
|
|
501
867
|
|
|
@@ -583,11 +949,26 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
583
949
|
}
|
|
584
950
|
|
|
585
951
|
case 'grep': {
|
|
586
|
-
const
|
|
952
|
+
const { FILE_TYPE_EXTENSIONS } = await import('./grep.ts');
|
|
953
|
+
|
|
954
|
+
const pattern = args.pattern === null ? undefined : (args.pattern as string | undefined);
|
|
955
|
+
const fileType = args.file_type === null ? undefined : (args.file_type as string | undefined);
|
|
587
956
|
const query = args.query as string;
|
|
588
957
|
const searchPath = (args.path === null ? undefined : (args.path as string | undefined)) || '.';
|
|
589
958
|
const caseSensitive = ((args.case_sensitive === null ? undefined : (args.case_sensitive as boolean | undefined)) ?? false);
|
|
590
|
-
const
|
|
959
|
+
const isRegex = ((args.regex === null ? undefined : (args.regex as boolean | undefined)) ?? false);
|
|
960
|
+
const wholeWord = ((args.whole_word === null ? undefined : (args.whole_word as boolean | undefined)) ?? false);
|
|
961
|
+
const multiline = ((args.multiline === null ? undefined : (args.multiline as boolean | undefined)) ?? false);
|
|
962
|
+
const context = ((args.context === null ? undefined : (args.context as number | undefined)) ?? 0);
|
|
963
|
+
const contextBefore = ((args.context_before === null ? undefined : (args.context_before as number | undefined)) ?? context);
|
|
964
|
+
const contextAfter = ((args.context_after === null ? undefined : (args.context_after as number | undefined)) ?? context);
|
|
965
|
+
const maxResults = ((args.max_results === null ? undefined : (args.max_results as number | undefined)) ?? 500);
|
|
966
|
+
const maxFileSize = ((args.max_file_size === null ? undefined : (args.max_file_size as number | undefined)) ?? DEFAULT_MAX_FILE_SIZE);
|
|
967
|
+
const includeHidden = ((args.include_hidden === null ? undefined : (args.include_hidden as boolean | undefined)) ?? false);
|
|
968
|
+
const excludePattern = args.exclude_pattern === null ? undefined : (args.exclude_pattern as string | undefined);
|
|
969
|
+
const outputMode = ((args.output_mode === null ? undefined : (args.output_mode as string | undefined)) ?? 'matches') as 'matches' | 'files' | 'count';
|
|
970
|
+
const invertMatch = ((args.invert_match === null ? undefined : (args.invert_match as boolean | undefined)) ?? false);
|
|
971
|
+
|
|
591
972
|
const fullPath = resolve(workspace, searchPath);
|
|
592
973
|
|
|
593
974
|
if (!validatePath(fullPath, workspace)) {
|
|
@@ -597,46 +978,185 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
597
978
|
};
|
|
598
979
|
}
|
|
599
980
|
|
|
600
|
-
const
|
|
981
|
+
const testSearchOptions: SearchOptions = {
|
|
982
|
+
caseSensitive,
|
|
983
|
+
isRegex,
|
|
984
|
+
wholeWord,
|
|
985
|
+
multiline,
|
|
986
|
+
contextBefore: 0,
|
|
987
|
+
contextAfter: 0,
|
|
988
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
989
|
+
invertMatch: false,
|
|
990
|
+
};
|
|
991
|
+
const regexTest = buildSearchRegex(query, testSearchOptions);
|
|
992
|
+
if (regexTest.error) {
|
|
993
|
+
return {
|
|
994
|
+
success: false,
|
|
995
|
+
error: `Invalid search pattern: ${regexTest.error}`
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
let finalPattern: string;
|
|
1000
|
+
if (pattern) {
|
|
1001
|
+
finalPattern = pattern.includes('**') ? pattern : `**/${pattern}`;
|
|
1002
|
+
} else if (fileType) {
|
|
1003
|
+
const extensions = FILE_TYPE_EXTENSIONS[fileType.toLowerCase()];
|
|
1004
|
+
if (!extensions) {
|
|
1005
|
+
return {
|
|
1006
|
+
success: false,
|
|
1007
|
+
error: `Unknown file type: ${fileType}. Available types: ${Object.keys(FILE_TYPE_EXTENSIONS).join(', ')}`
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
if (extensions.length === 1) {
|
|
1011
|
+
finalPattern = `**/*${extensions[0]}`;
|
|
1012
|
+
} else {
|
|
1013
|
+
finalPattern = '**/*';
|
|
1014
|
+
}
|
|
1015
|
+
} else {
|
|
1016
|
+
finalPattern = '**/*';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
let allFiles = await findFilesByPattern(finalPattern, fullPath);
|
|
1020
|
+
|
|
1021
|
+
if (!includeHidden) {
|
|
1022
|
+
allFiles = allFiles.filter(f => !f.split('/').some(part => part.startsWith('.')));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (fileType && !pattern) {
|
|
1026
|
+
const extensions = FILE_TYPE_EXTENSIONS[fileType.toLowerCase()];
|
|
1027
|
+
if (extensions) {
|
|
1028
|
+
allFiles = allFiles.filter(f => extensions.some(ext => f.toLowerCase().endsWith(ext)));
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
601
1031
|
|
|
602
|
-
|
|
1032
|
+
if (excludePattern) {
|
|
1033
|
+
allFiles = allFiles.filter(f => !matchGlob(f, excludePattern));
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const searchOptions: SearchOptions = {
|
|
1037
|
+
caseSensitive,
|
|
1038
|
+
isRegex,
|
|
1039
|
+
wholeWord,
|
|
1040
|
+
multiline,
|
|
1041
|
+
contextBefore,
|
|
1042
|
+
contextAfter,
|
|
1043
|
+
maxFileSize,
|
|
1044
|
+
invertMatch,
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
type MatchType = { line: number; content: string; context?: { before: string[]; after: string[] } };
|
|
1048
|
+
const results: Array<{ file: string; matches: MatchType[]; count?: number }> = [];
|
|
1049
|
+
const skippedFiles: Array<{ file: string; reason: string }> = [];
|
|
603
1050
|
let totalResults = 0;
|
|
1051
|
+
let totalMatchCount = 0;
|
|
604
1052
|
|
|
605
|
-
const BATCH_SIZE =
|
|
606
|
-
for (let i = 0; i <
|
|
607
|
-
if (totalResults >= maxResults) break;
|
|
1053
|
+
const BATCH_SIZE = 15;
|
|
1054
|
+
for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
|
|
1055
|
+
if (!invertMatch && totalResults >= maxResults) break;
|
|
608
1056
|
|
|
609
|
-
const batch =
|
|
1057
|
+
const batch = allFiles.slice(i, i + BATCH_SIZE);
|
|
610
1058
|
const batchResults = await Promise.all(
|
|
611
1059
|
batch.map(async (file) => {
|
|
612
1060
|
const filePath = resolve(fullPath, file);
|
|
613
|
-
const
|
|
614
|
-
return {
|
|
1061
|
+
const searchResult = await searchInFile(filePath, query, searchOptions);
|
|
1062
|
+
return {
|
|
1063
|
+
file: join(searchPath, file),
|
|
1064
|
+
matches: searchResult.matches,
|
|
1065
|
+
matchCount: searchResult.matchCount ?? searchResult.matches.length,
|
|
1066
|
+
skipped: searchResult.skipped,
|
|
1067
|
+
skipReason: searchResult.skipReason,
|
|
1068
|
+
};
|
|
615
1069
|
})
|
|
616
1070
|
);
|
|
617
1071
|
|
|
618
|
-
for (const { file, matches } of batchResults) {
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1072
|
+
for (const { file, matches, matchCount, skipped, skipReason } of batchResults) {
|
|
1073
|
+
if (skipped && skipReason) {
|
|
1074
|
+
skippedFiles.push({ file, reason: skipReason });
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (invertMatch) {
|
|
1079
|
+
if (matchCount === 1) {
|
|
1080
|
+
results.push({ file, matches: [], count: 1 });
|
|
1081
|
+
totalResults++;
|
|
1082
|
+
}
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (matches.length > 0 || matchCount > 0) {
|
|
1087
|
+
totalMatchCount += matchCount;
|
|
1088
|
+
|
|
1089
|
+
if (outputMode === 'files') {
|
|
1090
|
+
results.push({ file, matches: [] });
|
|
1091
|
+
totalResults++;
|
|
1092
|
+
} else if (outputMode === 'count') {
|
|
1093
|
+
results.push({ file, matches: [], count: matchCount });
|
|
1094
|
+
totalResults++;
|
|
1095
|
+
} else {
|
|
1096
|
+
const remainingSlots = maxResults - totalResults;
|
|
1097
|
+
const matchesToInclude = matches.slice(0, remainingSlots);
|
|
1098
|
+
results.push({ file, matches: matchesToInclude, count: matchCount });
|
|
1099
|
+
totalResults += matchesToInclude.length;
|
|
1100
|
+
}
|
|
626
1101
|
}
|
|
627
1102
|
}
|
|
628
1103
|
}
|
|
629
1104
|
|
|
1105
|
+
let formattedResult: string;
|
|
1106
|
+
|
|
1107
|
+
if (outputMode === 'files') {
|
|
1108
|
+
const filesOnly = results.map(r => r.file);
|
|
1109
|
+
const summary = {
|
|
1110
|
+
files_found: filesOnly.length,
|
|
1111
|
+
files: filesOnly,
|
|
1112
|
+
...(skippedFiles.length > 0 && { skipped: skippedFiles.length })
|
|
1113
|
+
};
|
|
1114
|
+
formattedResult = JSON.stringify(summary, null, 2);
|
|
1115
|
+
} else if (outputMode === 'count') {
|
|
1116
|
+
const counts = results.map(r => ({ file: r.file, count: r.count ?? 0 }));
|
|
1117
|
+
const summary = {
|
|
1118
|
+
total_matches: totalMatchCount,
|
|
1119
|
+
files_with_matches: counts.length,
|
|
1120
|
+
counts,
|
|
1121
|
+
...(skippedFiles.length > 0 && { skipped: skippedFiles.length })
|
|
1122
|
+
};
|
|
1123
|
+
formattedResult = JSON.stringify(summary, null, 2);
|
|
1124
|
+
} else {
|
|
1125
|
+
const summary = {
|
|
1126
|
+
total_matches: totalMatchCount,
|
|
1127
|
+
files_searched: allFiles.length,
|
|
1128
|
+
files_with_matches: results.length,
|
|
1129
|
+
...(skippedFiles.length > 0 && { skipped_files: skippedFiles.length }),
|
|
1130
|
+
...(totalResults >= maxResults && { truncated: true, max_results: maxResults }),
|
|
1131
|
+
results: results.map(r => ({
|
|
1132
|
+
file: r.file,
|
|
1133
|
+
match_count: r.count ?? r.matches.length,
|
|
1134
|
+
matches: r.matches.map(m => {
|
|
1135
|
+
if (m.context && (m.context.before.length > 0 || m.context.after.length > 0)) {
|
|
1136
|
+
return {
|
|
1137
|
+
line: m.line,
|
|
1138
|
+
content: m.content,
|
|
1139
|
+
context: m.context
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
return { line: m.line, content: m.content };
|
|
1143
|
+
})
|
|
1144
|
+
}))
|
|
1145
|
+
};
|
|
1146
|
+
formattedResult = JSON.stringify(summary, null, 2);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
630
1149
|
return {
|
|
631
1150
|
success: true,
|
|
632
|
-
result:
|
|
1151
|
+
result: formattedResult
|
|
633
1152
|
};
|
|
634
1153
|
}
|
|
635
1154
|
|
|
636
1155
|
case 'edit': {
|
|
637
1156
|
const path = args.path as string;
|
|
638
1157
|
const oldContent = args.old_content as string;
|
|
639
|
-
|
|
1158
|
+
let newContent = args.new_content as string;
|
|
1159
|
+
if (newContent) newContent = newContent.trimEnd(); // Ensure no trailing empty lines
|
|
640
1160
|
const occurrence = ((args.occurrence === null ? undefined : (args.occurrence as number | undefined)) ?? 1);
|
|
641
1161
|
const fullPath = resolve(workspace, path);
|
|
642
1162
|
|
|
@@ -735,6 +1255,95 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
735
1255
|
};
|
|
736
1256
|
}
|
|
737
1257
|
|
|
1258
|
+
case 'fetch': {
|
|
1259
|
+
const url = args.url as string;
|
|
1260
|
+
const maxLength = (args.max_length as number | undefined) ?? DEFAULT_FETCH_MAX_LENGTH;
|
|
1261
|
+
const startIndex = (args.start_index as number | undefined) ?? 0;
|
|
1262
|
+
const raw = (args.raw as boolean | undefined) ?? false;
|
|
1263
|
+
const timeout = (args.timeout as number | undefined) ?? DEFAULT_FETCH_TIMEOUT;
|
|
1264
|
+
|
|
1265
|
+
try {
|
|
1266
|
+
new URL(url);
|
|
1267
|
+
} catch {
|
|
1268
|
+
return {
|
|
1269
|
+
success: false,
|
|
1270
|
+
error: `Invalid URL: ${url}`,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
try {
|
|
1275
|
+
let fetchResult = await fetchUrlContent(url, { raw, timeout });
|
|
1276
|
+
let { content, contentType, title, isSPA, status, statusText } = fetchResult;
|
|
1277
|
+
|
|
1278
|
+
if (isSPA && !raw) {
|
|
1279
|
+
const rawResult = await fetchUrlContent(url, { raw: true, timeout });
|
|
1280
|
+
content = rawResult.content;
|
|
1281
|
+
contentType = rawResult.contentType;
|
|
1282
|
+
title = rawResult.title;
|
|
1283
|
+
status = rawResult.status;
|
|
1284
|
+
statusText = rawResult.statusText;
|
|
1285
|
+
isSPA = false;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const totalLength = content.length;
|
|
1289
|
+
|
|
1290
|
+
if (startIndex >= totalLength) {
|
|
1291
|
+
return {
|
|
1292
|
+
success: false,
|
|
1293
|
+
error: `Start index ${startIndex} exceeds content length ${totalLength}`,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const extractedContent = content.slice(startIndex, startIndex + maxLength);
|
|
1298
|
+
const truncated = startIndex + maxLength < totalLength;
|
|
1299
|
+
const nextStartIndex = truncated ? startIndex + maxLength : undefined;
|
|
1300
|
+
|
|
1301
|
+
const parts: string[] = [];
|
|
1302
|
+
|
|
1303
|
+
if (title) {
|
|
1304
|
+
parts.push(`# ${title}\n`);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
parts.push(`**URL:** ${url}`);
|
|
1308
|
+
parts.push(`**Status:** ${status} ${statusText}`);
|
|
1309
|
+
parts.push(`**Content-Type:** ${contentType}`);
|
|
1310
|
+
parts.push(`**Length:** ${extractedContent.length} / ${totalLength} characters`);
|
|
1311
|
+
|
|
1312
|
+
if (fetchResult.isSPA) {
|
|
1313
|
+
parts.push(`**Note:** SPA detected (React/Vue/Angular). Showing raw HTML source.`);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (truncated && nextStartIndex !== undefined) {
|
|
1317
|
+
parts.push(`**Status:** Content truncated. Use start_index=${nextStartIndex} to continue reading.`);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
parts.push('\n---\n');
|
|
1321
|
+
parts.push(extractedContent);
|
|
1322
|
+
|
|
1323
|
+
if (truncated && nextStartIndex !== undefined) {
|
|
1324
|
+
parts.push(`\n\n---\n*Content truncated at ${extractedContent.length} characters. Call fetch again with start_index=${nextStartIndex} to continue reading.*`);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
success: true,
|
|
1329
|
+
result: parts.join('\n'),
|
|
1330
|
+
};
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1333
|
+
|
|
1334
|
+
if (message.includes('abort')) {
|
|
1335
|
+
return {
|
|
1336
|
+
success: false,
|
|
1337
|
+
error: `Request timed out after ${timeout}ms`,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return {
|
|
1342
|
+
success: false,
|
|
1343
|
+
error: `Failed to fetch ${url}: ${message}`,
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
738
1347
|
|
|
739
1348
|
default:
|
|
740
1349
|
return {
|
|
@@ -748,4 +1357,4 @@ DO NOT continue without using the question tool. DO NOT ask in plain text.`;
|
|
|
748
1357
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
749
1358
|
};
|
|
750
1359
|
}
|
|
751
|
-
}
|
|
1360
|
+
}
|