@levnikolaevich/hex-line-mcp 1.1.2 → 1.3.0

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 CHANGED
@@ -3,7 +3,9 @@
3
3
  Hash-verified file editing MCP + token efficiency hook for AI coding agents.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@levnikolaevich/hex-line-mcp)](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp)
6
- ![License](https://img.shields.io/badge/license-MIT-green)
6
+ [![downloads](https://img.shields.io/npm/dm/@levnikolaevich/hex-line-mcp)](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp)
7
+ [![license](https://img.shields.io/npm/l/@levnikolaevich/hex-line-mcp)](./LICENSE)
8
+ ![node](https://img.shields.io/node/v/@levnikolaevich/hex-line-mcp)
7
9
 
8
10
  Every line carries an FNV-1a content hash. Every edit must present those hashes back -- proving the agent is editing what it thinks it's editing. No stale context, no silent corruption.
9
11
 
@@ -106,6 +108,7 @@ Read a file with FNV-1a hash-annotated lines and range checksums. Supports direc
106
108
  | Parameter | Type | Required | Description |
107
109
  |-----------|------|----------|-------------|
108
110
  | `path` | string | yes | File or directory path |
111
+ | `paths` | string[] | no | Array of file paths to read (batch mode) |
109
112
  | `offset` | number | no | Start line, 1-indexed (default: 1) |
110
113
  | `limit` | number | no | Max lines to return (default: 2000, 0 = all) |
111
114
  | `plain` | boolean | no | Omit hashes, output `lineNum\|content` instead |
@@ -161,8 +164,10 @@ Search file contents using ripgrep with hash-annotated results.
161
164
  | `glob` | string | no | Glob filter, e.g. `"*.ts"` |
162
165
  | `type` | string | no | File type filter, e.g. `"js"`, `"py"` |
163
166
  | `case_insensitive` | boolean | no | Ignore case |
167
+ | `smart_case` | boolean | no | Case-insensitive when pattern is all lowercase, case-sensitive if it has uppercase (`-S`) |
164
168
  | `context` | number | no | Context lines around matches |
165
169
  | `limit` | number | no | Max matches per file (default: 100) |
170
+ | `plain` | boolean | no | Omit hash tags, return `file:line:content` |
166
171
 
167
172
  ### outline
168
173
 
@@ -194,8 +199,11 @@ Compact directory tree with .gitignore support and file sizes.
194
199
  | Parameter | Type | Required | Description |
195
200
  |-----------|------|----------|-------------|
196
201
  | `path` | string | yes | Directory path |
197
- | `max_depth` | number | no | Max recursion depth (default: 3) |
202
+ | `pattern` | string | no | Glob filter on names (e.g. `"*-mcp"`, `"*.mjs"`). Returns flat match list instead of tree |
203
+ | `type` | string | no | `"file"`, `"dir"`, or `"all"` (default). Like `find -type f/d` |
204
+ | `max_depth` | number | no | Max recursion depth (default: 3, or 20 in pattern mode) |
198
205
  | `gitignore` | boolean | no | Respect .gitignore patterns (default: true) |
206
+ | `format` | string | no | `"compact"` = names only, no sizes, depth 1. `"full"` = default with sizes |
199
207
 
200
208
  Skips `node_modules`, `.git`, `dist`, `build`, `__pycache__`, `.next`, `coverage` by default.
201
209
 
@@ -283,16 +291,6 @@ FNV-1a accumulator over all line hashes in the range (little-endian byte feed).
283
291
  - Write path validation (ancestor directory must exist)
284
292
  - Directory restrictions delegated to Claude Code sandbox
285
293
 
286
- ## Differences from trueline-mcp
287
-
288
- | Aspect | hex-line-mcp | trueline-mcp |
289
- |--------|---------------|--------------|
290
- | Hash algorithm | FNV-1a (pure JS, zero dependencies) | xxHash (native addon) |
291
- | Diff output | Compact unified diff via `diff` npm package | Custom diff implementation |
292
- | Hook | Unified `hook.mjs` (reminder + RTK filter) | Separate hook scripts |
293
- | Path security | Canonicalization + binary detection, no ALLOWED_DIRS | Explicit ALLOWED_DIRS allowlist |
294
- | Transport | stdio only | stdio |
295
- | Outline | tree-sitter WASM (15+ languages) | tree-sitter WASM |
296
294
 
297
295
  ## FAQ
298
296
 
@@ -331,6 +329,14 @@ Yes. Remove the PreToolUse hook from `.claude/settings.local.json`. The MCP tool
331
329
 
332
330
  </details>
333
331
 
332
+ ## Hex Family
333
+
334
+ | Package | Purpose | npm |
335
+ |---------|---------|-----|
336
+ | [hex-line-mcp](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp) | Local file editing with hash verification + hooks | [![npm](https://img.shields.io/npm/v/@levnikolaevich/hex-line-mcp)](https://www.npmjs.com/package/@levnikolaevich/hex-line-mcp) |
337
+ | [hex-ssh-mcp](https://www.npmjs.com/package/@levnikolaevich/hex-ssh-mcp) | Remote file editing over SSH | [![npm](https://img.shields.io/npm/v/@levnikolaevich/hex-ssh-mcp)](https://www.npmjs.com/package/@levnikolaevich/hex-ssh-mcp) |
338
+ | [hex-graph-mcp](https://www.npmjs.com/package/@levnikolaevich/hex-graph-mcp) | Code knowledge graph with AST indexing | [![npm](https://img.shields.io/npm/v/@levnikolaevich/hex-graph-mcp)](https://www.npmjs.com/package/@levnikolaevich/hex-graph-mcp) |
339
+
334
340
  ## License
335
341
 
336
342
  MIT
package/hook.mjs CHANGED
@@ -39,17 +39,31 @@ const BINARY_EXT = new Set([
39
39
  ".ttf", ".otf", ".woff", ".woff2",
40
40
  ]);
41
41
 
42
+ const REVERSE_TOOL_HINTS = {
43
+ "mcp__hex-line__read_file": "Read (file_path, offset, limit)",
44
+ "mcp__hex-line__edit_file": "Edit (file_path, old_string, new_string)",
45
+ "mcp__hex-line__write_file": "Write (file_path, content)",
46
+ "mcp__hex-line__grep_search": "Grep (pattern, path)",
47
+ "mcp__hex-line__directory_tree": "Glob (pattern) or Bash(ls)",
48
+ "mcp__hex-line__get_file_info": "Bash(stat/wc)",
49
+ "mcp__hex-line__outline": "Read with offset/limit",
50
+ "mcp__hex-line__verify": "Read the file again with Read",
51
+ "mcp__hex-line__changes": "Bash(git diff)",
52
+ "mcp__hex-line__bulk_replace": "Edit for each file",
53
+ "mcp__hex-line__setup_hooks": "Not available (hex-line disabled)",
54
+ };
55
+
42
56
  const TOOL_HINTS = {
43
57
  Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
44
58
  Edit: "mcp__hex-line__edit_file (not Edit, not sed -i). read_file first for hashes",
45
59
  Write: "mcp__hex-line__write_file (not Write). No prior Read needed",
46
- Grep: "mcp__hex-line__grep_search (not Grep, not grep/rg)",
60
+ Grep: "mcp__hex-line__grep_search (not Grep). Params: case_insensitive, smart_case",
47
61
  cat: "mcp__hex-line__read_file (not cat/head/tail/less/more)",
48
62
  head: "mcp__hex-line__read_file with limit param (not head)",
49
63
  tail: "mcp__hex-line__read_file with offset param (not tail)",
50
64
  ls: "mcp__hex-line__directory_tree with pattern param (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
51
65
  stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
52
- grep: "mcp__hex-line__grep_search (not grep/rg)",
66
+ grep: "mcp__hex-line__grep_search (not grep/rg). Params: case_insensitive, smart_case",
53
67
  sed: "mcp__hex-line__edit_file (not sed -i). read_file first for hashes",
54
68
  diff: "mcp__hex-line__changes (not diff). Git-based semantic diff",
55
69
  outline: "mcp__hex-line__outline (before reading large code files)",
@@ -138,6 +152,39 @@ function detectCommandType(cmd) {
138
152
  return "generic";
139
153
  }
140
154
 
155
+ /** Cache: null = not computed yet */
156
+ let _hexLineDisabled = null;
157
+
158
+ /**
159
+ * Check if hex-line MCP is disabled for the current project.
160
+ * Reads ~/.claude.json → projects.{cwd}.disabledMcpServers.
161
+ * Fail-open: returns false on any error.
162
+ */
163
+ function isHexLineDisabled(configPath) {
164
+ if (_hexLineDisabled !== null) return _hexLineDisabled;
165
+ _hexLineDisabled = false;
166
+ try {
167
+ const p = configPath || resolve(homedir(), ".claude.json");
168
+ const claudeJson = JSON.parse(readFileSync(p, "utf-8"));
169
+ const projects = claudeJson.projects;
170
+ if (!projects || typeof projects !== "object") return _hexLineDisabled;
171
+ const cwd = process.cwd().replace(/\\/g, "/").replace(/\/$/, "").toLowerCase();
172
+ for (const [path, config] of Object.entries(projects)) {
173
+ if (path.replace(/\\/g, "/").replace(/\/$/, "").toLowerCase() === cwd) {
174
+ const disabled = config.disabledMcpServers;
175
+ if (Array.isArray(disabled) && disabled.includes("hex-line")) {
176
+ _hexLineDisabled = true;
177
+ }
178
+ break;
179
+ }
180
+ }
181
+ } catch { /* fail open */ }
182
+ return _hexLineDisabled;
183
+ }
184
+
185
+ /** Reset cache (for testing). */
186
+ function _resetHexLineDisabledCache() { _hexLineDisabled = null; }
187
+
141
188
  function block(reason, context) {
142
189
  const output = {
143
190
  hookSpecificOutput: {
@@ -237,6 +284,28 @@ function handlePreToolUse(data) {
237
284
  process.exit(0);
238
285
  }
239
286
 
287
+ // ---- PreToolUse REVERSE handler (hex-line disabled) ----
288
+
289
+ function handlePreToolUseReverse(data) {
290
+ const toolName = data.tool_name || "";
291
+
292
+ // Agent tries hex-line tool that's disabled → redirect to built-in
293
+ if (toolName.startsWith("mcp__hex-line__")) {
294
+ const builtIn = REVERSE_TOOL_HINTS[toolName];
295
+ if (builtIn) {
296
+ const target = builtIn.split(" ")[0];
297
+ block(
298
+ `hex-line is disabled in this project. Use ${target}`,
299
+ `hex-line disabled. Use built-in: ${builtIn}`
300
+ );
301
+ }
302
+ block("hex-line is disabled in this project", "Disabled via project settings");
303
+ }
304
+
305
+ // All built-in tools — approve silently
306
+ process.exit(0);
307
+ }
308
+
240
309
  // ---- PostToolUse handler ----
241
310
 
242
311
  function handlePostToolUse(data) {
@@ -341,6 +410,13 @@ process.stdin.on("end", () => {
341
410
  const data = JSON.parse(input);
342
411
  const event = data.hook_event_name || "";
343
412
 
413
+ if (isHexLineDisabled()) {
414
+ // REVERSE MODE: block hex-line calls, approve everything else
415
+ if (event === "PreToolUse") handlePreToolUseReverse(data);
416
+ process.exit(0); // SessionStart, PostToolUse — silent exit
417
+ }
418
+
419
+ // NORMAL MODE
344
420
  if (event === "SessionStart") handleSessionStart();
345
421
  else if (event === "PreToolUse") handlePreToolUse(data);
346
422
  else if (event === "PostToolUse") handlePostToolUse(data);
@@ -349,3 +425,6 @@ process.stdin.on("end", () => {
349
425
  process.exit(0);
350
426
  }
351
427
  });
428
+
429
+ // ---- Exports for testing ----
430
+ export { isHexLineDisabled, _resetHexLineDisabledCache };
package/lib/edit.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { readFileSync, writeFileSync } from "node:fs";
12
12
  import { diffLines } from "diff";
13
- import { fnv1a, lineTag, rangeChecksum } from "./hash.mjs";
13
+ import { fnv1a, lineTag, rangeChecksum, parseChecksum } from "./hash.mjs";
14
14
  import { validatePath } from "./security.mjs";
15
15
  import { getGraphDB, blastRadius, getRelativePath } from "./graph-enrich.mjs";
16
16
 
@@ -348,12 +348,33 @@ export function editFile(filePath, edits, opts = {}) {
348
348
  // Range checksum verification (mandatory)
349
349
  const rc = e.replace_lines.range_checksum;
350
350
  if (!rc) throw new Error("range_checksum required for replace_lines. Read the range first via read_file, then pass its checksum.");
351
- const rcHex = rc.includes(":") ? rc.split(":")[1] : rc;
351
+
352
+ // Checksum's range is authoritative (from read_file), not anchor range
353
+ const { start: csStart, end: csEnd, hex: csHex } = parseChecksum(rc);
354
+
355
+ // Coverage check: checksum range must contain ACTUAL edit range (after relocation)
356
+ const actualStart = si + 1;
357
+ const actualEnd = ei + 1;
358
+ if (csStart > actualStart || csEnd < actualEnd) {
359
+ throw new Error(
360
+ `Checksum range ${csStart}-${csEnd} does not cover edit range ${actualStart}-${actualEnd}. ` +
361
+ `Re-read lines ${actualStart}-${actualEnd} first.`
362
+ );
363
+ }
364
+
365
+ // Verify freshness over checksum's own range using origLines snapshot
366
+ const csStartIdx = csStart - 1;
367
+ const csEndIdx = csEnd - 1;
368
+ if (csStartIdx < 0 || csEndIdx >= origLines.length) {
369
+ throw new Error(`Checksum range ${csStart}-${csEnd} out of bounds (file has ${origLines.length} lines). Re-read the file.`);
370
+ }
352
371
  const lineHashes = [];
353
- for (let i = si; i <= ei; i++) lineHashes.push(fnv1a(lines[i]));
354
- const actual = rangeChecksum(lineHashes, s.line, en.line);
372
+ for (let i = csStartIdx; i <= csEndIdx; i++) lineHashes.push(fnv1a(origLines[i]));
373
+ const actual = rangeChecksum(lineHashes, csStart, csEnd);
355
374
  const actualHex = actual.split(":")[1];
356
- if (rcHex !== actualHex) throw new Error(`Range checksum mismatch: expected ${rc}, got ${actual}. File changed \u2014 re-read lines ${s.line}-${en.line}.`);
375
+ if (csHex !== actualHex) {
376
+ throw new Error(`Range checksum mismatch: expected ${rc}, got ${actual}. File changed \u2014 re-read lines ${csStart}-${csEnd}.`);
377
+ }
357
378
 
358
379
  const txt = e.replace_lines.new_text;
359
380
  if (!txt && txt !== 0) {
package/lib/hash.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * FNV-1a hashing for hash-verified file editing.
3
3
  *
4
- * Trueline-compatible: 2-char tags from 32-symbol alphabet,
4
+ * 2-char tags from 32-symbol alphabet,
5
5
  * range checksums as FNV-1a accumulator over line hashes.
6
6
  *
7
7
  * Line format: {tag}.{lineNum}\t{content}
package/lib/search.mjs CHANGED
@@ -30,6 +30,7 @@ export function grepSearch(pattern, opts = {}) {
30
30
  const plain = !!opts.plain;
31
31
 
32
32
  if (opts.caseInsensitive) args.push("-i");
33
+ else if (opts.smartCase) args.push("-S");
33
34
  if (opts.context && opts.context > 0) args.push("-C", String(opts.context));
34
35
  if (opts.glob) args.push("--glob", opts.glob);
35
36
  if (opts.type) args.push("--type", opts.type);
package/lib/setup.mjs CHANGED
@@ -36,7 +36,7 @@ const CLAUDE_HOOKS = {
36
36
  hooks: [{ type: "command", command: HOOK_COMMAND, timeout: 5 }],
37
37
  },
38
38
  PreToolUse: {
39
- matcher: "Read|Edit|Write|Grep|Bash",
39
+ matcher: "Read|Edit|Write|Grep|Bash|mcp__hex-line__.*",
40
40
  hooks: [{ type: "command", command: HOOK_COMMAND, timeout: 5 }],
41
41
  },
42
42
  PostToolUse: {
@@ -230,18 +230,54 @@ function setupCodex() {
230
230
  return "Codex: Not supported (Codex CLI does not support hooks. Add MCP Tool Preferences to AGENTS.md instead)";
231
231
  }
232
232
 
233
+ // ---- Uninstall: remove hex-line hooks ----
234
+
235
+ function uninstallClaude() {
236
+ const globalPath = resolve(homedir(), ".claude/settings.json");
237
+ const config = readJson(globalPath);
238
+ if (!config || !config.hooks || typeof config.hooks !== "object") {
239
+ return "Claude: no hooks to remove";
240
+ }
241
+
242
+ let changed = false;
243
+ for (const event of Object.keys(CLAUDE_HOOKS)) {
244
+ if (!Array.isArray(config.hooks[event])) continue;
245
+ const idx = findEntryByCommand(config.hooks[event]);
246
+ if (idx >= 0) {
247
+ config.hooks[event].splice(idx, 1);
248
+ if (config.hooks[event].length === 0) delete config.hooks[event];
249
+ changed = true;
250
+ }
251
+ }
252
+
253
+ if (Object.keys(config.hooks).length === 0) delete config.hooks;
254
+
255
+ if (!changed) return "Claude: no hex-line hooks found";
256
+
257
+ writeJson(globalPath, config);
258
+ return "Claude: hex-line hooks removed from global settings";
259
+ }
260
+
233
261
  // ---- Public API ----
234
262
 
235
263
  const AGENTS = { claude: setupClaude, gemini: setupGemini, codex: setupCodex };
264
+ const UNINSTALL_AGENTS = { claude: uninstallClaude };
236
265
 
237
266
  /**
238
267
  * Configure hex-line hooks for one or all supported agents.
239
268
  * Claude: writes to ~/.claude/settings.json (global), cleans per-project hooks.
240
269
  * @param {string} [agent="all"] - "claude", "gemini", "codex", or "all"
270
+ * @param {string} [action="install"] - "install" or "uninstall"
241
271
  * @returns {string} Status report
242
272
  */
243
- export function setupHooks(agent = "all") {
273
+ export function setupHooks(agent = "all", action = "install") {
244
274
  const target = (agent || "all").toLowerCase();
275
+ const act = (action || "install").toLowerCase();
276
+
277
+ if (act === "uninstall") {
278
+ const result = uninstallClaude();
279
+ return `Hooks uninstalled:\n ${result}\n\nRestart Claude Code to apply changes.`;
280
+ }
245
281
 
246
282
  if (target !== "all" && !AGENTS[target]) {
247
283
  throw new Error(`UNKNOWN_AGENT: '${agent}'. Supported: claude, gemini, codex, all`);
package/lib/tree.mjs CHANGED
@@ -63,6 +63,28 @@ function formatSize(bytes) {
63
63
  return `${bytes}B`;
64
64
  }
65
65
 
66
+ function countFileLines(filePath, size) {
67
+ if (size === 0 || size > 1_000_000) return null;
68
+ try {
69
+ const buf = readFileSync(filePath);
70
+ const checkLen = Math.min(buf.length, 8192);
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;
75
+ } catch { return null; }
76
+ }
77
+
78
+ function relativeTime(mtime) {
79
+ const sec = (Date.now() - mtime.getTime()) / 1000;
80
+ if (sec < 60) return "now";
81
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
82
+ if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`;
83
+ if (sec < 604800) return `${Math.floor(sec / 86400)}d ago`;
84
+ if (sec < 2592000) return `${Math.floor(sec / 604800)}w ago`;
85
+ return `${Math.floor(sec / 2592000)}mo ago`;
86
+ }
87
+
66
88
  /**
67
89
  * Find files/dirs by glob pattern. Returns flat list of relative paths.
68
90
  * @param {string} dirPath - Root directory to search
@@ -199,14 +221,19 @@ export function directoryTree(dirPath, opts = {}) {
199
221
  if (compact) {
200
222
  lines.push(`${prefix}${name}`);
201
223
  } else {
202
- let size = 0;
203
- try { size = statSync(full).size; } catch { /* skip */ }
224
+ let size = 0, mtime = null, lineCount = null;
225
+ try {
226
+ const st = statSync(full);
227
+ size = st.size;
228
+ mtime = st.mtime;
229
+ } catch { /* skip */ }
204
230
  totalSize += size;
205
- if (size >= 1024) {
206
- lines.push(`${prefix}${name} (${formatSize(size)})`);
207
- } else {
208
- lines.push(`${prefix}${name}`);
209
- }
231
+ lineCount = countFileLines(full, size);
232
+ const parts = [];
233
+ if (lineCount !== null) parts.push(`${lineCount}L`);
234
+ parts.push(formatSize(size));
235
+ if (mtime) parts.push(relativeTime(mtime));
236
+ lines.push(`${prefix}${name} (${parts.join(", ")})`);
210
237
  }
211
238
  }
212
239
  }
package/output-style.md CHANGED
@@ -6,7 +6,7 @@ keep-coding-instructions: true
6
6
 
7
7
  # MCP Tool Preferences
8
8
 
9
- When `hex-line` MCP is available, **always prefer it** over built-in file tools:
9
+ **MANDATORY:** NEVER use built-in Read, Edit, Write, Grep. Use hex-line MCP equivalents:
10
10
 
11
11
  | Instead of | Use | Why |
12
12
  |-----------|-----|-----|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levnikolaevich/hex-line-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
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",
@@ -28,7 +28,11 @@
28
28
  "tree-sitter-wasms": "^0.1.0",
29
29
  "web-tree-sitter": "^0.25.0"
30
30
  },
31
+ "author": "Lev Nikolaevich <https://github.com/levnikolaevich>",
31
32
  "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/levnikolaevich/claude-code-skills/issues"
35
+ },
32
36
  "keywords": [
33
37
  "mcp",
34
38
  "hex-line",
@@ -40,8 +44,10 @@
40
44
  "hash-verified",
41
45
  "token-efficiency",
42
46
  "hook",
43
- "rtk",
44
- "bulk-replace"
47
+ "bulk-replace",
48
+ "claude",
49
+ "ai",
50
+ "llm"
45
51
  ],
46
52
  "engines": {
47
53
  "node": ">=18.0.0"
package/server.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * hex-line-mcp — MCP server for hash-verified file operations.
4
4
  *
5
5
  * 11 tools: read_file, edit_file, write_file, grep_search, outline, verify, directory_tree, get_file_info, setup_hooks, changes, bulk_replace
6
- * FNV-1a 2-char tags + range checksums (trueline-compatible)
6
+ * FNV-1a 2-char tags + range checksums
7
7
  * Security: root policy, path validation, binary/size rejection
8
8
  * Transport: stdio
9
9
  */
@@ -54,7 +54,7 @@ try {
54
54
  process.exit(1);
55
55
  }
56
56
 
57
- const server = new McpServer({ name: "hex-line-mcp", version: "1.1.2" });
57
+ const server = new McpServer({ name: "hex-line-mcp", version: "1.3.0" });
58
58
 
59
59
 
60
60
  // ==================== read_file ====================
@@ -166,17 +166,18 @@ server.registerTool("grep_search", {
166
166
  path: z.string().optional().describe("Search dir/file (default: cwd)"),
167
167
  glob: z.string().optional().describe('Glob filter (e.g. "*.ts")'),
168
168
  type: z.string().optional().describe('File type (e.g. "js", "py")'),
169
- case_insensitive: flexBool().describe("Ignore case"),
169
+ case_insensitive: flexBool().describe("Ignore case (-i)"),
170
+ smart_case: flexBool().describe("CI when pattern is all lowercase, CS if it has uppercase (-S)"),
170
171
  context: flexNum().describe("Context lines around matches"),
171
172
  limit: flexNum().describe("Max matches per file (default: 100)"),
172
173
  plain: flexBool().describe("Omit hash tags, return file:line:content"),
173
174
  }),
174
175
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
175
176
  }, async (rawParams) => {
176
- const { pattern, path: p, glob, type, case_insensitive, context, limit, plain } = coerceParams(rawParams);
177
+ const { pattern, path: p, glob, type, case_insensitive, smart_case, context, limit, plain } = coerceParams(rawParams);
177
178
  try {
178
179
  const result = await grepSearch(pattern, {
179
- path: p, glob, type, caseInsensitive: case_insensitive, context, limit, plain,
180
+ path: p, glob, type, caseInsensitive: case_insensitive, smartCase: smart_case, context, limit, plain,
180
181
  });
181
182
  return { content: [{ type: "text", text: result }] };
182
183
  } catch (e) {
@@ -286,19 +287,19 @@ server.registerTool("get_file_info", {
286
287
  server.registerTool("setup_hooks", {
287
288
  title: "Setup Hooks",
288
289
  description:
289
- "Configure hex-line hooks in CLI agent settings. " +
290
- "Claude: writes hooks to ~/.claude/settings.json (global) with absolute path, " +
291
- "removes old hooks from per-project settings.local.json. " +
292
- "Gemini/Codex: returns guidance (no hook support). " +
293
- "Idempotent: re-running produces no changes if already configured.",
290
+ "Install or uninstall hex-line hooks in CLI agent settings. " +
291
+ "install: writes hooks to ~/.claude/settings.json, removes old per-project hooks. " +
292
+ "uninstall: removes hex-line hooks from global settings. " +
293
+ "Idempotent: re-running produces no changes if already in desired state.",
294
294
  inputSchema: z.object({
295
295
  agent: z.string().optional().describe('Target agent: "claude", "gemini", "codex", or "all" (default: "all")'),
296
+ action: z.string().optional().describe('"install" (default) or "uninstall"'),
296
297
  }),
297
298
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
298
299
  }, async (rawParams) => {
299
- const { agent } = coerceParams(rawParams);
300
+ const { agent, action } = coerceParams(rawParams);
300
301
  try {
301
- return { content: [{ type: "text", text: setupHooks(agent) }] };
302
+ return { content: [{ type: "text", text: setupHooks(agent, action) }] };
302
303
  } catch (e) {
303
304
  return { content: [{ type: "text", text: e.message }], isError: true };
304
305
  }
@@ -364,4 +365,4 @@ server.registerTool("bulk_replace", {
364
365
 
365
366
  const transport = new StdioServerTransport();
366
367
  await server.connect(transport);
367
- void checkForUpdates("@levnikolaevich/hex-line-mcp", "1.1.2");
368
+ void checkForUpdates("@levnikolaevich/hex-line-mcp", "1.3.0");