@yeaft/webchat-agent 0.1.441 → 0.1.443
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude.js +41 -1
- package/package.json +1 -1
- package/unify/tools/agent.js +89 -0
- package/unify/tools/apply-patch.js +176 -0
- package/unify/tools/ask-user.js +62 -0
- package/unify/tools/bash.js +189 -0
- package/unify/tools/close-agent.js +58 -0
- package/unify/tools/file-edit.js +120 -0
- package/unify/tools/file-read.js +125 -0
- package/unify/tools/file-write.js +73 -0
- package/unify/tools/glob.js +143 -0
- package/unify/tools/grep.js +268 -0
- package/unify/tools/history-search.js +69 -0
- package/unify/tools/image-generation.js +97 -0
- package/unify/tools/index.js +92 -0
- package/unify/tools/js-repl.js +122 -0
- package/unify/tools/list-agents.js +56 -0
- package/unify/tools/list-dir.js +106 -0
- package/unify/tools/memory-read.js +91 -0
- package/unify/tools/memory-search.js +101 -0
- package/unify/tools/memory-write.js +114 -0
- package/unify/tools/notebook-edit.js +132 -0
- package/unify/tools/request-permissions.js +60 -0
- package/unify/tools/send-message.js +62 -0
- package/unify/tools/task-tools.js +358 -0
- package/unify/tools/tool-search.js +97 -0
- package/unify/tools/view-image.js +117 -0
- package/unify/tools/wait-agent.js +84 -0
- package/unify/tools/web-fetch.js +131 -0
- package/unify/tools/web-search.js +80 -0
- package/unify/tools/write-stdin.js +54 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-edit.js — Surgical string-replacement edits to files.
|
|
3
|
+
*
|
|
4
|
+
* Performs exact string matching and replacement within files,
|
|
5
|
+
* similar to Claude Code's Edit tool. Supports replace_all for
|
|
6
|
+
* bulk replacements across the file.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { defineTool } from './types.js';
|
|
10
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { resolve } from 'path';
|
|
13
|
+
|
|
14
|
+
export default defineTool({
|
|
15
|
+
name: 'FileEdit',
|
|
16
|
+
description: `Make surgical text replacements in an existing file.
|
|
17
|
+
|
|
18
|
+
Replaces exact occurrences of old_string with new_string.
|
|
19
|
+
The old_string must be unique in the file unless replace_all is true.
|
|
20
|
+
|
|
21
|
+
Guidelines:
|
|
22
|
+
- old_string must match EXACTLY (including whitespace and indentation)
|
|
23
|
+
- The edit fails if old_string is not found or is not unique
|
|
24
|
+
- Use replace_all: true to replace ALL occurrences
|
|
25
|
+
- For creating new files or full rewrites, use FileWrite instead
|
|
26
|
+
- Always read the file first to understand its current content`,
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
file_path: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Path to the file to edit (absolute or relative to cwd)',
|
|
33
|
+
},
|
|
34
|
+
old_string: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'The exact text to find and replace',
|
|
37
|
+
},
|
|
38
|
+
new_string: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'The replacement text',
|
|
41
|
+
},
|
|
42
|
+
replace_all: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'Replace all occurrences (default: false — fails if not unique)',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ['file_path', 'old_string', 'new_string'],
|
|
48
|
+
},
|
|
49
|
+
modes: ['work'],
|
|
50
|
+
isConcurrencySafe: () => false,
|
|
51
|
+
isReadOnly: () => false,
|
|
52
|
+
isDestructive: () => false,
|
|
53
|
+
async execute(input, ctx) {
|
|
54
|
+
const { file_path, old_string, new_string, replace_all = false } = input;
|
|
55
|
+
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
56
|
+
if (old_string === undefined) return JSON.stringify({ error: 'old_string is required' });
|
|
57
|
+
if (new_string === undefined) return JSON.stringify({ error: 'new_string is required' });
|
|
58
|
+
if (old_string === new_string) return JSON.stringify({ error: 'old_string and new_string are identical' });
|
|
59
|
+
|
|
60
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
61
|
+
const absPath = resolve(cwd, file_path);
|
|
62
|
+
|
|
63
|
+
if (!existsSync(absPath)) {
|
|
64
|
+
return JSON.stringify({ error: `File not found: ${absPath}` });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(absPath, 'utf-8');
|
|
69
|
+
|
|
70
|
+
// Count occurrences
|
|
71
|
+
let count = 0;
|
|
72
|
+
let idx = 0;
|
|
73
|
+
while (true) {
|
|
74
|
+
idx = content.indexOf(old_string, idx);
|
|
75
|
+
if (idx === -1) break;
|
|
76
|
+
count++;
|
|
77
|
+
idx += old_string.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (count === 0) {
|
|
81
|
+
// Provide context for debugging
|
|
82
|
+
const preview = old_string.length > 100
|
|
83
|
+
? old_string.slice(0, 100) + '...'
|
|
84
|
+
: old_string;
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
error: `old_string not found in file`,
|
|
87
|
+
hint: `The exact text "${preview}" was not found in ${absPath}. Check whitespace and indentation.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (count > 1 && !replace_all) {
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
error: `old_string found ${count} times — not unique. Use replace_all: true to replace all occurrences, or provide more context to make it unique.`,
|
|
94
|
+
occurrences: count,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Perform replacement
|
|
99
|
+
let newContent;
|
|
100
|
+
if (replace_all) {
|
|
101
|
+
newContent = content.split(old_string).join(new_string);
|
|
102
|
+
} else {
|
|
103
|
+
// Replace only the first occurrence (which is guaranteed unique)
|
|
104
|
+
const replaceIdx = content.indexOf(old_string);
|
|
105
|
+
newContent = content.slice(0, replaceIdx) + new_string + content.slice(replaceIdx + old_string.length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await writeFile(absPath, newContent, 'utf-8');
|
|
109
|
+
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
success: true,
|
|
112
|
+
path: absPath,
|
|
113
|
+
replacements: replace_all ? count : 1,
|
|
114
|
+
message: `Replaced ${replace_all ? count : 1} occurrence(s) in ${absPath}`,
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return JSON.stringify({ error: `Failed to edit file: ${err.message}` });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-read.js — Read file contents with line numbers.
|
|
3
|
+
*
|
|
4
|
+
* Reads text files with `cat -n` style line numbering, supports
|
|
5
|
+
* offset/limit for large files, and handles binary file detection.
|
|
6
|
+
*
|
|
7
|
+
* Modeled after Claude Code's Read tool.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineTool } from './types.js';
|
|
11
|
+
import { readFile, stat } from 'fs/promises';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { resolve, extname } from 'path';
|
|
14
|
+
|
|
15
|
+
/** Binary file extensions that shouldn't be read as text. */
|
|
16
|
+
const BINARY_EXTS = new Set([
|
|
17
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg',
|
|
18
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wav', '.flac',
|
|
19
|
+
'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
|
|
20
|
+
'.exe', '.dll', '.so', '.dylib', '.o',
|
|
21
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
22
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
23
|
+
'.sqlite', '.db',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Max file size to read (10 MB). */
|
|
27
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
/** Default number of lines to read. */
|
|
30
|
+
const DEFAULT_LIMIT = 2000;
|
|
31
|
+
|
|
32
|
+
export default defineTool({
|
|
33
|
+
name: 'FileRead',
|
|
34
|
+
description: `Read a file from the filesystem with line numbers.
|
|
35
|
+
|
|
36
|
+
Returns file contents with line numbers (like \`cat -n\`).
|
|
37
|
+
Supports offset and limit for reading specific portions of large files.
|
|
38
|
+
|
|
39
|
+
Guidelines:
|
|
40
|
+
- Use absolute paths when possible
|
|
41
|
+
- For large files, use offset and limit to read specific sections
|
|
42
|
+
- Binary files are detected by extension and rejected
|
|
43
|
+
- Maximum file size: 10MB
|
|
44
|
+
- Default limit: 2000 lines`,
|
|
45
|
+
parameters: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
file_path: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'Path to the file to read (absolute or relative to cwd)',
|
|
51
|
+
},
|
|
52
|
+
offset: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
description: 'Line number to start reading from (0-based, default: 0)',
|
|
55
|
+
},
|
|
56
|
+
limit: {
|
|
57
|
+
type: 'number',
|
|
58
|
+
description: `Maximum number of lines to read (default: ${DEFAULT_LIMIT})`,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
required: ['file_path'],
|
|
62
|
+
},
|
|
63
|
+
modes: ['work'],
|
|
64
|
+
isConcurrencySafe: () => true,
|
|
65
|
+
isReadOnly: () => true,
|
|
66
|
+
async execute(input, ctx) {
|
|
67
|
+
const { file_path, offset = 0, limit = DEFAULT_LIMIT } = input;
|
|
68
|
+
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
69
|
+
|
|
70
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
71
|
+
const absPath = resolve(cwd, file_path);
|
|
72
|
+
|
|
73
|
+
// Check existence
|
|
74
|
+
if (!existsSync(absPath)) {
|
|
75
|
+
return JSON.stringify({ error: `File not found: ${absPath}` });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check binary
|
|
79
|
+
const ext = extname(absPath).toLowerCase();
|
|
80
|
+
if (BINARY_EXTS.has(ext)) {
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
error: `Cannot read binary file: ${absPath}`,
|
|
83
|
+
hint: 'Use a specialized tool for binary files',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Check file size
|
|
89
|
+
const fileStat = await stat(absPath);
|
|
90
|
+
if (fileStat.isDirectory()) {
|
|
91
|
+
return JSON.stringify({ error: `Path is a directory: ${absPath}. Use ListDir instead.` });
|
|
92
|
+
}
|
|
93
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
94
|
+
return JSON.stringify({
|
|
95
|
+
error: `File too large: ${(fileStat.size / 1024 / 1024).toFixed(1)}MB (max: ${MAX_FILE_SIZE / 1024 / 1024}MB)`,
|
|
96
|
+
hint: 'Use offset and limit to read specific sections',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const content = await readFile(absPath, 'utf-8');
|
|
101
|
+
const allLines = content.split('\n');
|
|
102
|
+
const totalLines = allLines.length;
|
|
103
|
+
|
|
104
|
+
// Apply offset and limit
|
|
105
|
+
const startLine = Math.max(0, Math.min(offset, totalLines));
|
|
106
|
+
const endLine = Math.min(startLine + limit, totalLines);
|
|
107
|
+
const lines = allLines.slice(startLine, endLine);
|
|
108
|
+
|
|
109
|
+
// Format with line numbers (1-based like cat -n)
|
|
110
|
+
const numbered = lines.map((line, i) => {
|
|
111
|
+
const lineNum = startLine + i + 1;
|
|
112
|
+
return `${lineNum}\t${line}`;
|
|
113
|
+
}).join('\n');
|
|
114
|
+
|
|
115
|
+
// Add metadata if partial
|
|
116
|
+
if (startLine > 0 || endLine < totalLines) {
|
|
117
|
+
return `${numbered}\n\n[Showing lines ${startLine + 1}-${endLine} of ${totalLines} total]`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return numbered;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return JSON.stringify({ error: `Failed to read file: ${err.message}` });
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-write.js — Write or create files.
|
|
3
|
+
*
|
|
4
|
+
* Creates new files or overwrites existing ones. Creates parent
|
|
5
|
+
* directories as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
10
|
+
import { resolve, dirname } from 'path';
|
|
11
|
+
|
|
12
|
+
export default defineTool({
|
|
13
|
+
name: 'FileWrite',
|
|
14
|
+
description: `Write content to a file, creating it if it doesn't exist.
|
|
15
|
+
|
|
16
|
+
Creates parent directories automatically. Overwrites existing files.
|
|
17
|
+
|
|
18
|
+
Guidelines:
|
|
19
|
+
- Use absolute paths when possible
|
|
20
|
+
- For modifying existing files, prefer FileEdit (surgical edits) over FileWrite (full overwrite)
|
|
21
|
+
- Parent directories are created automatically
|
|
22
|
+
- Content should be the complete file content`,
|
|
23
|
+
parameters: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
file_path: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Path to the file to write (absolute or relative to cwd)',
|
|
29
|
+
},
|
|
30
|
+
content: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'The complete file content to write',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['file_path', 'content'],
|
|
36
|
+
},
|
|
37
|
+
modes: ['work'],
|
|
38
|
+
isConcurrencySafe: () => false,
|
|
39
|
+
isReadOnly: () => false,
|
|
40
|
+
isDestructive: () => false,
|
|
41
|
+
async execute(input, ctx) {
|
|
42
|
+
const { file_path, content } = input;
|
|
43
|
+
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
|
44
|
+
if (content === undefined || content === null) {
|
|
45
|
+
return JSON.stringify({ error: 'content is required' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
49
|
+
const absPath = resolve(cwd, file_path);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Ensure parent directory exists
|
|
53
|
+
const dir = dirname(absPath);
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Write the file
|
|
57
|
+
await writeFile(absPath, content, 'utf-8');
|
|
58
|
+
|
|
59
|
+
const lines = content.split('\n').length;
|
|
60
|
+
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
61
|
+
|
|
62
|
+
return JSON.stringify({
|
|
63
|
+
success: true,
|
|
64
|
+
path: absPath,
|
|
65
|
+
lines,
|
|
66
|
+
bytes,
|
|
67
|
+
message: `Wrote ${lines} lines (${bytes} bytes) to ${absPath}`,
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return JSON.stringify({ error: `Failed to write file: ${err.message}` });
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glob.js — Find files by pattern matching.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js glob patterns to find files matching a pattern.
|
|
5
|
+
* Results are sorted by modification time (newest first).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
import { readdir, stat } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { resolve, join, relative } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Simple glob pattern matcher (supports * and **).
|
|
15
|
+
* @param {string} pattern
|
|
16
|
+
* @param {string} str
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function matchGlob(pattern, str) {
|
|
20
|
+
// Convert glob pattern to regex
|
|
21
|
+
// IMPORTANT: escape dots FIRST before replacing glob chars to avoid
|
|
22
|
+
// corrupting regex tokens like [^/]* and .*
|
|
23
|
+
let regex = pattern
|
|
24
|
+
.replace(/\\/g, '/')
|
|
25
|
+
.replace(/\./g, '\\.') // Escape dots first (before glob replacements)
|
|
26
|
+
.replace(/\*\*/g, '<<<GLOBSTAR>>>')
|
|
27
|
+
.replace(/\*/g, '[^/]*')
|
|
28
|
+
.replace(/<<<GLOBSTAR>>>/g, '.*')
|
|
29
|
+
.replace(/\?/g, '[^/]');
|
|
30
|
+
regex = '^' + regex + '$';
|
|
31
|
+
return new RegExp(regex).test(str.replace(/\\/g, '/'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively walk a directory, yielding relative paths.
|
|
36
|
+
*/
|
|
37
|
+
async function* walkDir(dir, baseDir, maxDepth = 10, depth = 0) {
|
|
38
|
+
if (depth > maxDepth) return;
|
|
39
|
+
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Skip common large/irrelevant directories
|
|
48
|
+
const SKIP = new Set([
|
|
49
|
+
'node_modules', '.git', '__pycache__', '.next', '.nuxt',
|
|
50
|
+
'dist', 'build', '.cache', '.venv', 'venv', '.tox',
|
|
51
|
+
'vendor', 'target', '.gradle', '.idea', '.vscode',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const fullPath = join(dir, entry.name);
|
|
56
|
+
const relPath = relative(baseDir, fullPath);
|
|
57
|
+
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (SKIP.has(entry.name)) continue;
|
|
60
|
+
yield { path: relPath, isDir: true };
|
|
61
|
+
yield* walkDir(fullPath, baseDir, maxDepth, depth + 1);
|
|
62
|
+
} else {
|
|
63
|
+
yield { path: relPath, isDir: false };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default defineTool({
|
|
69
|
+
name: 'Glob',
|
|
70
|
+
description: `Find files matching a glob pattern.
|
|
71
|
+
|
|
72
|
+
Supports glob patterns like "**/*.js", "src/**/*.ts", "*.md".
|
|
73
|
+
Results are sorted by modification time (newest first).
|
|
74
|
+
|
|
75
|
+
Guidelines:
|
|
76
|
+
- Use "**/" for recursive directory matching
|
|
77
|
+
- Common directories (node_modules, .git, etc.) are skipped
|
|
78
|
+
- Returns file paths relative to the search directory
|
|
79
|
+
- Limited to 500 results by default`,
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
pattern: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Glob pattern to match files (e.g. "**/*.js")',
|
|
86
|
+
},
|
|
87
|
+
path: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Directory to search in (default: cwd)',
|
|
90
|
+
},
|
|
91
|
+
limit: {
|
|
92
|
+
type: 'number',
|
|
93
|
+
description: 'Maximum number of results (default: 500)',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['pattern'],
|
|
97
|
+
},
|
|
98
|
+
modes: ['work'],
|
|
99
|
+
isConcurrencySafe: () => true,
|
|
100
|
+
isReadOnly: () => true,
|
|
101
|
+
async execute(input, ctx) {
|
|
102
|
+
const { pattern, path: searchPath, limit = 500 } = input;
|
|
103
|
+
if (!pattern) return JSON.stringify({ error: 'pattern is required' });
|
|
104
|
+
|
|
105
|
+
const cwd = ctx?.cwd || process.cwd();
|
|
106
|
+
const baseDir = searchPath ? resolve(cwd, searchPath) : cwd;
|
|
107
|
+
|
|
108
|
+
if (!existsSync(baseDir)) {
|
|
109
|
+
return JSON.stringify({ error: `Directory not found: ${baseDir}` });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const matches = [];
|
|
114
|
+
|
|
115
|
+
for await (const entry of walkDir(baseDir, baseDir)) {
|
|
116
|
+
if (matches.length >= limit * 2) break; // over-fetch for sorting
|
|
117
|
+
|
|
118
|
+
if (!entry.isDir && matchGlob(pattern, entry.path)) {
|
|
119
|
+
// Get mtime for sorting
|
|
120
|
+
try {
|
|
121
|
+
const fileStat = await stat(join(baseDir, entry.path));
|
|
122
|
+
matches.push({
|
|
123
|
+
path: entry.path,
|
|
124
|
+
mtime: fileStat.mtimeMs,
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
matches.push({ path: entry.path, mtime: 0 });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Sort by mtime (newest first)
|
|
133
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
134
|
+
|
|
135
|
+
// Trim to limit
|
|
136
|
+
const trimmed = matches.slice(0, limit);
|
|
137
|
+
|
|
138
|
+
return trimmed.map(m => m.path).join('\n') || '(no matches)';
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return JSON.stringify({ error: `Glob search failed: ${err.message}` });
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|