@levnikolaevich/hex-line-mcp 1.3.0 → 1.3.1
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/README.md +46 -26
- package/hook.mjs +32 -29
- package/lib/bulk-replace.mjs +8 -6
- package/lib/changes.mjs +7 -6
- package/lib/coerce.mjs +0 -6
- package/lib/edit.mjs +119 -41
- package/lib/format.mjs +138 -0
- package/lib/info.mjs +13 -31
- package/lib/read.mjs +5 -24
- package/lib/search.mjs +213 -77
- package/lib/security.mjs +6 -8
- package/lib/setup.mjs +0 -4
- package/lib/tree.mjs +69 -97
- package/lib/verify.mjs +2 -2
- package/package.json +4 -2
- package/server.mjs +31 -16
package/lib/tree.mjs
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compact directory tree with .gitignore support.
|
|
2
|
+
* Compact directory tree with root .gitignore support.
|
|
3
3
|
*
|
|
4
4
|
* Skips common build/cache dirs by default.
|
|
5
|
-
*
|
|
5
|
+
* Uses `ignore` package for spec-compliant .gitignore matching (path-based, negation, dir-only).
|
|
6
|
+
* Only root .gitignore is loaded — nested .gitignore files are not supported.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
9
10
|
import { resolve, basename, join, relative } from "node:path";
|
|
11
|
+
import { formatSize, relativeTime, countFileLines } from "./format.mjs";
|
|
12
|
+
import { normalizePath } from "./security.mjs";
|
|
13
|
+
import ignore from "ignore";
|
|
10
14
|
|
|
11
15
|
const SKIP_DIRS = new Set([
|
|
12
16
|
"node_modules", ".git", "dist", "build", "__pycache__", ".next", "coverage",
|
|
13
17
|
]);
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
|
-
* Convert a simple glob pattern to a RegExp.
|
|
17
|
-
*
|
|
20
|
+
* Convert a simple glob pattern to a RegExp for name matching.
|
|
21
|
+
* Used by pattern-mode to match entry names.
|
|
18
22
|
*/
|
|
19
23
|
function globToRegex(pat) {
|
|
20
24
|
return new RegExp(
|
|
@@ -27,62 +31,30 @@ function globToRegex(pat) {
|
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
34
|
+
* Load root .gitignore into an `ignore` instance.
|
|
35
|
+
* @param {string} rootDir - absolute path to tree root
|
|
36
|
+
* @returns {ReturnType<typeof ignore>|null}
|
|
32
37
|
*/
|
|
33
|
-
function
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
for (const raw of lines) {
|
|
37
|
-
const line = raw.trim();
|
|
38
|
-
if (!line || line.startsWith("#")) continue;
|
|
39
|
-
const negate = line.startsWith("!");
|
|
40
|
-
let pat = negate ? line.slice(1) : line;
|
|
41
|
-
// Strip leading /
|
|
42
|
-
if (pat.startsWith("/")) pat = pat.slice(1);
|
|
43
|
-
// Strip trailing /
|
|
44
|
-
const dirOnly = pat.endsWith("/");
|
|
45
|
-
if (dirOnly) pat = pat.slice(0, -1);
|
|
46
|
-
const re = globToRegex(pat);
|
|
47
|
-
patterns.push({ re, negate, dirOnly });
|
|
48
|
-
}
|
|
49
|
-
return patterns;
|
|
50
|
-
}
|
|
51
|
-
function isIgnored(name, isDir, patterns) {
|
|
52
|
-
let ignored = false;
|
|
53
|
-
for (const { re, negate, dirOnly } of patterns) {
|
|
54
|
-
if (dirOnly && !isDir) continue;
|
|
55
|
-
if (re.test(name)) ignored = !negate;
|
|
56
|
-
}
|
|
57
|
-
return ignored;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function formatSize(bytes) {
|
|
61
|
-
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
62
|
-
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
63
|
-
return `${bytes}B`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function countFileLines(filePath, size) {
|
|
67
|
-
if (size === 0 || size > 1_000_000) return null;
|
|
38
|
+
function loadGitignore(rootDir) {
|
|
39
|
+
const gi = join(rootDir, ".gitignore");
|
|
40
|
+
if (!existsSync(gi)) return null;
|
|
68
41
|
try {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
for (let i = 0; i < checkLen; i++) if (buf[i] === 0) return null; // binary
|
|
72
|
-
let count = 1;
|
|
73
|
-
for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0A) count++;
|
|
74
|
-
return count;
|
|
42
|
+
const content = readFileSync(gi, "utf-8");
|
|
43
|
+
return ignore().add(content);
|
|
75
44
|
} catch { return null; }
|
|
76
45
|
}
|
|
77
46
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Check if a relative path should be ignored.
|
|
49
|
+
* @param {ReturnType<typeof ignore>|null} ig - ignore instance (null = no gitignore)
|
|
50
|
+
* @param {string} relPath - POSIX relative path from tree root
|
|
51
|
+
* @param {boolean} isDir - true if directory
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isIgnored(ig, relPath, isDir) {
|
|
55
|
+
if (!ig) return false;
|
|
56
|
+
// ignore package expects dir paths to end with /
|
|
57
|
+
return ig.ignores(isDir ? relPath + "/" : relPath);
|
|
86
58
|
}
|
|
87
59
|
|
|
88
60
|
/**
|
|
@@ -95,22 +67,12 @@ function findByPattern(dirPath, opts) {
|
|
|
95
67
|
const re = globToRegex(opts.pattern);
|
|
96
68
|
const filterType = opts.type || "all";
|
|
97
69
|
const maxDepth = opts.max_depth ?? 20;
|
|
98
|
-
const useGitignore = opts.gitignore ?? true;
|
|
99
70
|
|
|
100
|
-
const
|
|
101
|
-
? dirPath[1] + ":" + dirPath.slice(2) : dirPath;
|
|
102
|
-
const abs = resolve(normalized);
|
|
71
|
+
const abs = resolve(normalizePath(dirPath));
|
|
103
72
|
if (!existsSync(abs)) throw new Error(`DIRECTORY_NOT_FOUND: ${abs}`);
|
|
104
73
|
if (!statSync(abs).isDirectory()) throw new Error(`Not a directory: ${abs}`);
|
|
105
74
|
|
|
106
|
-
|
|
107
|
-
if (useGitignore) {
|
|
108
|
-
const gi = join(abs, ".gitignore");
|
|
109
|
-
if (existsSync(gi)) {
|
|
110
|
-
try { patterns = parseGitignore(readFileSync(gi, "utf-8")); } catch { /* skip */ }
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
75
|
+
const ig = (opts.gitignore ?? true) ? loadGitignore(abs) : null;
|
|
114
76
|
const matches = [];
|
|
115
77
|
|
|
116
78
|
function walk(dir, depth) {
|
|
@@ -121,15 +83,15 @@ function findByPattern(dirPath, opts) {
|
|
|
121
83
|
for (const entry of entries) {
|
|
122
84
|
const isDir = entry.isDirectory();
|
|
123
85
|
if (SKIP_DIRS.has(entry.name) && isDir) continue;
|
|
124
|
-
if (isIgnored(entry.name, isDir, patterns)) continue;
|
|
125
86
|
|
|
126
87
|
const full = join(dir, entry.name);
|
|
88
|
+
const rel = relative(abs, full).replace(/\\/g, "/");
|
|
89
|
+
if (isIgnored(ig, rel, isDir)) continue;
|
|
127
90
|
|
|
128
91
|
if (re.test(entry.name)) {
|
|
129
92
|
if (filterType === "all" ||
|
|
130
93
|
(filterType === "dir" && isDir) ||
|
|
131
94
|
(filterType === "file" && !isDir)) {
|
|
132
|
-
const rel = relative(abs, full).replace(/\\/g, "/");
|
|
133
95
|
matches.push(isDir ? rel + "/" : rel);
|
|
134
96
|
}
|
|
135
97
|
}
|
|
@@ -159,35 +121,28 @@ export function directoryTree(dirPath, opts = {}) {
|
|
|
159
121
|
|
|
160
122
|
const compact = opts.format === "compact";
|
|
161
123
|
const maxDepth = compact ? 1 : (opts.max_depth ?? 3);
|
|
162
|
-
const useGitignore = opts.gitignore ?? true;
|
|
163
124
|
|
|
164
|
-
|
|
165
|
-
const normalized = (process.platform === "win32" && /^\/[a-zA-Z]\//.test(dirPath))
|
|
166
|
-
? dirPath[1] + ":" + dirPath.slice(2) : dirPath;
|
|
167
|
-
const abs = resolve(normalized);
|
|
125
|
+
const abs = resolve(normalizePath(dirPath));
|
|
168
126
|
if (!existsSync(abs)) throw new Error(`DIRECTORY_NOT_FOUND: ${abs}. Check path or use directory_tree on parent directory.`);
|
|
169
127
|
const rootStat = statSync(abs);
|
|
170
128
|
if (!rootStat.isDirectory()) throw new Error(`Not a directory: ${abs}`);
|
|
171
129
|
|
|
172
|
-
|
|
173
|
-
let patterns = [];
|
|
174
|
-
if (useGitignore) {
|
|
175
|
-
const gi = join(abs, ".gitignore");
|
|
176
|
-
if (existsSync(gi)) {
|
|
177
|
-
try { patterns = parseGitignore(readFileSync(gi, "utf-8")); } catch { /* skip */ }
|
|
178
|
-
}
|
|
179
|
-
}
|
|
130
|
+
const ig = (opts.gitignore ?? true) ? loadGitignore(abs) : null;
|
|
180
131
|
|
|
181
132
|
let totalFiles = 0;
|
|
182
133
|
let totalSize = 0;
|
|
183
134
|
const lines = [];
|
|
184
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Recursive walk. Returns total file count for entire subtree
|
|
138
|
+
* (including beyond maxDepth — count is always full, display is depth-limited).
|
|
139
|
+
* Output order: pre-order (dir line before children).
|
|
140
|
+
*/
|
|
185
141
|
function walk(dir, prefix, depth) {
|
|
186
|
-
if (depth > maxDepth) return;
|
|
187
142
|
let entries;
|
|
188
143
|
try {
|
|
189
144
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
190
|
-
} catch { return; }
|
|
145
|
+
} catch { return 0; }
|
|
191
146
|
|
|
192
147
|
// Sort: directories first, then files, alphabetical
|
|
193
148
|
entries.sort((a, b) => {
|
|
@@ -197,27 +152,35 @@ export function directoryTree(dirPath, opts = {}) {
|
|
|
197
152
|
return a.name.localeCompare(b.name);
|
|
198
153
|
});
|
|
199
154
|
|
|
155
|
+
let subTotal = 0;
|
|
156
|
+
|
|
200
157
|
for (const entry of entries) {
|
|
201
158
|
const name = entry.name;
|
|
202
159
|
const isDir = entry.isDirectory();
|
|
203
160
|
|
|
204
161
|
if (SKIP_DIRS.has(name) && isDir) continue;
|
|
205
|
-
if (isIgnored(name, isDir, patterns)) continue;
|
|
206
162
|
|
|
207
163
|
const full = join(dir, name);
|
|
164
|
+
const rel = relative(abs, full).replace(/\\/g, "/");
|
|
165
|
+
if (isIgnored(ig, rel, isDir)) continue;
|
|
208
166
|
|
|
209
167
|
if (isDir) {
|
|
210
168
|
if (compact) {
|
|
211
169
|
lines.push(`${prefix}${name}/`);
|
|
212
170
|
} else {
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
171
|
+
// Pre-order: placeholder for dir line, patch after recursion
|
|
172
|
+
const lineIdx = lines.length;
|
|
173
|
+
lines.push("");
|
|
174
|
+
const count = depth < maxDepth
|
|
175
|
+
? walk(full, prefix + " ", depth + 1)
|
|
176
|
+
: countSubtreeFiles(full, ig, abs);
|
|
177
|
+
lines[lineIdx] = `${prefix}${name}/ (${count} files)`;
|
|
178
|
+
subTotal += count;
|
|
217
179
|
}
|
|
218
|
-
walk(full, prefix + " ", depth + 1);
|
|
180
|
+
if (compact) walk(full, prefix + " ", depth + 1);
|
|
219
181
|
} else {
|
|
220
182
|
totalFiles++;
|
|
183
|
+
subTotal++;
|
|
221
184
|
if (compact) {
|
|
222
185
|
lines.push(`${prefix}${name}`);
|
|
223
186
|
} else {
|
|
@@ -232,26 +195,35 @@ export function directoryTree(dirPath, opts = {}) {
|
|
|
232
195
|
const parts = [];
|
|
233
196
|
if (lineCount !== null) parts.push(`${lineCount}L`);
|
|
234
197
|
parts.push(formatSize(size));
|
|
235
|
-
if (mtime) parts.push(relativeTime(mtime));
|
|
198
|
+
if (mtime) parts.push(relativeTime(mtime, true));
|
|
236
199
|
lines.push(`${prefix}${name} (${parts.join(", ")})`);
|
|
237
200
|
}
|
|
238
201
|
}
|
|
239
202
|
}
|
|
203
|
+
|
|
204
|
+
return subTotal;
|
|
240
205
|
}
|
|
241
206
|
|
|
242
|
-
|
|
243
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Count files in subtree without emitting lines (for dirs beyond maxDepth).
|
|
209
|
+
*/
|
|
210
|
+
function countSubtreeFiles(dir, ig, rootAbs, depth = 0) {
|
|
211
|
+
if (depth > 10) return 0;
|
|
244
212
|
let entries;
|
|
245
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
213
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return 0; }
|
|
214
|
+
let count = 0;
|
|
246
215
|
for (const entry of entries) {
|
|
247
216
|
if (SKIP_DIRS.has(entry.name) && entry.isDirectory()) continue;
|
|
248
|
-
|
|
217
|
+
const full = join(dir, entry.name);
|
|
218
|
+
const rel = relative(rootAbs, full).replace(/\\/g, "/");
|
|
219
|
+
if (isIgnored(ig, rel, entry.isDirectory())) continue;
|
|
249
220
|
if (entry.isDirectory()) {
|
|
250
|
-
|
|
221
|
+
count += countSubtreeFiles(full, ig, rootAbs, depth + 1);
|
|
251
222
|
} else {
|
|
252
|
-
|
|
223
|
+
count++;
|
|
253
224
|
}
|
|
254
225
|
}
|
|
226
|
+
return count;
|
|
255
227
|
}
|
|
256
228
|
|
|
257
229
|
const rootName = basename(abs);
|
package/lib/verify.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Validates range checksums from prior reads.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
7
6
|
import { fnv1a, rangeChecksum, parseChecksum } from "./hash.mjs";
|
|
8
7
|
import { validatePath } from "./security.mjs";
|
|
8
|
+
import { readText } from "./format.mjs";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Verify checksums against current file state.
|
|
@@ -16,7 +16,7 @@ import { validatePath } from "./security.mjs";
|
|
|
16
16
|
*/
|
|
17
17
|
export function verifyChecksums(filePath, checksums) {
|
|
18
18
|
const real = validatePath(filePath);
|
|
19
|
-
const content =
|
|
19
|
+
const content = readText(real);
|
|
20
20
|
const lines = content.split("\n");
|
|
21
21
|
|
|
22
22
|
// Pre-compute all line hashes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 11 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, setup_hooks, changes, bulk_replace.",
|
|
6
6
|
"main": "server.mjs",
|
|
@@ -25,8 +25,10 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
27
27
|
"diff": "^8.0.3",
|
|
28
|
+
"ignore": "^7.0.5",
|
|
28
29
|
"tree-sitter-wasms": "^0.1.0",
|
|
29
|
-
"web-tree-sitter": "^0.25.0"
|
|
30
|
+
"web-tree-sitter": "^0.25.0",
|
|
31
|
+
"zod": "^3.24.0"
|
|
30
32
|
},
|
|
31
33
|
"author": "Lev Nikolaevich <https://github.com/levnikolaevich>",
|
|
32
34
|
"license": "MIT",
|
package/server.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
12
12
|
import { dirname } from "node:path";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
const { version } = createRequire(import.meta.url)("./package.json");
|
|
13
15
|
import { z } from "zod";
|
|
14
16
|
// LLM clients may send booleans as strings ("true"/"false").
|
|
15
17
|
// z.coerce.boolean() is unsafe: Boolean("false") === true.
|
|
@@ -54,7 +56,7 @@ try {
|
|
|
54
56
|
process.exit(1);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
const server = new McpServer({ name: "hex-line-mcp", version
|
|
59
|
+
const server = new McpServer({ name: "hex-line-mcp", version });
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
// ==================== read_file ====================
|
|
@@ -101,17 +103,19 @@ server.registerTool("read_file", {
|
|
|
101
103
|
server.registerTool("edit_file", {
|
|
102
104
|
title: "Edit File",
|
|
103
105
|
description:
|
|
104
|
-
"Edit a file using hash-verified anchors or text replacement. Returns diff. " +
|
|
105
|
-
"
|
|
106
|
-
"
|
|
106
|
+
"Edit a file using hash-verified anchors or text replacement. Returns diff + post-edit checksums. " +
|
|
107
|
+
"Batch multiple edits in ONE call for atomicity (bottom-to-top auto-sorted). " +
|
|
108
|
+
"set_line: single line (from grep/read). replace_lines: range with checksum. " +
|
|
109
|
+
"replace: unique text match, or all:true for rename. insert_after: add below anchor.",
|
|
107
110
|
inputSchema: z.object({
|
|
108
111
|
path: z.string().describe("File to edit"),
|
|
109
112
|
edits: z.string().describe(
|
|
110
113
|
'JSON array. Examples:\n' +
|
|
111
114
|
'{"set_line":{"anchor":"ab.12","new_text":"new"}} — replace line\n' +
|
|
112
|
-
'{"replace_lines":{"start_anchor":"ab.10","end_anchor":"cd.15","new_text":"...","range_checksum":"10-15:a1b2c3d4"}} — range
|
|
115
|
+
'{"replace_lines":{"start_anchor":"ab.10","end_anchor":"cd.15","new_text":"...","range_checksum":"10-15:a1b2c3d4"}} — range\n' +
|
|
113
116
|
'{"insert_after":{"anchor":"ab.20","text":"inserted"}} — insert below\n' +
|
|
114
|
-
'{"replace":{"old_text":"find","new_text":"replace"
|
|
117
|
+
'{"replace":{"old_text":"find","new_text":"replace"}} — unique match\n' +
|
|
118
|
+
'{"replace":{"old_text":"find","new_text":"replace","all":true}} — replace all',
|
|
115
119
|
),
|
|
116
120
|
dry_run: flexBool().describe("Preview changes without writing"),
|
|
117
121
|
restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
|
|
@@ -159,25 +163,36 @@ server.registerTool("write_file", {
|
|
|
159
163
|
server.registerTool("grep_search", {
|
|
160
164
|
title: "Search Files",
|
|
161
165
|
description:
|
|
162
|
-
"Search file contents with ripgrep. Returns hash-annotated matches for direct editing. " +
|
|
163
|
-
"
|
|
166
|
+
"Search file contents with ripgrep. Returns hash-annotated matches with per-group checksums for direct editing. " +
|
|
167
|
+
"Output modes: content (default, edit-ready hashes+checksums), files (paths only), count (match counts). " +
|
|
168
|
+
"For single-line edits: grep -> set_line directly. For range edits: use checksum from grep output. " +
|
|
169
|
+
"ALWAYS prefer over shell grep/rg/findstr.",
|
|
164
170
|
inputSchema: z.object({
|
|
165
|
-
pattern: z.string().describe("
|
|
171
|
+
pattern: z.string().describe("Search pattern (regex by default, literal if literal:true)"),
|
|
166
172
|
path: z.string().optional().describe("Search dir/file (default: cwd)"),
|
|
167
173
|
glob: z.string().optional().describe('Glob filter (e.g. "*.ts")'),
|
|
168
174
|
type: z.string().optional().describe('File type (e.g. "js", "py")'),
|
|
175
|
+
output: z.enum(["content", "files", "count"]).optional().describe('Output format (default: content)'),
|
|
169
176
|
case_insensitive: flexBool().describe("Ignore case (-i)"),
|
|
170
|
-
smart_case: flexBool().describe("CI when pattern is all lowercase, CS if
|
|
171
|
-
|
|
177
|
+
smart_case: flexBool().describe("CI when pattern is all lowercase, CS if uppercase (-S)"),
|
|
178
|
+
literal: flexBool().describe("Literal string search, no regex (-F)"),
|
|
179
|
+
multiline: flexBool().describe("Pattern can span multiple lines (-U)"),
|
|
180
|
+
context: flexNum().describe("Symmetric context lines around matches (-C)"),
|
|
181
|
+
context_before: flexNum().describe("Context lines BEFORE match (-B)"),
|
|
182
|
+
context_after: flexNum().describe("Context lines AFTER match (-A)"),
|
|
172
183
|
limit: flexNum().describe("Max matches per file (default: 100)"),
|
|
184
|
+
total_limit: flexNum().describe("Total match events across all files; multiline matches count as 1 (0 = unlimited)"),
|
|
173
185
|
plain: flexBool().describe("Omit hash tags, return file:line:content"),
|
|
174
186
|
}),
|
|
175
187
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
176
188
|
}, async (rawParams) => {
|
|
177
|
-
const { pattern, path: p, glob, type, case_insensitive, smart_case,
|
|
189
|
+
const { pattern, path: p, glob, type, output, case_insensitive, smart_case, literal, multiline,
|
|
190
|
+
context, context_before, context_after, limit, total_limit, plain } = coerceParams(rawParams);
|
|
178
191
|
try {
|
|
179
192
|
const result = await grepSearch(pattern, {
|
|
180
|
-
path: p, glob, type, caseInsensitive: case_insensitive, smartCase: smart_case,
|
|
193
|
+
path: p, glob, type, output, caseInsensitive: case_insensitive, smartCase: smart_case,
|
|
194
|
+
literal, multiline, context, contextBefore: context_before, contextAfter: context_after,
|
|
195
|
+
limit, totalLimit: total_limit, plain,
|
|
181
196
|
});
|
|
182
197
|
return { content: [{ type: "text", text: result }] };
|
|
183
198
|
} catch (e) {
|
|
@@ -238,7 +253,7 @@ server.registerTool("verify", {
|
|
|
238
253
|
server.registerTool("directory_tree", {
|
|
239
254
|
title: "Directory Tree",
|
|
240
255
|
description:
|
|
241
|
-
"Compact directory tree with .gitignore support. " +
|
|
256
|
+
"Compact directory tree with root .gitignore support (path-based rules, negation). " +
|
|
242
257
|
"Supports pattern glob to find files/dirs by name (like find -name). " +
|
|
243
258
|
"Use to understand repo structure or find specific files/dirs. " +
|
|
244
259
|
"Skips node_modules, .git, dist by default.",
|
|
@@ -247,7 +262,7 @@ server.registerTool("directory_tree", {
|
|
|
247
262
|
pattern: z.string().optional().describe('Glob filter on names (e.g. "*-mcp", "*.mjs"). Returns flat match list instead of tree'),
|
|
248
263
|
type: z.enum(["file", "dir", "all"]).optional().describe('"file", "dir", or "all" (default). Like find -type f/d'),
|
|
249
264
|
max_depth: flexNum().describe("Max recursion depth (default: 3, or 20 in pattern mode)"),
|
|
250
|
-
gitignore: flexBool().describe("Respect .gitignore patterns (default: true)"),
|
|
265
|
+
gitignore: flexBool().describe("Respect root .gitignore patterns (default: true). Nested .gitignore not supported"),
|
|
251
266
|
format: z.enum(["compact", "full"]).optional().describe('"compact" = names only, no sizes, depth 1. "full" = default with sizes'),
|
|
252
267
|
}),
|
|
253
268
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
@@ -365,4 +380,4 @@ server.registerTool("bulk_replace", {
|
|
|
365
380
|
|
|
366
381
|
const transport = new StdioServerTransport();
|
|
367
382
|
await server.connect(transport);
|
|
368
|
-
void checkForUpdates("@levnikolaevich/hex-line-mcp",
|
|
383
|
+
void checkForUpdates("@levnikolaevich/hex-line-mcp", version);
|