@remcp/runtime 0.2.0 → 0.2.5
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 +23 -0
- package/README.md +96 -41
- package/package.json +3 -2
- package/src/catalog.mjs +377 -11
- package/src/config.mjs +47 -5
- package/src/diff.mjs +86 -0
- package/src/index.mjs +39 -6
- package/src/invoke.mjs +5 -2
- package/src/patch.mjs +95 -0
- package/src/policy.mjs +71 -20
- package/src/sessions.mjs +58 -10
- package/src/telemetry.mjs +16 -2
- package/src/tools/files.mjs +725 -33
- package/src/tools/search.mjs +12 -5
- package/src/tools/system.mjs +58 -1
- package/src/tools/terminal.mjs +85 -32
- package/src/util.mjs +34 -2
package/src/catalog.mjs
CHANGED
|
@@ -28,6 +28,24 @@ export const toolDefinitions = [
|
|
|
28
28
|
annotations: readOnly,
|
|
29
29
|
handler: fileToolHandlers.read_file,
|
|
30
30
|
},
|
|
31
|
+
{
|
|
32
|
+
name: 'read_files',
|
|
33
|
+
title: 'Read files by glob',
|
|
34
|
+
description: 'Read every file matching a glob under a directory in one call, each section prefixed with its path and line count. Use this to load a whole project area into context quickly instead of one read per file.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
path: { type: 'string', description: 'Absolute directory (or single file) to start from.' },
|
|
39
|
+
pattern: { type: 'string', description: 'Glob matched against the relative path and the file name, such as "src/**/*.ts" or "*.md". Default **/* .' },
|
|
40
|
+
max_files: { type: 'number', description: 'Stop after this many files. Default 100, maximum 500.' },
|
|
41
|
+
max_lines_per_file: { type: 'number', description: 'Lines kept per file. Default 2000.' },
|
|
42
|
+
include_ignored: { type: 'boolean', description: 'Also descend into .git and node_modules. Default false.' },
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
annotations: readOnly,
|
|
47
|
+
handler: fileToolHandlers.read_files,
|
|
48
|
+
},
|
|
31
49
|
{
|
|
32
50
|
name: 'read_multiple_files',
|
|
33
51
|
title: 'Read multiple files',
|
|
@@ -43,15 +61,81 @@ export const toolDefinitions = [
|
|
|
43
61
|
annotations: readOnly,
|
|
44
62
|
handler: fileToolHandlers.read_multiple_files,
|
|
45
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: 'read_image',
|
|
66
|
+
title: 'Read image',
|
|
67
|
+
description: 'Return an image file (PNG, JPEG, GIF, WebP, BMP, AVIF, or SVG) as a viewable image, so screenshots and diagrams can be inspected. Fails above the inline size limit.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
path: { type: 'string', description: 'Absolute path of the image file.' },
|
|
72
|
+
},
|
|
73
|
+
required: ['path'],
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
annotations: readOnly,
|
|
77
|
+
handler: fileToolHandlers.read_image,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'read_binary',
|
|
81
|
+
title: 'Read binary chunk',
|
|
82
|
+
description: 'Read any file as base64, in chunks, for transferring binaries, images, archives, or documents off the computer. Returns size, offset, and nextOffsetBytes; call again with offset_bytes set to nextOffsetBytes until complete is true.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
path: { type: 'string', description: 'Absolute path of the file to read.' },
|
|
87
|
+
offset_bytes: { type: 'number', description: 'Byte offset to start at. Default 0.' },
|
|
88
|
+
length_bytes: { type: 'number', description: 'Chunk size in bytes. Default and maximum 1048576 (1 MiB).' },
|
|
89
|
+
},
|
|
90
|
+
required: ['path'],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
annotations: readOnly,
|
|
94
|
+
handler: fileToolHandlers.read_binary,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'write_binary',
|
|
98
|
+
title: 'Write binary chunk',
|
|
99
|
+
description: 'Write base64 data to a file byte for byte, creating parent directories. Use mode "append" to send a large file as consecutive chunks. Replaces the file by default.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
path: { type: 'string', description: 'Absolute path of the file to write.' },
|
|
104
|
+
data: { type: 'string', description: 'Base64-encoded content.' },
|
|
105
|
+
mode: { type: 'string', enum: ['rewrite', 'append'], description: 'rewrite replaces the file, append adds to the end. Default rewrite.' },
|
|
106
|
+
},
|
|
107
|
+
required: ['path', 'data'],
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
},
|
|
110
|
+
annotations: mutating,
|
|
111
|
+
handler: fileToolHandlers.write_binary,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'hash_file',
|
|
115
|
+
title: 'Hash file',
|
|
116
|
+
description: 'Compute a checksum of a file without reading it into memory. Useful to verify a copy, compare two files, or confirm a download.',
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
path: { type: 'string', description: 'Absolute path of the file to hash.' },
|
|
121
|
+
algorithm: { type: 'string', enum: ['sha256', 'sha1', 'md5'], description: 'Hash algorithm. Default sha256.' },
|
|
122
|
+
},
|
|
123
|
+
required: ['path'],
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
},
|
|
126
|
+
annotations: readOnly,
|
|
127
|
+
handler: fileToolHandlers.hash_file,
|
|
128
|
+
},
|
|
46
129
|
{
|
|
47
130
|
name: 'list_directory',
|
|
48
131
|
title: 'List directory',
|
|
49
|
-
description: 'List the files and directories at a path. Entries are prefixed with [DIR], [FILE], or [
|
|
132
|
+
description: 'List the files and directories at a path. Entries are prefixed with [DIR], [FILE], [LINK], or [DENIED] when a subdirectory cannot be read. depth controls how many directory levels are included and pattern filters file names by glob.',
|
|
50
133
|
inputSchema: {
|
|
51
134
|
type: 'object',
|
|
52
135
|
properties: {
|
|
53
136
|
path: { type: 'string', description: 'Absolute path of the directory to list.' },
|
|
54
137
|
depth: { type: 'number', description: 'Directory levels to list, from 1 to 5. Default 1.' },
|
|
138
|
+
pattern: { type: 'string', description: 'Optional glob that filters file names, such as "*.log". Directories are always listed.' },
|
|
55
139
|
},
|
|
56
140
|
required: ['path'],
|
|
57
141
|
additionalProperties: false,
|
|
@@ -77,7 +161,7 @@ export const toolDefinitions = [
|
|
|
77
161
|
{
|
|
78
162
|
name: 'write_file',
|
|
79
163
|
title: 'Write file',
|
|
80
|
-
description: 'Create a file or
|
|
164
|
+
description: 'Create a file or change its full content. Parent directories are created automatically. Replaces the file by default; use mode "append" to add to the end. For binary data pass encoding-free base64 through write_binary instead.',
|
|
81
165
|
inputSchema: {
|
|
82
166
|
type: 'object',
|
|
83
167
|
properties: {
|
|
@@ -91,10 +175,74 @@ export const toolDefinitions = [
|
|
|
91
175
|
annotations: mutating,
|
|
92
176
|
handler: fileToolHandlers.write_file,
|
|
93
177
|
},
|
|
178
|
+
{
|
|
179
|
+
name: 'write_files',
|
|
180
|
+
title: 'Write multiple files',
|
|
181
|
+
description: 'Create or replace many files in one call, each with its own path, content, and optional mode. Use this to scaffold a project or apply a multi-file change without one round trip per file.',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
files: {
|
|
186
|
+
type: 'array',
|
|
187
|
+
description: 'Files to write, at most 200 per call.',
|
|
188
|
+
items: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
path: { type: 'string', description: 'Absolute path of the file.' },
|
|
192
|
+
content: { type: 'string', description: 'Full file content.' },
|
|
193
|
+
mode: { type: 'string', enum: ['rewrite', 'append'], description: 'rewrite replaces the file (default), append adds to the end.' },
|
|
194
|
+
},
|
|
195
|
+
required: ['path', 'content'],
|
|
196
|
+
additionalProperties: false,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: ['files'],
|
|
201
|
+
additionalProperties: false,
|
|
202
|
+
},
|
|
203
|
+
annotations: mutating,
|
|
204
|
+
handler: fileToolHandlers.write_files,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'apply_patch',
|
|
208
|
+
title: 'Apply patch',
|
|
209
|
+
description: 'Apply a unified diff to one file or to several files at once, matching each hunk with a little fuzz so small offsets and whitespace differences still apply. This is the fastest way to land a multi-line change a model has already worked out. Pass dry_run to see the result as a diff first.',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
patch: { type: 'string', description: 'Unified diff, including ---/+++ headers and @@ hunks.' },
|
|
214
|
+
path: { type: 'string', description: 'Apply every hunk to this file, ignoring the patch headers.' },
|
|
215
|
+
dry_run: { type: 'boolean', description: 'Report the diff without writing. Default false.' },
|
|
216
|
+
},
|
|
217
|
+
required: ['patch'],
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
},
|
|
220
|
+
annotations: mutating,
|
|
221
|
+
handler: fileToolHandlers.apply_patch,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'set_permissions',
|
|
225
|
+
title: 'Set permissions',
|
|
226
|
+
description: 'Change the permission mode of a file or directory, optionally recursively and optionally with a numeric owner. Use this to make a script executable after writing it.',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
path: { type: 'string', description: 'Absolute path whose permissions should change.' },
|
|
231
|
+
mode: { type: 'string', description: 'Octal mode such as "755" or "0644".' },
|
|
232
|
+
recursive: { type: 'boolean', description: 'Apply to a directory and everything inside it. Default false.' },
|
|
233
|
+
uid: { type: 'number', description: 'Optional numeric user id to set as owner.' },
|
|
234
|
+
gid: { type: 'number', description: 'Optional numeric group id to set as owner.' },
|
|
235
|
+
},
|
|
236
|
+
required: ['path', 'mode'],
|
|
237
|
+
additionalProperties: false,
|
|
238
|
+
},
|
|
239
|
+
annotations: mutating,
|
|
240
|
+
handler: fileToolHandlers.set_permissions,
|
|
241
|
+
},
|
|
94
242
|
{
|
|
95
243
|
name: 'edit_block',
|
|
96
244
|
title: 'Edit file',
|
|
97
|
-
description: 'Replace an exact block of text in a file. Provide enough surrounding context to make old_string unique; the call fails unless the number of matches equals expected_replacements. When the exact text is not found, a whitespace-tolerant match is attempted and reported.',
|
|
245
|
+
description: 'Replace an exact block of text in a file. Provide enough surrounding context to make old_string unique; the call fails unless the number of matches equals expected_replacements. When the exact text is not found, a whitespace-tolerant match is attempted and reported. Pass dry_run to preview the change as a diff without writing.',
|
|
98
246
|
inputSchema: {
|
|
99
247
|
type: 'object',
|
|
100
248
|
properties: {
|
|
@@ -103,6 +251,7 @@ export const toolDefinitions = [
|
|
|
103
251
|
new_string: { type: 'string', description: 'Replacement text.' },
|
|
104
252
|
expected_replacements: { type: 'number', description: 'Number of matches required for the edit to apply. Default 1.' },
|
|
105
253
|
allow_fuzzy: { type: 'boolean', description: 'Allow a whitespace-tolerant fallback when the exact text is not found. Default true.' },
|
|
254
|
+
dry_run: { type: 'boolean', description: 'Return the diff without changing the file. Default false.' },
|
|
106
255
|
},
|
|
107
256
|
required: ['file_path', 'old_string', 'new_string'],
|
|
108
257
|
additionalProperties: false,
|
|
@@ -110,16 +259,88 @@ export const toolDefinitions = [
|
|
|
110
259
|
annotations: mutating,
|
|
111
260
|
handler: fileToolHandlers.edit_block,
|
|
112
261
|
},
|
|
262
|
+
{
|
|
263
|
+
name: 'replace_lines',
|
|
264
|
+
title: 'Replace lines',
|
|
265
|
+
description: 'Replace an inclusive 1-based line range with new text. The rest of the file, including its line endings, is preserved. Pass dry_run to preview the change as a diff without writing.',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
path: { type: 'string', description: 'Absolute path of the file to edit.' },
|
|
270
|
+
start_line: { type: 'number', description: 'First line to replace, 1-based and inclusive.' },
|
|
271
|
+
end_line: { type: 'number', description: 'Last line to replace, 1-based and inclusive.' },
|
|
272
|
+
content: { type: 'string', description: 'Replacement text; an empty string deletes the range.' },
|
|
273
|
+
dry_run: { type: 'boolean', description: 'Return the diff without changing the file. Default false.' },
|
|
274
|
+
},
|
|
275
|
+
required: ['path', 'start_line', 'end_line', 'content'],
|
|
276
|
+
additionalProperties: false,
|
|
277
|
+
},
|
|
278
|
+
annotations: mutating,
|
|
279
|
+
handler: fileToolHandlers.replace_lines,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'replace_in_files',
|
|
283
|
+
title: 'Replace in files',
|
|
284
|
+
description: 'Replace text or a regular expression across the text files under a path and report what changed. Applies immediately; pass dry_run true to preview the affected files first.',
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
properties: {
|
|
288
|
+
path: { type: 'string', description: 'Absolute path of a file or directory to search.' },
|
|
289
|
+
pattern: { type: 'string', description: 'Text or regular expression to find.' },
|
|
290
|
+
replacement: { type: 'string', description: 'Replacement text. In regex mode, $1 and friends refer to capture groups.' },
|
|
291
|
+
filePattern: { type: 'string', description: 'Optional glob limiting which file names are changed, such as "*.ts".' },
|
|
292
|
+
regex: { type: 'boolean', description: 'Treat pattern as a regular expression. Default false (plain text).' },
|
|
293
|
+
dry_run: { type: 'boolean', description: 'Only report the files that would change. Default false.' },
|
|
294
|
+
maxFiles: { type: 'number', description: 'Stop after this many changed files. Default 100, maximum 500.' },
|
|
295
|
+
},
|
|
296
|
+
required: ['path', 'pattern', 'replacement'],
|
|
297
|
+
additionalProperties: false,
|
|
298
|
+
},
|
|
299
|
+
annotations: mutating,
|
|
300
|
+
handler: fileToolHandlers.replace_in_files,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: 'diff_files',
|
|
304
|
+
title: 'Diff files',
|
|
305
|
+
description: 'Show a unified diff between two local text files, with line counts. Useful to check what changed before reporting or reverting it.',
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {
|
|
309
|
+
left: { type: 'string', description: 'Absolute path of the original file.' },
|
|
310
|
+
right: { type: 'string', description: 'Absolute path of the file to compare against it.' },
|
|
311
|
+
context_lines: { type: 'number', description: 'Lines of context around each change. Default 3, maximum 20.' },
|
|
312
|
+
},
|
|
313
|
+
required: ['left', 'right'],
|
|
314
|
+
additionalProperties: false,
|
|
315
|
+
},
|
|
316
|
+
annotations: readOnly,
|
|
317
|
+
handler: fileToolHandlers.diff_files,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'move_to_trash',
|
|
321
|
+
title: 'Move to trash',
|
|
322
|
+
description: 'Move a file or directory to the system trash instead of deleting it, so the change can be undone. When the trash is outside the device allowed roots, a .remcp-trash folder beside the file is used instead.',
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: 'object',
|
|
325
|
+
properties: {
|
|
326
|
+
source: { type: 'string', description: 'Absolute path to move to the trash.' },
|
|
327
|
+
},
|
|
328
|
+
required: ['source'],
|
|
329
|
+
additionalProperties: false,
|
|
330
|
+
},
|
|
331
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
332
|
+
handler: fileToolHandlers.move_to_trash,
|
|
333
|
+
},
|
|
113
334
|
{
|
|
114
335
|
name: 'create_directory',
|
|
115
|
-
title: 'Create
|
|
116
|
-
description: 'Create a
|
|
336
|
+
title: 'Create directories',
|
|
337
|
+
description: 'Create one directory or many in a single call, including any missing parent directories. Succeeds when a directory already exists.',
|
|
117
338
|
inputSchema: {
|
|
118
339
|
type: 'object',
|
|
119
340
|
properties: {
|
|
120
341
|
path: { type: 'string', description: 'Absolute path of the directory to create.' },
|
|
342
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'Several directories to create at once, at most 200.' },
|
|
121
343
|
},
|
|
122
|
-
required: ['path'],
|
|
123
344
|
additionalProperties: false,
|
|
124
345
|
},
|
|
125
346
|
annotations: additive,
|
|
@@ -128,36 +349,173 @@ export const toolDefinitions = [
|
|
|
128
349
|
{
|
|
129
350
|
name: 'move_file',
|
|
130
351
|
title: 'Move or rename',
|
|
131
|
-
description: 'Move or rename a file or directory.
|
|
352
|
+
description: 'Move or rename a file or directory. Replaces an existing destination file by default; pass overwrite false to refuse instead.',
|
|
132
353
|
inputSchema: {
|
|
133
354
|
type: 'object',
|
|
134
355
|
properties: {
|
|
135
356
|
source: { type: 'string', description: 'Absolute path to move.' },
|
|
136
357
|
destination: { type: 'string', description: 'Absolute destination path.' },
|
|
358
|
+
overwrite: { type: 'boolean', description: 'Replace an existing destination file. Default true.' },
|
|
137
359
|
},
|
|
138
360
|
required: ['source', 'destination'],
|
|
139
361
|
additionalProperties: false,
|
|
140
362
|
},
|
|
141
|
-
annotations:
|
|
363
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
142
364
|
handler: fileToolHandlers.move_file,
|
|
143
365
|
},
|
|
144
366
|
{
|
|
145
367
|
name: 'copy_file',
|
|
146
368
|
title: 'Copy file',
|
|
147
|
-
description: 'Copy one file to a new path
|
|
369
|
+
description: 'Copy one file to a new path, replacing the destination by default. Pass overwrite false to refuse an existing destination. Directories are not copied recursively.',
|
|
148
370
|
inputSchema: {
|
|
149
371
|
type: 'object',
|
|
150
372
|
properties: {
|
|
151
373
|
source: { type: 'string', description: 'Absolute path of the file to copy.' },
|
|
152
374
|
destination: { type: 'string', description: 'Absolute destination path.' },
|
|
153
|
-
overwrite: { type: 'boolean', description: 'Replace the destination when it already exists. Default
|
|
375
|
+
overwrite: { type: 'boolean', description: 'Replace the destination when it already exists. Default true.' },
|
|
154
376
|
},
|
|
155
377
|
required: ['source', 'destination'],
|
|
156
378
|
additionalProperties: false,
|
|
157
379
|
},
|
|
158
|
-
annotations:
|
|
380
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
159
381
|
handler: fileToolHandlers.copy_file,
|
|
160
382
|
},
|
|
383
|
+
{
|
|
384
|
+
name: 'copy_paths',
|
|
385
|
+
title: 'Copy paths',
|
|
386
|
+
description: 'Copy many files or whole directories in one call, each with its own source and destination. Directories are copied recursively.',
|
|
387
|
+
inputSchema: {
|
|
388
|
+
type: 'object',
|
|
389
|
+
properties: {
|
|
390
|
+
paths: {
|
|
391
|
+
type: 'array',
|
|
392
|
+
description: 'Pairs to copy, at most 200 per call.',
|
|
393
|
+
items: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
source: { type: 'string', description: 'Absolute path to copy.' },
|
|
397
|
+
destination: { type: 'string', description: 'Absolute destination path.' },
|
|
398
|
+
},
|
|
399
|
+
required: ['source', 'destination'],
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
overwrite: { type: 'boolean', description: 'Replace an existing destination. Default true.' },
|
|
404
|
+
},
|
|
405
|
+
required: ['paths'],
|
|
406
|
+
additionalProperties: false,
|
|
407
|
+
},
|
|
408
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
409
|
+
handler: fileToolHandlers.copy_paths,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'move_paths',
|
|
413
|
+
title: 'Move paths',
|
|
414
|
+
description: 'Move or rename many files or whole directories in one call, each with its own source and destination. Falls back to copy-and-delete across filesystems.',
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
paths: {
|
|
419
|
+
type: 'array',
|
|
420
|
+
description: 'Pairs to move, at most 200 per call.',
|
|
421
|
+
items: {
|
|
422
|
+
type: 'object',
|
|
423
|
+
properties: {
|
|
424
|
+
source: { type: 'string', description: 'Absolute path to move.' },
|
|
425
|
+
destination: { type: 'string', description: 'Absolute destination path.' },
|
|
426
|
+
},
|
|
427
|
+
required: ['source', 'destination'],
|
|
428
|
+
additionalProperties: false,
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
overwrite: { type: 'boolean', description: 'Replace an existing destination. Default true.' },
|
|
432
|
+
},
|
|
433
|
+
required: ['paths'],
|
|
434
|
+
additionalProperties: false,
|
|
435
|
+
},
|
|
436
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
437
|
+
handler: fileToolHandlers.move_paths,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: 'delete_path',
|
|
441
|
+
title: 'Delete path',
|
|
442
|
+
description: 'Delete a file or a directory on the computer. Directories are removed with their contents unless recursive is false. The filesystem root is refused.',
|
|
443
|
+
inputSchema: {
|
|
444
|
+
type: 'object',
|
|
445
|
+
properties: {
|
|
446
|
+
path: { type: 'string', description: 'Absolute path to delete.' },
|
|
447
|
+
recursive: { type: 'boolean', description: 'Delete a non-empty directory with its contents. Default true.' },
|
|
448
|
+
},
|
|
449
|
+
required: ['path'],
|
|
450
|
+
additionalProperties: false,
|
|
451
|
+
},
|
|
452
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
453
|
+
handler: fileToolHandlers.delete_path,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: 'delete_paths',
|
|
457
|
+
title: 'Delete paths',
|
|
458
|
+
description: 'Delete many files and directories in one call, reporting each result. Use move_to_trash instead when the deletion should be reversible.',
|
|
459
|
+
inputSchema: {
|
|
460
|
+
type: 'object',
|
|
461
|
+
properties: {
|
|
462
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'Absolute paths to delete, at most 500 per call.' },
|
|
463
|
+
recursive: { type: 'boolean', description: 'Delete non-empty directories with their contents. Default true.' },
|
|
464
|
+
},
|
|
465
|
+
required: ['paths'],
|
|
466
|
+
additionalProperties: false,
|
|
467
|
+
},
|
|
468
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
469
|
+
handler: fileToolHandlers.delete_paths,
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'create_archive',
|
|
473
|
+
title: 'Create archive',
|
|
474
|
+
description: 'Pack files and directories into a tar, tar.gz, or zip archive on the device, so a whole tree can be transferred or backed up in one call.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'Absolute paths of the files and directories to include.' },
|
|
479
|
+
destination: { type: 'string', description: 'Absolute path of the archive to create.' },
|
|
480
|
+
format: { type: 'string', enum: ['tar', 'tar.gz', 'zip'], description: 'Archive format. Default tar.gz, or zip when the destination ends in .zip.' },
|
|
481
|
+
},
|
|
482
|
+
required: ['paths', 'destination'],
|
|
483
|
+
additionalProperties: false,
|
|
484
|
+
},
|
|
485
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
486
|
+
handler: fileToolHandlers.create_archive,
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: 'extract_archive',
|
|
490
|
+
title: 'Extract archive',
|
|
491
|
+
description: 'Extract a tar, tar.gz, tar.bz2, tar.xz, or zip archive on the device into a directory, creating it when needed.',
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: 'object',
|
|
494
|
+
properties: {
|
|
495
|
+
archive: { type: 'string', description: 'Absolute path of the archive to extract.' },
|
|
496
|
+
destination: { type: 'string', description: 'Absolute directory to extract into. Defaults to the archive directory.' },
|
|
497
|
+
},
|
|
498
|
+
required: ['archive'],
|
|
499
|
+
additionalProperties: false,
|
|
500
|
+
},
|
|
501
|
+
annotations: mutating,
|
|
502
|
+
handler: fileToolHandlers.extract_archive,
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'take_screenshot',
|
|
506
|
+
title: 'Take screenshot',
|
|
507
|
+
description: 'Capture the screen of the paired computer and return it as an image, for GUI work, visual checks, and demonstrating what is on screen. Uses grim, gnome-screenshot, spectacle, scrot, ImageMagick import, screencapture, or PowerShell depending on the platform.',
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: 'object',
|
|
510
|
+
properties: {
|
|
511
|
+
directory: { type: 'string', description: 'Absolute directory to write the temporary PNG into. Defaults to the system temp directory.' },
|
|
512
|
+
keep: { type: 'boolean', description: 'Keep the PNG on disk instead of deleting it after it is returned. Default false.' },
|
|
513
|
+
},
|
|
514
|
+
additionalProperties: false,
|
|
515
|
+
},
|
|
516
|
+
annotations: readOnly,
|
|
517
|
+
handler: fileToolHandlers.take_screenshot,
|
|
518
|
+
},
|
|
161
519
|
{
|
|
162
520
|
name: 'start_search',
|
|
163
521
|
title: 'Start search',
|
|
@@ -313,6 +671,14 @@ export const toolDefinitions = [
|
|
|
313
671
|
annotations: readOnly,
|
|
314
672
|
handler: terminalToolHandlers.list_sessions,
|
|
315
673
|
},
|
|
674
|
+
{
|
|
675
|
+
name: 'get_system_info',
|
|
676
|
+
title: 'Get system info',
|
|
677
|
+
description: 'Report host details for the paired computer: operating system and kernel, CPU model and load, memory pressure, free disk space on the working volume, uptime, and the default shell.',
|
|
678
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
679
|
+
annotations: readOnly,
|
|
680
|
+
handler: systemToolHandlers.get_system_info,
|
|
681
|
+
},
|
|
316
682
|
{
|
|
317
683
|
name: 'list_processes',
|
|
318
684
|
title: 'List processes',
|
package/src/config.mjs
CHANGED
|
@@ -7,8 +7,35 @@ const configDir = process.env.REMCP_RUNTIME_CONFIG_DIR || path.join(os.homedir()
|
|
|
7
7
|
export const runtimeConfigPath = path.join(configDir, 'runtime.json');
|
|
8
8
|
export const runtimeConfigDir = configDir;
|
|
9
9
|
|
|
10
|
+
// The MCP SDK's stdio client closes the connection on a message above 10 MB, which kills
|
|
11
|
+
// this process. Keep the runtime's own output cap well below that so raising an env var
|
|
12
|
+
// cannot turn a large tool result into a dead device.
|
|
13
|
+
export const HARD_OUTPUT_CEILING_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
let configError = null;
|
|
16
|
+
|
|
10
17
|
function readConfigFile() {
|
|
11
|
-
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = readFileSync(runtimeConfigPath, 'utf8');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error?.code === 'ENOENT') return {};
|
|
23
|
+
configError = `Could not read ${runtimeConfigPath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
if (!raw.trim()) return {};
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
30
|
+
configError = `${runtimeConfigPath} must contain a JSON object`;
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// A trailing comma used to silently drop allowedRoots and re-enable usage metrics.
|
|
36
|
+
configError = `${runtimeConfigPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`;
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
12
39
|
}
|
|
13
40
|
|
|
14
41
|
const file = readConfigFile();
|
|
@@ -34,7 +61,10 @@ function booleanValue(value, fallback) {
|
|
|
34
61
|
|
|
35
62
|
const DANGEROUS_MODES = ['block', 'warn', 'allow'];
|
|
36
63
|
|
|
37
|
-
|
|
64
|
+
// `warn` runs the command and adds a note when it matches the destructive-command list:
|
|
65
|
+
// nothing is ever blocked or delayed, but a catastrophic command is visible in the tool
|
|
66
|
+
// result. Set `allow` for zero noise or `block` to refuse.
|
|
67
|
+
function dangerousMode(value, fallback = 'warn') {
|
|
38
68
|
const normalized = String(value ?? '').trim().toLowerCase();
|
|
39
69
|
return DANGEROUS_MODES.includes(normalized) ? normalized : fallback;
|
|
40
70
|
}
|
|
@@ -50,19 +80,29 @@ const telemetryEnabled = telemetryDisabled
|
|
|
50
80
|
? false
|
|
51
81
|
: booleanValue(process.env.REMCP_RUNTIME_TELEMETRY ?? file.telemetryEnabled, true);
|
|
52
82
|
|
|
83
|
+
// 2 MiB of tool result per call by default: enough for a large file read or a batch of
|
|
84
|
+
// files, still comfortably below the transport ceiling.
|
|
85
|
+
const configuredOutputBytes = positiveNumber(process.env.REMCP_RUNTIME_MAX_OUTPUT_BYTES ?? file.maxOutputBytes, 2 * 1024 * 1024);
|
|
86
|
+
|
|
53
87
|
export const runtimeConfig = Object.freeze({
|
|
54
88
|
allowedRoots: Object.freeze(allowedRoots),
|
|
55
89
|
blockedCommands: Object.freeze(stringList(process.env.REMCP_RUNTIME_BLOCKED_COMMANDS ?? file.blockedCommands)),
|
|
56
90
|
dangerousCommands: dangerousMode(process.env.REMCP_RUNTIME_DANGEROUS_COMMANDS ?? file.dangerousCommands),
|
|
57
|
-
maxOutputBytes:
|
|
58
|
-
maxReadLines: positiveNumber(process.env.REMCP_RUNTIME_MAX_READ_LINES ?? file.maxReadLines,
|
|
59
|
-
maxBufferedLines: positiveNumber(process.env.REMCP_RUNTIME_MAX_BUFFERED_LINES ?? file.maxBufferedLines,
|
|
91
|
+
maxOutputBytes: Math.min(configuredOutputBytes, HARD_OUTPUT_CEILING_BYTES),
|
|
92
|
+
maxReadLines: positiveNumber(process.env.REMCP_RUNTIME_MAX_READ_LINES ?? file.maxReadLines, 4000),
|
|
93
|
+
maxBufferedLines: positiveNumber(process.env.REMCP_RUNTIME_MAX_BUFFERED_LINES ?? file.maxBufferedLines, 100000),
|
|
60
94
|
maxWriteBytes: positiveNumber(process.env.REMCP_RUNTIME_MAX_WRITE_BYTES ?? file.maxWriteBytes, 8 * 1024 * 1024),
|
|
61
95
|
defaultShell: String(process.env.REMCP_RUNTIME_SHELL || file.defaultShell || '').trim(),
|
|
62
96
|
name: String(process.env.REMCP_RUNTIME_NAME || file.name || os.hostname()).trim(),
|
|
63
97
|
telemetryEnabled,
|
|
64
98
|
});
|
|
65
99
|
|
|
100
|
+
// A configuration the user cannot read is not a configuration we should quietly ignore:
|
|
101
|
+
// it is how allowedRoots and an opt-out silently disappear.
|
|
102
|
+
export function configurationError() {
|
|
103
|
+
return configError;
|
|
104
|
+
}
|
|
105
|
+
|
|
66
106
|
export function describeConfig() {
|
|
67
107
|
return {
|
|
68
108
|
name: runtimeConfig.name,
|
|
@@ -70,10 +110,12 @@ export function describeConfig() {
|
|
|
70
110
|
arch: process.arch,
|
|
71
111
|
node: process.versions.node,
|
|
72
112
|
configFile: runtimeConfigPath,
|
|
113
|
+
configError,
|
|
73
114
|
allowedRoots: [...runtimeConfig.allowedRoots],
|
|
74
115
|
blockedCommands: [...runtimeConfig.blockedCommands],
|
|
75
116
|
dangerousCommands: runtimeConfig.dangerousCommands,
|
|
76
117
|
maxOutputBytes: runtimeConfig.maxOutputBytes,
|
|
118
|
+
maxOutputBytesCeiling: HARD_OUTPUT_CEILING_BYTES,
|
|
77
119
|
maxReadLines: runtimeConfig.maxReadLines,
|
|
78
120
|
maxBufferedLines: runtimeConfig.maxBufferedLines,
|
|
79
121
|
maxWriteBytes: runtimeConfig.maxWriteBytes,
|
package/src/diff.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { splitLines } from './util.mjs';
|
|
2
|
+
|
|
3
|
+
// Compact line diff. Files routinely have a large identical prefix and suffix, so those
|
|
4
|
+
// are trimmed first; the remaining window is diffed with an LCS table capped at a size
|
|
5
|
+
// that keeps memory bounded on pathological input.
|
|
6
|
+
|
|
7
|
+
const MAX_DIFF_LINES = 4000;
|
|
8
|
+
const CONTEXT = 3;
|
|
9
|
+
|
|
10
|
+
function commonPrefix(a, b) {
|
|
11
|
+
const limit = Math.min(a.length, b.length);
|
|
12
|
+
let index = 0;
|
|
13
|
+
while (index < limit && a[index] === b[index]) index += 1;
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function commonSuffix(a, b, fromStart) {
|
|
18
|
+
const limit = Math.min(a.length, b.length) - fromStart;
|
|
19
|
+
let count = 0;
|
|
20
|
+
while (count < limit && a[a.length - 1 - count] === b[b.length - 1 - count]) count += 1;
|
|
21
|
+
return count;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function lcsOperations(a, b) {
|
|
25
|
+
const rows = a.length + 1;
|
|
26
|
+
const cols = b.length + 1;
|
|
27
|
+
const table = Array.from({ length: rows }, () => new Uint32Array(cols));
|
|
28
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
29
|
+
for (let j = b.length - 1; j >= 0; j -= 1) {
|
|
30
|
+
table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const operations = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
let j = 0;
|
|
36
|
+
while (i < a.length && j < b.length) {
|
|
37
|
+
if (a[i] === b[j]) { operations.push({ type: 'equal', line: a[i] }); i += 1; j += 1; }
|
|
38
|
+
else if (table[i + 1][j] >= table[i][j + 1]) { operations.push({ type: 'delete', line: a[i] }); i += 1; }
|
|
39
|
+
else { operations.push({ type: 'insert', line: b[j] }); j += 1; }
|
|
40
|
+
}
|
|
41
|
+
while (i < a.length) { operations.push({ type: 'delete', line: a[i] }); i += 1; }
|
|
42
|
+
while (j < b.length) { operations.push({ type: 'insert', line: b[j] }); j += 1; }
|
|
43
|
+
return operations;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function diffStats(oldText, newText) {
|
|
47
|
+
const before = splitLines(oldText);
|
|
48
|
+
const after = splitLines(newText);
|
|
49
|
+
const prefix = commonPrefix(before, after);
|
|
50
|
+
const suffix = commonSuffix(before, after, prefix);
|
|
51
|
+
const middleBefore = before.slice(prefix, before.length - suffix);
|
|
52
|
+
const middleAfter = after.slice(prefix, after.length - suffix);
|
|
53
|
+
const operations = middleBefore.length > MAX_DIFF_LINES || middleAfter.length > MAX_DIFF_LINES
|
|
54
|
+
? [...middleBefore.map(line => ({ type: 'delete', line })), ...middleAfter.map(line => ({ type: 'insert', line }))]
|
|
55
|
+
: lcsOperations(middleBefore, middleAfter);
|
|
56
|
+
return {
|
|
57
|
+
added: operations.filter(op => op.type === 'insert').length,
|
|
58
|
+
removed: operations.filter(op => op.type === 'delete').length,
|
|
59
|
+
truncated: middleBefore.length > MAX_DIFF_LINES || middleAfter.length > MAX_DIFF_LINES,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function unifiedDiff(oldText, newText, { oldLabel = 'before', newLabel = 'after', context = CONTEXT } = {}) {
|
|
64
|
+
const before = splitLines(oldText);
|
|
65
|
+
const after = splitLines(newText);
|
|
66
|
+
if (before.join('\n') === after.join('\n')) return '';
|
|
67
|
+
|
|
68
|
+
const prefix = commonPrefix(before, after);
|
|
69
|
+
const suffix = commonSuffix(before, after, prefix);
|
|
70
|
+
const middleBefore = before.slice(prefix, before.length - suffix);
|
|
71
|
+
const middleAfter = after.slice(prefix, after.length - suffix);
|
|
72
|
+
const truncated = middleBefore.length > MAX_DIFF_LINES || middleAfter.length > MAX_DIFF_LINES;
|
|
73
|
+
const operations = truncated
|
|
74
|
+
? [...middleBefore.map(line => ({ type: 'delete', line })), ...middleAfter.map(line => ({ type: 'insert', line }))]
|
|
75
|
+
: lcsOperations(middleBefore, middleAfter);
|
|
76
|
+
|
|
77
|
+
const rows = [];
|
|
78
|
+
for (let index = 0; index < Math.min(prefix, context); index += 1) rows.push({ type: 'equal', line: before[prefix - Math.min(prefix, context) + index] });
|
|
79
|
+
rows.push(...operations);
|
|
80
|
+
for (let index = 0; index < Math.min(suffix, context); index += 1) rows.push({ type: 'equal', line: before[before.length - suffix + index] });
|
|
81
|
+
|
|
82
|
+
const header = `--- ${oldLabel}\n+++ ${newLabel}`;
|
|
83
|
+
const body = rows.map(row => `${row.type === 'insert' ? '+' : row.type === 'delete' ? '-' : ' '}${row.line}`);
|
|
84
|
+
if (truncated) body.unshift('… diff truncated to the changed region (file is very large) …');
|
|
85
|
+
return [header, `@@ -${prefix + 1},${middleBefore.length} +${prefix + 1},${middleAfter.length} @@`, ...body].join('\n');
|
|
86
|
+
}
|