@remcp/runtime 0.2.94 → 0.2.96
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/package.json +1 -1
- package/src/sessions.mjs +1 -0
- package/src/tools/search.mjs +59 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.96 — 2026-09-22
|
|
4
|
+
|
|
5
|
+
- Keeps readable search matches when an unreadable descendant is encountered while surfacing a warning.
|
|
6
|
+
- Makes the regression backend-neutral so both ripgrep and the built-in scanner are validated consistently.
|
|
7
|
+
- Supersedes the failed unpublished 0.2.95 tag; runtime behavior is otherwise unchanged from that candidate.
|
|
8
|
+
|
|
9
|
+
## 0.2.95 — 2026-09-22
|
|
10
|
+
|
|
11
|
+
- Search sessions preserve readable matches when ripgrep encounters unreadable descendant paths, reporting the partial traversal problem as a warning instead of a fatal session error.
|
|
12
|
+
- The dependency-free fallback scanner now skips unreadable descendant directories/files with the same warning semantics.
|
|
13
|
+
- Requested-root failures and non-traversal ripgrep errors still fail closed.
|
|
14
|
+
- Added ripgrep and fallback regressions; runtime tool names and schemas remain unchanged.
|
|
15
|
+
|
|
3
16
|
## 0.2.94 — 2026-09-22
|
|
4
17
|
|
|
5
18
|
- Replaced DOCX/PDF regex-heavy extraction with bounded scanners and added adversarial parser regressions.
|
package/package.json
CHANGED
package/src/sessions.mjs
CHANGED
package/src/tools/search.mjs
CHANGED
|
@@ -71,17 +71,50 @@ function formatContentResult(file, lineNumber, line, contextLines) {
|
|
|
71
71
|
return rows.join('\n');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
function rememberSearchWarning(session, warning) {
|
|
75
|
+
if (session.warning) return;
|
|
76
|
+
session.warning = warning instanceof Error ? warning.message : String(warning);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ripgrepTraversalWarnings(stderr, target) {
|
|
80
|
+
const lines = String(stderr || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
81
|
+
if (!lines.length) return null;
|
|
82
|
+
const root = path.resolve(target);
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const match = line.match(/^rg: (.*): .* \(os error \d+\)$/);
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
const reported = path.resolve(match[1]);
|
|
87
|
+
const relative = path.relative(root, reported);
|
|
88
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function walk(target, options, onFile, isRoot = true) {
|
|
94
|
+
let info;
|
|
95
|
+
try {
|
|
96
|
+
info = await stat(target);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (isRoot) fail(`Search path not found: ${displayPath(target)}`);
|
|
99
|
+
options.onWarning?.(error);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
76
102
|
if (info.isFile()) { await onFile(target); return; }
|
|
77
|
-
|
|
103
|
+
let entries;
|
|
104
|
+
try {
|
|
105
|
+
entries = await readdir(target, { withFileTypes: true });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (isRoot) throw error;
|
|
108
|
+
options.onWarning?.(error);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
78
111
|
for (const entry of entries) {
|
|
79
112
|
if (options.stopped()) return;
|
|
80
113
|
if (!options.includeHidden && entry.name.startsWith('.')) continue;
|
|
81
114
|
const child = path.join(target, entry.name);
|
|
82
115
|
if (entry.isDirectory()) {
|
|
83
116
|
if (options.skipDirectories.has(entry.name)) continue;
|
|
84
|
-
await walk(child, options, onFile);
|
|
117
|
+
await walk(child, options, onFile, false);
|
|
85
118
|
} else if (entry.isFile()) {
|
|
86
119
|
await onFile(child);
|
|
87
120
|
}
|
|
@@ -131,8 +164,13 @@ function runRipgrep(session, { path: target, pattern, searchType, filePattern, i
|
|
|
131
164
|
child.on('error', error => finishSearchSession(session, 'failed', error.message));
|
|
132
165
|
child.on('close', code => {
|
|
133
166
|
if (session.status !== 'running') { finishSearchSession(session, session.status); return; }
|
|
134
|
-
// ripgrep exits 2 for
|
|
135
|
-
|
|
167
|
+
// ripgrep exits 2 both for fatal invocation errors and for partial traversal errors.
|
|
168
|
+
// Descendant OS errors are recoverable: preserve readable matches and surface a warning.
|
|
169
|
+
if (code === 2 && stderr.trim()) {
|
|
170
|
+
const warnings = ripgrepTraversalWarnings(stderr, target);
|
|
171
|
+
if (!warnings) { finishSearchSession(session, 'failed', stderr.trim().split(/\r?\n/)[0]); return; }
|
|
172
|
+
rememberSearchWarning(session, warnings.length === 1 ? warnings[0] : `${warnings[0]} (+${warnings.length - 1} more)`);
|
|
173
|
+
}
|
|
136
174
|
finishSearchSession(session, capped ? 'capped' : 'completed');
|
|
137
175
|
});
|
|
138
176
|
}
|
|
@@ -142,7 +180,12 @@ async function runFallback(session, { path: target, matcher, searchType, filePat
|
|
|
142
180
|
const nameGlob = searchType === 'files' ? globToRegExp(fileNameGlob(session.pattern)) : null;
|
|
143
181
|
const nameLiteral = searchType === 'files' && !/[*?[\]{}]/.test(session.pattern) ? session.pattern : null;
|
|
144
182
|
let collected = 0;
|
|
145
|
-
await walk(target, {
|
|
183
|
+
await walk(target, {
|
|
184
|
+
includeHidden,
|
|
185
|
+
skipDirectories: includeIgnored ? new Set() : SKIP_DIRECTORIES,
|
|
186
|
+
stopped: () => session.status !== 'running' || collected >= maxResults,
|
|
187
|
+
onWarning: warning => rememberSearchWarning(session, warning),
|
|
188
|
+
}, async file => {
|
|
146
189
|
if (session.status !== 'running' || collected >= maxResults) return;
|
|
147
190
|
if (fileGlobs.length && !fileGlobs.some(glob => glob.test(path.basename(file)))) return;
|
|
148
191
|
if (searchType === 'files') {
|
|
@@ -153,8 +196,13 @@ async function runFallback(session, { path: target, matcher, searchType, filePat
|
|
|
153
196
|
appendSearchResults(session, [displayPath(file)]);
|
|
154
197
|
return;
|
|
155
198
|
}
|
|
156
|
-
|
|
157
|
-
|
|
199
|
+
let handle;
|
|
200
|
+
try {
|
|
201
|
+
handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
rememberSearchWarning(session, error);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
158
206
|
let buffer;
|
|
159
207
|
try {
|
|
160
208
|
const info = await handle.stat();
|
|
@@ -210,6 +258,7 @@ export async function startSearchTool(args) {
|
|
|
210
258
|
return text([
|
|
211
259
|
`searchId: ${session.id} · type: ${searchType} · status: ${status} · results so far: ${session.results.length}`,
|
|
212
260
|
`pattern: ${pattern} · path: ${displayPath(target)}`,
|
|
261
|
+
session.warning ? `warning: ${session.warning}` : '',
|
|
213
262
|
initial.join('\n'),
|
|
214
263
|
session.results.length > initial.length ? `… ${session.results.length - initial.length} more results buffered; use get_more_search_results with searchId ${session.id}` : '',
|
|
215
264
|
].filter(Boolean).join('\n'));
|
|
@@ -234,6 +283,7 @@ export async function getMoreSearchResultsTool(args) {
|
|
|
234
283
|
const status = session.error ? `failed: ${session.error}` : session.status;
|
|
235
284
|
return text([
|
|
236
285
|
`searchId: ${session.id} · status: ${status} · results ${start}-${end} of ${total}`,
|
|
286
|
+
session.warning ? `warning: ${session.warning}` : '',
|
|
237
287
|
session.results.slice(start, end).join('\n'),
|
|
238
288
|
].filter(Boolean).join('\n'));
|
|
239
289
|
}
|