@remcp/runtime 0.2.107 → 0.2.109
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/CHANGELOG.md +13 -0
- package/README.md +14 -2
- package/package.json +2 -2
- package/src/catalog.mjs +8 -0
- package/src/compact-catalog.mjs +2 -2
- package/src/extended/browser.mjs +384 -40
- package/src/extended/catalog.mjs +11 -8
- package/src/extended/desktop.mjs +14 -10
- package/src/extended/diagnostics.mjs +88 -27
- package/src/extended/documents.mjs +252 -82
- package/src/index.mjs +2 -2
- package/src/mcp-v2.mjs +7 -3
- package/src/screenshot-portal.mjs +115 -6
- package/src/sessions.mjs +4 -0
- package/src/tools/files.mjs +738 -288
- package/src/tools/search.mjs +84 -41
- package/src/util.mjs +4 -0
package/src/tools/search.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { readdir } from 'node:fs/promises';
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { openDirectoryPath, openRegularFile } from './files.mjs';
|
|
6
6
|
import { countEvent, recordEvent } from '../telemetry.mjs';
|
|
7
7
|
import {
|
|
8
8
|
appendSearchResults,
|
|
@@ -76,6 +76,12 @@ function rememberSearchWarning(session, warning) {
|
|
|
76
76
|
session.warning = warning instanceof Error ? warning.message : String(warning);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function displayRipgrepLine(line, target) {
|
|
80
|
+
if (line === target.anchor) return target.displayPath;
|
|
81
|
+
if (line.startsWith(`${target.anchor}/`)) return `${target.displayPath}${line.slice(target.anchor.length)}`;
|
|
82
|
+
return line;
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
function ripgrepTraversalWarnings(stderr, target) {
|
|
80
86
|
const lines = String(stderr || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
81
87
|
if (!lines.length) return null;
|
|
@@ -90,19 +96,50 @@ function ripgrepTraversalWarnings(stderr, target) {
|
|
|
90
96
|
return lines;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
const SEARCH_READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0);
|
|
100
|
+
|
|
101
|
+
function onceAsync(callback) {
|
|
102
|
+
let called = false;
|
|
103
|
+
return async () => {
|
|
104
|
+
if (called) return;
|
|
105
|
+
called = true;
|
|
106
|
+
await callback();
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function openSearchTarget(absolute) {
|
|
111
|
+
let directory;
|
|
112
|
+
let directoryError;
|
|
95
113
|
try {
|
|
96
|
-
|
|
114
|
+
directory = await openDirectoryPath(absolute);
|
|
115
|
+
const info = await directory.handle.stat();
|
|
116
|
+
if (!info.isDirectory()) fail('Search path is not a directory');
|
|
117
|
+
const close = onceAsync(directory.close);
|
|
118
|
+
const anchor = directory.anchor.replace(/^\/proc\/self\/fd\//, `/proc/${process.pid}/fd/`);
|
|
119
|
+
return { kind:'directory', anchor, displayPath:absolute, close };
|
|
97
120
|
} catch (error) {
|
|
98
|
-
|
|
99
|
-
|
|
121
|
+
directoryError = error;
|
|
122
|
+
if (directory) await directory.close().catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const opened = await openRegularFile(absolute, SEARCH_READ_FLAGS);
|
|
126
|
+
const close = onceAsync(opened.close);
|
|
127
|
+
const anchor = process.platform === 'linux' ? `/proc/${process.pid}/fd/${opened.handle.fd}` : absolute;
|
|
128
|
+
return { kind:'file', anchor, displayPath:absolute, handle:opened.handle, info:opened.info, close };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (directoryError?.code === 'ENOENT' || error?.code === 'ENOENT') fail(`Search path not found: ${displayPath(absolute)}`);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function walk(node, options, onFile, isRoot = true) {
|
|
136
|
+
if (node.kind === 'file') {
|
|
137
|
+
await onFile(node);
|
|
100
138
|
return;
|
|
101
139
|
}
|
|
102
|
-
if (info.isFile()) { await onFile(target); return; }
|
|
103
140
|
let entries;
|
|
104
141
|
try {
|
|
105
|
-
entries = await readdir(
|
|
142
|
+
entries = await readdir(node.anchor, { withFileTypes: true });
|
|
106
143
|
} catch (error) {
|
|
107
144
|
if (isRoot) throw error;
|
|
108
145
|
options.onWarning?.(error);
|
|
@@ -111,20 +148,40 @@ async function walk(target, options, onFile, isRoot = true) {
|
|
|
111
148
|
for (const entry of entries) {
|
|
112
149
|
if (options.stopped()) return;
|
|
113
150
|
if (!options.includeHidden && entry.name.startsWith('.')) continue;
|
|
114
|
-
const
|
|
151
|
+
const childDisplay = path.join(node.displayPath, entry.name);
|
|
152
|
+
const childAnchor = path.join(node.anchor, entry.name);
|
|
115
153
|
if (entry.isDirectory()) {
|
|
116
154
|
if (options.skipDirectories.has(entry.name)) continue;
|
|
117
|
-
|
|
155
|
+
let child;
|
|
156
|
+
try {
|
|
157
|
+
child = await openDirectoryPath(childAnchor);
|
|
158
|
+
const info = await child.handle.stat();
|
|
159
|
+
if (!info.isDirectory()) fail('Filesystem path component is not a directory');
|
|
160
|
+
await walk({ kind:'directory', anchor:child.anchor, displayPath:childDisplay, close:child.close }, options, onFile, false);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
options.onWarning?.(error);
|
|
163
|
+
} finally {
|
|
164
|
+
if (child) await child.close().catch(() => {});
|
|
165
|
+
}
|
|
118
166
|
} else if (entry.isFile()) {
|
|
119
|
-
|
|
167
|
+
let child;
|
|
168
|
+
try {
|
|
169
|
+
child = await openRegularFile(childAnchor, SEARCH_READ_FLAGS);
|
|
170
|
+
await onFile({ kind:'file', displayPath:childDisplay, handle:child.handle, info:child.info });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
options.onWarning?.(error);
|
|
173
|
+
} finally {
|
|
174
|
+
if (child) await child.close().catch(() => {});
|
|
175
|
+
}
|
|
120
176
|
}
|
|
121
177
|
}
|
|
122
178
|
}
|
|
123
179
|
|
|
124
|
-
function runRipgrep(session, {
|
|
180
|
+
function runRipgrep(session, { target, pattern, searchType, filePattern, ignoreCase, includeHidden, includeIgnored, contextLines, maxResults, patternIsLiteral }) {
|
|
125
181
|
// Flags must come before the `--` separator: anything after it is treated as a path,
|
|
126
182
|
// which silently turned `--hidden` into a search target.
|
|
127
183
|
const args = ['--no-heading', '--color', 'never'];
|
|
184
|
+
const searchTarget = target.anchor;
|
|
128
185
|
if (ignoreCase) args.push('--ignore-case');
|
|
129
186
|
if (includeHidden) args.push('--hidden');
|
|
130
187
|
if (!includeIgnored) for (const glob of SKIP_GLOBS) args.push('-g', glob);
|
|
@@ -134,12 +191,12 @@ function runRipgrep(session, { path: target, pattern, searchType, filePattern, i
|
|
|
134
191
|
// Globs are case-sensitive even under --ignore-case, so a case-insensitive file
|
|
135
192
|
// search needs --iglob.
|
|
136
193
|
args.push(ignoreCase ? '--iglob' : '-g', fileNameGlob(pattern));
|
|
137
|
-
args.push('--',
|
|
194
|
+
args.push('--', searchTarget);
|
|
138
195
|
} else {
|
|
139
196
|
args.push('--line-number', '--with-filename');
|
|
140
197
|
if (patternIsLiteral) args.push('--fixed-strings');
|
|
141
198
|
if (contextLines) args.push('-C', String(contextLines));
|
|
142
|
-
args.push('--', pattern,
|
|
199
|
+
args.push('--', pattern, searchTarget);
|
|
143
200
|
}
|
|
144
201
|
const child = spawn(ripgrep(), args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
145
202
|
session.cancel = () => child.kill('SIGTERM');
|
|
@@ -154,7 +211,7 @@ function runRipgrep(session, { path: target, pattern, searchType, filePattern, i
|
|
|
154
211
|
const batch = [];
|
|
155
212
|
for (const line of lines) {
|
|
156
213
|
if (!line.trim()) continue;
|
|
157
|
-
batch.push(line);
|
|
214
|
+
batch.push(displayRipgrepLine(line, target));
|
|
158
215
|
collected += 1;
|
|
159
216
|
if (collected >= maxResults) { capped = true; child.kill('SIGTERM'); break; }
|
|
160
217
|
}
|
|
@@ -167,7 +224,7 @@ function runRipgrep(session, { path: target, pattern, searchType, filePattern, i
|
|
|
167
224
|
// ripgrep exits 2 both for fatal invocation errors and for partial traversal errors.
|
|
168
225
|
// Descendant OS errors are recoverable: preserve readable matches and surface a warning.
|
|
169
226
|
if (code === 2 && stderr.trim()) {
|
|
170
|
-
const warnings = ripgrepTraversalWarnings(stderr,
|
|
227
|
+
const warnings = ripgrepTraversalWarnings(stderr, searchTarget);
|
|
171
228
|
if (!warnings) { finishSearchSession(session, 'failed', stderr.trim().split(/\r?\n/)[0]); return; }
|
|
172
229
|
rememberSearchWarning(session, warnings.length === 1 ? warnings[0] : `${warnings[0]} (+${warnings.length - 1} more)`);
|
|
173
230
|
}
|
|
@@ -175,7 +232,7 @@ function runRipgrep(session, { path: target, pattern, searchType, filePattern, i
|
|
|
175
232
|
});
|
|
176
233
|
}
|
|
177
234
|
|
|
178
|
-
async function runFallback(session, {
|
|
235
|
+
async function runFallback(session, { target, matcher, searchType, filePattern, ignoreCase, contextLines, maxResults, includeHidden, includeIgnored }) {
|
|
179
236
|
const fileGlobs = splitGlobs(filePattern).map(globToRegExp);
|
|
180
237
|
const nameGlob = searchType === 'files' ? globToRegExp(fileNameGlob(session.pattern)) : null;
|
|
181
238
|
const nameLiteral = searchType === 'files' && !/[*?[\]{}]/.test(session.pattern) ? session.pattern : null;
|
|
@@ -187,30 +244,17 @@ async function runFallback(session, { path: target, matcher, searchType, filePat
|
|
|
187
244
|
onWarning: warning => rememberSearchWarning(session, warning),
|
|
188
245
|
}, async file => {
|
|
189
246
|
if (session.status !== 'running' || collected >= maxResults) return;
|
|
190
|
-
if (fileGlobs.length && !fileGlobs.some(glob => glob.test(path.basename(file)))) return;
|
|
247
|
+
if (fileGlobs.length && !fileGlobs.some(glob => glob.test(path.basename(file.displayPath)))) return;
|
|
191
248
|
if (searchType === 'files') {
|
|
192
|
-
const base = path.basename(file);
|
|
249
|
+
const base = path.basename(file.displayPath);
|
|
193
250
|
const matches = (nameGlob && nameGlob.test(base)) || (nameLiteral && (ignoreCase ? base.toLowerCase().includes(nameLiteral.toLowerCase()) : base.includes(nameLiteral)));
|
|
194
251
|
if (!matches) return;
|
|
195
252
|
collected += 1;
|
|
196
|
-
appendSearchResults(session, [displayPath(file)]);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
let handle;
|
|
200
|
-
try {
|
|
201
|
-
handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
202
|
-
} catch (error) {
|
|
203
|
-
rememberSearchWarning(session, error);
|
|
253
|
+
appendSearchResults(session, [displayPath(file.displayPath)]);
|
|
204
254
|
return;
|
|
205
255
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const info = await handle.stat();
|
|
209
|
-
if (!info.isFile() || info.size > MAX_FALLBACK_FILE_BYTES) return;
|
|
210
|
-
buffer = await handle.readFile();
|
|
211
|
-
} finally {
|
|
212
|
-
await handle.close();
|
|
213
|
-
}
|
|
256
|
+
if (!file.info.isFile() || file.info.size > MAX_FALLBACK_FILE_BYTES) return;
|
|
257
|
+
const buffer = await file.handle.readFile();
|
|
214
258
|
if (looksBinary(buffer)) return;
|
|
215
259
|
const lines = splitLines(buffer.toString('utf8'));
|
|
216
260
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -224,7 +268,7 @@ async function runFallback(session, { path: target, matcher, searchType, filePat
|
|
|
224
268
|
}
|
|
225
269
|
}
|
|
226
270
|
collected += 1;
|
|
227
|
-
appendSearchResults(session, [formatContentResult(file, index + 1, lines[index], context)]);
|
|
271
|
+
appendSearchResults(session, [formatContentResult(file.displayPath, index + 1, lines[index], context)]);
|
|
228
272
|
}
|
|
229
273
|
});
|
|
230
274
|
finishSearchSession(session, session.status === 'running' ? (collected >= maxResults ? 'capped' : 'completed') : session.status);
|
|
@@ -232,7 +276,6 @@ async function runFallback(session, { path: target, matcher, searchType, filePat
|
|
|
232
276
|
|
|
233
277
|
export async function startSearchTool(args) {
|
|
234
278
|
const target = await resolveSafePath(args.path);
|
|
235
|
-
await stat(target).catch(() => fail(`Search path not found: ${displayPath(target)}`));
|
|
236
279
|
const pattern = requireString(args.pattern, 'pattern');
|
|
237
280
|
const searchType = String(args.searchType || 'content').toLowerCase();
|
|
238
281
|
if (!['content', 'files'].includes(searchType)) fail('searchType must be content or files');
|
|
@@ -244,12 +287,12 @@ export async function startSearchTool(args) {
|
|
|
244
287
|
const contextLines = clampInteger(args.contextLines, 0, 0, 10);
|
|
245
288
|
const maxResults = clampInteger(args.maxResults, 200, 1, 5000);
|
|
246
289
|
const matcher = searchType === 'content' ? normalizePattern(pattern, args.literalSearch === true, ignoreCase) : { regex: null, literal: pattern, patternIsLiteral: false };
|
|
290
|
+
const openedTarget = await openSearchTarget(target);
|
|
247
291
|
const session = createSearchSession({ type: searchType, pattern, path: target, filePattern });
|
|
292
|
+
session.release = openedTarget.close;
|
|
248
293
|
countEvent('searchesStarted');
|
|
249
294
|
recordEvent('session_started', { sessionKind: 'search', success: true });
|
|
250
|
-
const options = {
|
|
251
|
-
// Literal searches also go to ripgrep through --fixed-strings; the JavaScript fallback
|
|
252
|
-
// only runs when ripgrep is unavailable.
|
|
295
|
+
const options = { target: openedTarget, pattern, searchType, filePattern, ignoreCase, includeHidden, includeIgnored, contextLines, maxResults, matcher, patternIsLiteral: matcher.patternIsLiteral === true };
|
|
253
296
|
if (ripgrep()) runRipgrep(session, options);
|
|
254
297
|
else void runFallback(session, options).catch(error => finishSearchSession(session, 'failed', error instanceof Error ? error.message : String(error)));
|
|
255
298
|
await waitForSearchResults(session, 1, 800);
|
package/src/util.mjs
CHANGED
|
@@ -76,6 +76,10 @@ export async function canonicalizePath(target) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
let resolvedRootsPromise;
|
|
79
|
+
export async function resolvedAllowedRoots() {
|
|
80
|
+
return resolvedRoots();
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
function resolvedRoots() {
|
|
80
84
|
if (!resolvedRootsPromise) {
|
|
81
85
|
resolvedRootsPromise = Promise.all(runtimeConfig.allowedRoots.map(async root => {
|