@levnikolaevich/hex-line-mcp 1.0.0 → 1.1.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/hook.mjs CHANGED
@@ -24,6 +24,9 @@
24
24
  */
25
25
 
26
26
  import { deduplicateLines, smartTruncate } from "./lib/normalize.mjs";
27
+ import { readFileSync } from "node:fs";
28
+ import { resolve } from "node:path";
29
+ import { homedir } from "node:os";
27
30
 
28
31
  // ---- Constants ----
29
32
 
@@ -37,18 +40,18 @@ const BINARY_EXT = new Set([
37
40
  ]);
38
41
 
39
42
  const TOOL_HINTS = {
40
- Read: "mcp__hex-line__read_file (not Read, not cat/head/tail)",
41
- Edit: "mcp__hex-line__edit_file (not Edit, not sed -i)",
42
- Write: "mcp__hex-line__write_file (not Write)",
43
+ Read: "mcp__hex-line__read_file (not Read). For writing: write_file (no prior Read needed)",
44
+ Edit: "mcp__hex-line__edit_file (not Edit, not sed -i). read_file first for hashes",
45
+ Write: "mcp__hex-line__write_file (not Write). No prior Read needed",
43
46
  Grep: "mcp__hex-line__grep_search (not Grep, not grep/rg)",
44
- cat: "mcp__hex-line__read_file (not cat)",
45
- head: "mcp__hex-line__read_file with offset/limit (not head)",
46
- tail: "mcp__hex-line__read_file with offset (not tail)",
47
- ls: "mcp__hex-line__directory_tree (not ls/find/tree)",
48
- stat: "mcp__hex-line__get_file_info (not stat/wc -l)",
47
+ cat: "mcp__hex-line__read_file (not cat/head/tail/less/more)",
48
+ head: "mcp__hex-line__read_file with limit param (not head)",
49
+ tail: "mcp__hex-line__read_file with offset param (not tail)",
50
+ ls: "mcp__hex-line__directory_tree with pattern param (not ls/find/tree). E.g. pattern='*-mcp' type='dir'",
51
+ stat: "mcp__hex-line__get_file_info (not stat/wc/file)",
49
52
  grep: "mcp__hex-line__grep_search (not grep/rg)",
50
- sed: "mcp__hex-line__edit_file (not sed -i)",
51
- diff: "mcp__hex-line__changes (not diff)",
53
+ sed: "mcp__hex-line__edit_file (not sed -i). read_file first for hashes",
54
+ diff: "mcp__hex-line__changes (not diff). Git-based semantic diff",
52
55
  outline: "mcp__hex-line__outline (before reading large code files)",
53
56
  verify: "mcp__hex-line__verify (staleness check without re-read)",
54
57
  changes: "mcp__hex-line__changes (semantic AST diff)",
@@ -60,10 +63,13 @@ const BASH_REDIRECTS = [
60
63
  { regex: /^cat\s+\S+/, key: "cat" },
61
64
  { regex: /^head\s+/, key: "head" },
62
65
  { regex: /^tail\s+/, key: "tail" },
66
+ { regex: /^(less|more)\s+/, key: "cat" },
63
67
  { regex: /^(ls|dir)(\s+-\S+)*\s+/, key: "ls" },
64
68
  { regex: /^tree\s+/, key: "ls" },
65
- { regex: /^find\s+.*-name/, key: "ls" },
66
- { regex: /^(stat|wc\s+-l)\s+/, key: "stat" },
69
+ { regex: /^find\s+/, key: "ls" },
70
+ { regex: /^du\s+/, key: "ls" },
71
+ { regex: /^(stat|wc)\s+/, key: "stat" },
72
+ { regex: /^file\s+/, key: "stat" },
67
73
  { regex: /^(grep|rg)\s+/, key: "grep" },
68
74
  { regex: /^sed\s+-i/, key: "sed" },
69
75
  { regex: /^diff\s+/, key: "diff" },
@@ -135,8 +141,16 @@ function detectCommandType(cmd) {
135
141
  return "generic";
136
142
  }
137
143
 
138
- function block(reason) {
139
- process.stdout.write(JSON.stringify({ decision: "block", reason }));
144
+ function block(reason, context) {
145
+ const output = {
146
+ hookSpecificOutput: {
147
+ hookEventName: "PreToolUse",
148
+ permissionDecision: "deny",
149
+ permissionDecisionReason: reason,
150
+ }
151
+ };
152
+ if (context) output.hookSpecificOutput.additionalContext = context;
153
+ process.stdout.write(JSON.stringify(output));
140
154
  process.exit(2);
141
155
  }
142
156
 
@@ -167,8 +181,11 @@ function handlePreToolUse(data) {
167
181
  process.exit(0);
168
182
  }
169
183
 
170
- // Block with redirect
171
- block("Obligatory use " + TOOL_HINTS[hintKey]);
184
+ // Block with redirect — include extracted path for instant retry
185
+ const hint = TOOL_HINTS[hintKey];
186
+ const toolName2 = hint.split(" (")[0];
187
+ const pathNote = filePath ? ` with path="${filePath}"` : "";
188
+ block(`Use ${toolName2}${pathNote}`, hint);
172
189
  }
173
190
 
174
191
  // Bash tool checks
@@ -184,7 +201,8 @@ function handlePreToolUse(data) {
184
201
  for (const { regex, reason } of DANGEROUS_PATTERNS) {
185
202
  if (regex.test(command)) {
186
203
  block(
187
- `DANGEROUS: ${reason}. Ask user to confirm, then retry with: # hex-confirmed`
204
+ `DANGEROUS: ${reason}. Ask user to confirm, then retry with: # hex-confirmed`,
205
+ `Original command: ${command.slice(0, 100)}`
188
206
  );
189
207
  }
190
208
  }
@@ -194,10 +212,14 @@ function handlePreToolUse(data) {
194
212
  process.exit(0);
195
213
  }
196
214
 
197
- // Simple command redirect
215
+ // Simple command redirect — extract args for instant retry
198
216
  for (const { regex, key } of BASH_REDIRECTS) {
199
217
  if (regex.test(command)) {
200
- block("Obligatory use " + TOOL_HINTS[key]);
218
+ const hint = TOOL_HINTS[key];
219
+ const toolName2 = hint.split(" (")[0];
220
+ const args = command.split(/\s+/).slice(1).join(" ");
221
+ const argsNote = args ? ` — args: "${args}"` : "";
222
+ block(`Use ${toolName2}${argsNote}`, hint);
201
223
  }
202
224
  }
203
225
  }
@@ -262,6 +284,27 @@ function handlePostToolUse(data) {
262
284
  // ---- SessionStart: inject tool preferences ----
263
285
 
264
286
  function handleSessionStart() {
287
+ // Check if hex-line output style is active (skip full hints if so)
288
+ const settingsFiles = [
289
+ resolve(process.cwd(), ".claude/settings.local.json"),
290
+ resolve(process.cwd(), ".claude/settings.json"),
291
+ resolve(homedir(), ".claude/settings.json"),
292
+ ];
293
+ let styleActive = false;
294
+ for (const f of settingsFiles) {
295
+ try {
296
+ const config = JSON.parse(readFileSync(f, "utf-8"));
297
+ if (config.outputStyle === "hex-line") { styleActive = true; break; }
298
+ if (config.outputStyle) break; // another style overrides
299
+ } catch { /* file missing or invalid */ }
300
+ }
301
+
302
+ if (styleActive) {
303
+ process.stdout.write(JSON.stringify({ systemMessage: "hex-line Output Style active." }));
304
+ process.exit(0);
305
+ }
306
+
307
+ // Full hints when output style is not active
265
308
  const seen = new Set();
266
309
  const lines = [];
267
310
  for (const hint of Object.values(TOOL_HINTS)) {
@@ -0,0 +1,541 @@
1
+ import { readFileSync, statSync, readdirSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import { performance } from "node:perf_hooks";
4
+ import { resolve, extname, join } from "node:path";
5
+ import { fnv1a, lineTag } from "./hash.mjs";
6
+ import { readFile } from "./read.mjs";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Constants (shared with benchmark.mjs)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const CODE_EXTS = new Set([".js", ".ts", ".py", ".mjs", ".go", ".rs", ".java", ".c", ".cpp", ".rb", ".php"]);
13
+ const MAX_FILES_PER_CAT = 3;
14
+ const RUNS = 5;
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // File discovery
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function walkDir(dir, depth = 0) {
21
+ if (depth > 10) return [];
22
+ const results = [];
23
+ let entries;
24
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
25
+ catch { return results; }
26
+ for (const e of entries) {
27
+ const full = resolve(dir, e.name);
28
+ if (e.isDirectory()) {
29
+ if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "vendor"
30
+ || e.name === "dist" || e.name === "__pycache__" || e.name === "target") continue;
31
+ results.push(...walkDir(full, depth + 1));
32
+ } else if (e.isFile() && CODE_EXTS.has(extname(e.name).toLowerCase())) {
33
+ try {
34
+ const st = statSync(full);
35
+ if (st.size > 0 && st.size < 1_000_000) results.push(full);
36
+ } catch { /* skip */ }
37
+ }
38
+ }
39
+ return results;
40
+ }
41
+
42
+ function getFileLines(f) {
43
+ try { return readFileSync(f, "utf-8").replace(/\r\n/g, "\n").split("\n"); }
44
+ catch { return null; }
45
+ }
46
+
47
+ function categorize(files) {
48
+ const cats = { small: [], medium: [], large: [], xl: [] };
49
+ for (const f of files) {
50
+ const lines = getFileLines(f);
51
+ if (!lines) continue;
52
+ const n = lines.length;
53
+ if (n >= 10 && n <= 50) cats.small.push(f);
54
+ else if (n > 50 && n <= 200) cats.medium.push(f);
55
+ else if (n > 200 && n <= 500) cats.large.push(f);
56
+ else if (n > 500) cats.xl.push(f);
57
+ }
58
+ for (const key of Object.keys(cats)) {
59
+ const arr = cats[key];
60
+ if (arr.length > MAX_FILES_PER_CAT) {
61
+ const step = Math.floor(arr.length / MAX_FILES_PER_CAT);
62
+ cats[key] = Array.from({ length: MAX_FILES_PER_CAT }, (_, i) => arr[i * step]);
63
+ }
64
+ }
65
+ return cats;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Temp file: 200 lines of realistic JS
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function generateTempCode() {
73
+ const lines = [];
74
+ lines.push('import { readFileSync } from "node:fs";');
75
+ lines.push('import { resolve, basename } from "node:path";');
76
+ lines.push("");
77
+ lines.push("const DEFAULT_TIMEOUT = 5000;");
78
+ lines.push("const MAX_RETRIES = 3;");
79
+ lines.push("");
80
+ lines.push("/**");
81
+ lines.push(" * Configuration manager for application settings.");
82
+ lines.push(" * Supports file-based and environment-based config sources.");
83
+ lines.push(" */");
84
+ lines.push("class ConfigManager {");
85
+ lines.push(" constructor(configPath) {");
86
+ lines.push(" this.configPath = resolve(configPath);");
87
+ lines.push(" this.cache = new Map();");
88
+ lines.push(" this.watchers = [];");
89
+ lines.push(" this.loaded = false;");
90
+ lines.push(" }");
91
+ lines.push("");
92
+ lines.push(" load() {");
93
+ lines.push(" const raw = readFileSync(this.configPath, 'utf-8');");
94
+ lines.push(" const parsed = JSON.parse(raw);");
95
+ lines.push(" for (const [key, value] of Object.entries(parsed)) {");
96
+ lines.push(" this.cache.set(key, value);");
97
+ lines.push(" }");
98
+ lines.push(" this.loaded = true;");
99
+ lines.push(" this.notifyWatchers('load', parsed);");
100
+ lines.push(" return this;");
101
+ lines.push(" }");
102
+ lines.push("");
103
+ lines.push(" get(key, defaultValue = undefined) {");
104
+ lines.push(" if (!this.loaded) this.load();");
105
+ lines.push(" return this.cache.has(key) ? this.cache.get(key) : defaultValue;");
106
+ lines.push(" }");
107
+ lines.push("");
108
+ lines.push(" set(key, value) {");
109
+ lines.push(" this.cache.set(key, value);");
110
+ lines.push(" this.notifyWatchers('set', { key, value });");
111
+ lines.push(" }");
112
+ lines.push("");
113
+ lines.push(" watch(callback) {");
114
+ lines.push(" this.watchers.push(callback);");
115
+ lines.push(" return () => {");
116
+ lines.push(" this.watchers = this.watchers.filter(w => w !== callback);");
117
+ lines.push(" };");
118
+ lines.push(" }");
119
+ lines.push("");
120
+ lines.push(" notifyWatchers(event, data) {");
121
+ lines.push(" for (const watcher of this.watchers) {");
122
+ lines.push(" try { watcher(event, data); }");
123
+ lines.push(" catch (e) { console.error('Watcher error:', e.message); }");
124
+ lines.push(" }");
125
+ lines.push(" }");
126
+ lines.push("}");
127
+ lines.push("");
128
+ lines.push("/**");
129
+ lines.push(" * Retry wrapper with exponential backoff.");
130
+ lines.push(" */");
131
+ lines.push("async function withRetry(fn, options = {}) {");
132
+ lines.push(" const { retries = MAX_RETRIES, delay = 100, backoff = 2 } = options;");
133
+ lines.push(" let lastError;");
134
+ lines.push(" for (let attempt = 0; attempt <= retries; attempt++) {");
135
+ lines.push(" try {");
136
+ lines.push(" return await fn(attempt);");
137
+ lines.push(" } catch (err) {");
138
+ lines.push(" lastError = err;");
139
+ lines.push(" if (attempt < retries) {");
140
+ lines.push(" const wait = delay * Math.pow(backoff, attempt);");
141
+ lines.push(" await new Promise(r => setTimeout(r, wait));");
142
+ lines.push(" }");
143
+ lines.push(" }");
144
+ lines.push(" }");
145
+ lines.push(" throw lastError;");
146
+ lines.push("}");
147
+ lines.push("");
148
+ lines.push("/**");
149
+ lines.push(" * HTTP client with timeout and retry support.");
150
+ lines.push(" */");
151
+ lines.push("class HttpClient {");
152
+ lines.push(" constructor(baseUrl, options = {}) {");
153
+ lines.push(" this.baseUrl = baseUrl.replace(/\\/$/, '');");
154
+ lines.push(" this.timeout = options.timeout || DEFAULT_TIMEOUT;");
155
+ lines.push(" this.headers = options.headers || {};");
156
+ lines.push(" this.retries = options.retries || MAX_RETRIES;");
157
+ lines.push(" }");
158
+ lines.push("");
159
+ lines.push(" async request(method, path, body = null) {");
160
+ lines.push(" const url = `${this.baseUrl}${path}`;");
161
+ lines.push(" const controller = new AbortController();");
162
+ lines.push(" const timer = setTimeout(() => controller.abort(), this.timeout);");
163
+ lines.push("");
164
+ lines.push(" try {");
165
+ lines.push(" return await withRetry(async () => {");
166
+ lines.push(" const opts = {");
167
+ lines.push(" method,");
168
+ lines.push(" headers: { ...this.headers },");
169
+ lines.push(" signal: controller.signal,");
170
+ lines.push(" };");
171
+ lines.push(" if (body) {");
172
+ lines.push(" opts.headers['Content-Type'] = 'application/json';");
173
+ lines.push(" opts.body = JSON.stringify(body);");
174
+ lines.push(" }");
175
+ lines.push(" const response = await fetch(url, opts);");
176
+ lines.push(" if (!response.ok) {");
177
+ lines.push(" throw new Error(`HTTP ${response.status}: ${response.statusText}`);");
178
+ lines.push(" }");
179
+ lines.push(" return response.json();");
180
+ lines.push(" }, { retries: this.retries });");
181
+ lines.push(" } finally {");
182
+ lines.push(" clearTimeout(timer);");
183
+ lines.push(" }");
184
+ lines.push(" }");
185
+ lines.push("");
186
+ lines.push(" get(path) { return this.request('GET', path); }");
187
+ lines.push(" post(path, body) { return this.request('POST', path, body); }");
188
+ lines.push(" put(path, body) { return this.request('PUT', path, body); }");
189
+ lines.push(" delete(path) { return this.request('DELETE', path); }");
190
+ lines.push("}");
191
+ lines.push("");
192
+ lines.push("/**");
193
+ lines.push(" * Simple event emitter for pub/sub patterns.");
194
+ lines.push(" */");
195
+ lines.push("class EventEmitter {");
196
+ lines.push(" constructor() {");
197
+ lines.push(" this.listeners = new Map();");
198
+ lines.push(" }");
199
+ lines.push("");
200
+ lines.push(" on(event, handler) {");
201
+ lines.push(" if (!this.listeners.has(event)) {");
202
+ lines.push(" this.listeners.set(event, []);");
203
+ lines.push(" }");
204
+ lines.push(" this.listeners.get(event).push(handler);");
205
+ lines.push(" return this;");
206
+ lines.push(" }");
207
+ lines.push("");
208
+ lines.push(" off(event, handler) {");
209
+ lines.push(" const handlers = this.listeners.get(event);");
210
+ lines.push(" if (handlers) {");
211
+ lines.push(" this.listeners.set(event, handlers.filter(h => h !== handler));");
212
+ lines.push(" }");
213
+ lines.push(" return this;");
214
+ lines.push(" }");
215
+ lines.push("");
216
+ lines.push(" emit(event, ...args) {");
217
+ lines.push(" const handlers = this.listeners.get(event) || [];");
218
+ lines.push(" for (const handler of handlers) {");
219
+ lines.push(" handler(...args);");
220
+ lines.push(" }");
221
+ lines.push(" }");
222
+ lines.push("");
223
+ lines.push(" once(event, handler) {");
224
+ lines.push(" const wrapper = (...args) => {");
225
+ lines.push(" handler(...args);");
226
+ lines.push(" this.off(event, wrapper);");
227
+ lines.push(" };");
228
+ lines.push(" return this.on(event, wrapper);");
229
+ lines.push(" }");
230
+ lines.push("}");
231
+ lines.push("");
232
+ lines.push("/**");
233
+ lines.push(" * Validate and sanitize user input.");
234
+ lines.push(" */");
235
+ lines.push("function validateInput(schema, data) {");
236
+ lines.push(" const errors = [];");
237
+ lines.push(" for (const [field, rules] of Object.entries(schema)) {");
238
+ lines.push(" const value = data[field];");
239
+ lines.push(" if (rules.required && (value === undefined || value === null)) {");
240
+ lines.push(" errors.push(`${field} is required`);");
241
+ lines.push(" continue;");
242
+ lines.push(" }");
243
+ lines.push(" if (value !== undefined && rules.type && typeof value !== rules.type) {");
244
+ lines.push(" errors.push(`${field} must be ${rules.type}`);");
245
+ lines.push(" }");
246
+ lines.push(" if (typeof value === 'string' && rules.maxLength && value.length > rules.maxLength) {");
247
+ lines.push(" errors.push(`${field} exceeds max length ${rules.maxLength}`);");
248
+ lines.push(" }");
249
+ lines.push(" if (typeof value === 'number' && rules.min !== undefined && value < rules.min) {");
250
+ lines.push(" errors.push(`${field} must be >= ${rules.min}`);");
251
+ lines.push(" }");
252
+ lines.push(" }");
253
+ lines.push(" return errors.length > 0 ? { valid: false, errors } : { valid: true };");
254
+ lines.push("}");
255
+ lines.push("");
256
+ lines.push("/**");
257
+ lines.push(" * Format bytes to human-readable string.");
258
+ lines.push(" */");
259
+ lines.push("function formatBytes(bytes) {");
260
+ lines.push(" if (bytes === 0) return '0 B';");
261
+ lines.push(" const units = ['B', 'KB', 'MB', 'GB', 'TB'];");
262
+ lines.push(" const exp = Math.floor(Math.log(bytes) / Math.log(1024));");
263
+ lines.push(" const value = bytes / Math.pow(1024, exp);");
264
+ lines.push(" return `${value.toFixed(exp > 0 ? 1 : 0)} ${units[exp]}`;");
265
+ lines.push("}");
266
+ lines.push("");
267
+ lines.push("/**");
268
+ lines.push(" * Deep merge two objects (source into target).");
269
+ lines.push(" */");
270
+ lines.push("function deepMerge(target, source) {");
271
+ lines.push(" const result = { ...target };");
272
+ lines.push(" for (const key of Object.keys(source)) {");
273
+ lines.push(" if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {");
274
+ lines.push(" result[key] = deepMerge(result[key] || {}, source[key]);");
275
+ lines.push(" } else {");
276
+ lines.push(" result[key] = source[key];");
277
+ lines.push(" }");
278
+ lines.push(" }");
279
+ lines.push(" return result;");
280
+ lines.push("}");
281
+ lines.push("");
282
+ lines.push("export { ConfigManager, HttpClient, EventEmitter, withRetry, validateInput, formatBytes, deepMerge };");
283
+
284
+ // Pad to exactly 200 lines
285
+ while (lines.length < 200) lines.push("");
286
+ return lines.slice(0, 200);
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Simulators -- "without hex-line" (built-in tool output)
291
+ // ---------------------------------------------------------------------------
292
+
293
+ /** Simulate built-in Read: `cat -n` full file with header */
294
+ function simBuiltInReadFull(filePath, lines) {
295
+ const body = lines.map((l, i) => ` ${String(i + 1).padStart(5)}\t${l}`).join("\n");
296
+ return `Contents of ${filePath}:\n\n${body}`;
297
+ }
298
+
299
+ /** Simulate outline via full read -- agent reads entire file to understand structure */
300
+ function simBuiltInOutlineFull(filePath, lines) {
301
+ return simBuiltInReadFull(filePath, lines);
302
+ }
303
+
304
+ /** Real ripgrep call (matches built-in Grep tool behavior) */
305
+ function simBuiltInGrep(pattern, path) {
306
+ try {
307
+ return execSync(`rg -n --no-heading "${pattern}" "${path}"`, { encoding: "utf-8", timeout: 10000 });
308
+ } catch { return ""; }
309
+ }
310
+
311
+ /** Simulate `ls -laR` style output for a directory */
312
+ function simBuiltInLsR(dirPath, depth = 0, maxDepth = 3) {
313
+ if (depth > maxDepth) return "";
314
+ const out = [];
315
+ let entries;
316
+ try { entries = readdirSync(dirPath, { withFileTypes: true }); }
317
+ catch { return ""; }
318
+
319
+ const SKIP = new Set(["node_modules", ".git", "dist", "build", "__pycache__", "coverage"]);
320
+
321
+ out.push(`${dirPath}:`);
322
+ out.push("total " + entries.length);
323
+
324
+ for (const e of entries) {
325
+ if (SKIP.has(e.name) && e.isDirectory()) continue;
326
+ const full = join(dirPath, e.name);
327
+ try {
328
+ const st = statSync(full);
329
+ const type = e.isDirectory() ? "d" : "-";
330
+ const size = String(st.size).padStart(8);
331
+ const date = st.mtime.toISOString().slice(0, 16).replace("T", " ");
332
+ out.push(`${type}rw-r--r-- 1 user group ${size} ${date} ${e.name}`);
333
+ } catch { /* skip */ }
334
+ }
335
+ out.push("");
336
+
337
+ for (const e of entries) {
338
+ if (!e.isDirectory()) continue;
339
+ if (SKIP.has(e.name)) continue;
340
+ const full = join(dirPath, e.name);
341
+ const sub = simBuiltInLsR(full, depth + 1, maxDepth);
342
+ if (sub) out.push(sub);
343
+ }
344
+
345
+ return out.join("\n");
346
+ }
347
+
348
+ /** Simulate `stat` output for a file */
349
+ function simBuiltInStat(filePath) {
350
+ const st = statSync(filePath);
351
+ return [
352
+ ` File: ${filePath}`,
353
+ ` Size: ${st.size}\tBlocks: ${Math.ceil(st.size / 512)}\tIO Block: 4096\tregular file`,
354
+ `Device: 0h/0d\tInode: 0\tLinks: 1`,
355
+ `Access: (0644/-rw-r--r--)\tUid: ( 1000/ user)\tGid: ( 1000/ group)`,
356
+ `Access: ${st.atime.toISOString()}`,
357
+ `Modify: ${st.mtime.toISOString()}`,
358
+ `Change: ${st.ctime.toISOString()}`,
359
+ ` Birth: ${st.birthtime.toISOString()}`,
360
+ ].join("\n");
361
+ }
362
+
363
+ /** Simulate built-in write response */
364
+ function simBuiltInWrite(filePath, content) {
365
+ const lineCount = content.split("\n").length;
366
+ return `File ${filePath} has been created successfully (${lineCount} lines).`;
367
+ }
368
+
369
+ /** Simulate built-in edit: old_string/new_string context blocks */
370
+ function simBuiltInEdit(filePath, origLines, newLines) {
371
+ let changeStart = -1, changeEnd = -1;
372
+ for (let i = 0; i < Math.max(origLines.length, newLines.length); i++) {
373
+ if (origLines[i] !== newLines[i]) {
374
+ if (changeStart === -1) changeStart = i;
375
+ changeEnd = i;
376
+ }
377
+ }
378
+ if (changeStart === -1) return "";
379
+
380
+ const ctxBefore = Math.max(0, changeStart - 3);
381
+ const ctxAfter = Math.min(origLines.length, changeEnd + 4);
382
+ const old_string = origLines.slice(ctxBefore, ctxAfter).join("\n");
383
+ const new_string = newLines.slice(ctxBefore, Math.min(newLines.length, changeEnd + 4)).join("\n");
384
+ return `The file ${filePath} has been edited. Here's the result of running \`cat -n\` on a snippet:\n` +
385
+ `old_string:\n${old_string}\nnew_string:\n${new_string}`;
386
+ }
387
+
388
+ /** Simulate built-in verify: full re-read to check if file changed */
389
+ function simBuiltInVerify(filePath, lines) {
390
+ return simBuiltInReadFull(filePath, lines);
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // Simulators -- "with hex-line" (lib function output)
395
+ // ---------------------------------------------------------------------------
396
+
397
+ /** Hex-line outline -- regex heuristic (no tree-sitter in benchmark) */
398
+ function simHexLineOutline(lines) {
399
+ const structural = /^\s*(export\s+)?(function|class|def|async\s+def|impl|fn|pub\s+fn|struct|interface|type|enum|const|let|var)\b/;
400
+ const importLine = /^\s*(import|from|require|use|#include)/;
401
+ const entries = [];
402
+ let importStart = -1, importEnd = -1, importCount = 0;
403
+
404
+ for (let i = 0; i < lines.length; i++) {
405
+ if (importLine.test(lines[i])) {
406
+ if (importStart === -1) importStart = i + 1;
407
+ importEnd = i + 1;
408
+ importCount++;
409
+ continue;
410
+ }
411
+ if (structural.test(lines[i])) {
412
+ let end = lines.length;
413
+ for (let j = i + 1; j < lines.length; j++) {
414
+ if (structural.test(lines[j])) { end = j; break; }
415
+ }
416
+ entries.push(`${i + 1}-${end}: ${lines[i].trim().slice(0, 120)}`);
417
+ }
418
+ }
419
+
420
+ const parts = [];
421
+ if (importCount > 0) parts.push(`${importStart}-${importEnd}: (${importCount} imports/declarations)`);
422
+ parts.push(...entries);
423
+ parts.push("", `(${entries.length} symbols, ${lines.length} source lines)`);
424
+ return `File: benchmark-target\n\n${parts.join("\n")}`;
425
+ }
426
+
427
+ /** Hex-line outline + targeted read of first function (30 lines) */
428
+ function simHexLineOutlinePlusRead(filePath, lines) {
429
+ const outlineStr = simHexLineOutline(lines);
430
+ const structural = /^\s*(export\s+)?(function|class|def|async\s+def|impl|fn|pub\s+fn|struct)\b/;
431
+ let funcStart = 0;
432
+ for (let i = 0; i < lines.length; i++) {
433
+ if (structural.test(lines[i])) { funcStart = i + 1; break; }
434
+ }
435
+ const start = Math.max(1, funcStart);
436
+ const readStr = readFile(filePath, { offset: start, limit: 30 });
437
+ return outlineStr + "\n---\n" + readStr;
438
+ }
439
+
440
+ /** Hex-line grep -- hash-annotated format */
441
+ function simHexLineGrep(filePath, lines, pattern) {
442
+ const re = new RegExp(pattern, "i");
443
+ const matches = [];
444
+ for (let i = 0; i < lines.length; i++) {
445
+ if (re.test(lines[i])) {
446
+ const tag = lineTag(fnv1a(lines[i]));
447
+ matches.push(`${filePath}:>>${tag}.${i + 1}\t${lines[i]}`);
448
+ }
449
+ }
450
+ return matches.length > 0
451
+ ? "```\n" + matches.join("\n") + "\n```"
452
+ : "No matches found.";
453
+ }
454
+
455
+ /** Hex-line write response */
456
+ function simHexLineWrite(filePath, content) {
457
+ const lineCount = content.split("\n").length;
458
+ return `Created ${filePath} (${lineCount} lines)`;
459
+ }
460
+
461
+ /** Hex-line edit response: compact diff hunks */
462
+ function simHexLineEditDiff(origLines, newLines, ctx = 3) {
463
+ const out = [];
464
+ const maxLen = Math.max(origLines.length, newLines.length);
465
+ let i = 0;
466
+
467
+ while (i < maxLen) {
468
+ if (i < origLines.length && i < newLines.length && origLines[i] === newLines[i]) {
469
+ i++;
470
+ continue;
471
+ }
472
+ // Found a difference -- show context before
473
+ const ctxStart = Math.max(0, i - ctx);
474
+ if (ctxStart < i) {
475
+ if (ctxStart > 0) out.push("...");
476
+ for (let k = ctxStart; k < i; k++) {
477
+ out.push(` ${k + 1}| ${origLines[k]}`);
478
+ }
479
+ }
480
+ // Show changed lines
481
+ const changeStart = i;
482
+ while (i < maxLen && (i >= origLines.length || i >= newLines.length || origLines[i] !== newLines[i])) {
483
+ if (i < origLines.length) out.push(`-${i + 1}| ${origLines[i]}`);
484
+ i++;
485
+ }
486
+ for (let k = changeStart; k < i && k < newLines.length; k++) {
487
+ out.push(`+${k + 1}| ${newLines[k]}`);
488
+ }
489
+ // Context after
490
+ const ctxEnd = Math.min(maxLen, i + ctx);
491
+ for (let k = i; k < ctxEnd && k < origLines.length; k++) {
492
+ out.push(` ${k + 1}| ${origLines[k]}`);
493
+ }
494
+ if (ctxEnd < maxLen) out.push("...");
495
+ break;
496
+ }
497
+
498
+ const diff = out.join("\n");
499
+ return diff
500
+ ? `Updated benchmark-file\n\nDiff:\n\`\`\`diff\n${diff}\n\`\`\``
501
+ : "Updated benchmark-file";
502
+ }
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // Runner utilities
506
+ // ---------------------------------------------------------------------------
507
+
508
+ function median(arr) {
509
+ const sorted = [...arr].sort((a, b) => a - b);
510
+ const mid = Math.floor(sorted.length / 2);
511
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
512
+ }
513
+
514
+ function runN(fn, n = RUNS) {
515
+ const results = [];
516
+ const times = [];
517
+ for (let i = 0; i < n; i++) {
518
+ const t0 = performance.now();
519
+ results.push(fn());
520
+ times.push(performance.now() - t0);
521
+ }
522
+ return { value: median(results), ms: parseFloat(median(times).toFixed(1)) };
523
+ }
524
+
525
+ function fmt(n) {
526
+ return n.toLocaleString("en-US", { maximumFractionDigits: 0 });
527
+ }
528
+
529
+ function pctSavings(without, withSL) {
530
+ if (without === 0) return "N/A";
531
+ const pct = ((without - withSL) / without) * 100;
532
+ return pct >= 0 ? `${pct.toFixed(0)}%` : `-${Math.abs(pct).toFixed(0)}%`;
533
+ }
534
+
535
+ export {
536
+ walkDir, getFileLines, categorize, generateTempCode,
537
+ simBuiltInReadFull, simBuiltInOutlineFull, simBuiltInGrep,
538
+ simBuiltInLsR, simBuiltInStat, simBuiltInWrite, simBuiltInEdit, simBuiltInVerify,
539
+ simHexLineOutline, simHexLineOutlinePlusRead, simHexLineGrep, simHexLineWrite, simHexLineEditDiff,
540
+ median, runN, fmt, pctSavings, RUNS,
541
+ };