@levnikolaevich/hex-line-mcp 1.8.1 → 1.10.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 +6 -4
- package/dist/hook.mjs +39 -35
- package/dist/server.mjs +243 -95
- package/output-style.md +15 -20
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -97,13 +97,15 @@ If a project already has `.hex-skills/codegraph/index.db`, `hex-line` can add li
|
|
|
97
97
|
|
|
98
98
|
- Graph enrichment is optional. If `.hex-skills/codegraph/index.db` is missing, `hex-line` falls back to standard behavior silently.
|
|
99
99
|
- `better-sqlite3` is optional. If it is unavailable, `hex-line` still works without graph hints.
|
|
100
|
-
- `edit_file` reports **
|
|
100
|
+
- `edit_file` reports **Semantic impact** using explainable graph facts: external callers, downstream return/property flow, and clone peers when present.
|
|
101
101
|
|
|
102
102
|
`hex-line` does not read `hex-graph` internals directly anymore. The integration uses a small read-only contract exposed by `hex-graph-mcp`:
|
|
103
103
|
|
|
104
|
-
- `
|
|
105
|
-
- `
|
|
106
|
-
- `
|
|
104
|
+
- `hex_line_symbols`
|
|
105
|
+
- `hex_line_line_facts`
|
|
106
|
+
- `hex_line_edit_impacts`
|
|
107
|
+
- `hex_line_edit_impact_facts`
|
|
108
|
+
- `hex_line_clone_siblings`
|
|
107
109
|
|
|
108
110
|
## Tools Reference
|
|
109
111
|
|
package/dist/hook.mjs
CHANGED
|
@@ -54,7 +54,7 @@ function normalizeOutput(text, opts = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// hook.mjs
|
|
57
|
-
import { readFileSync, statSync } from "node:fs";
|
|
57
|
+
import { readFileSync, statSync, writeSync } from "node:fs";
|
|
58
58
|
import { resolve } from "node:path";
|
|
59
59
|
import { homedir } from "node:os";
|
|
60
60
|
import { fileURLToPath } from "node:url";
|
|
@@ -141,6 +141,7 @@ var TOOL_HINTS = {
|
|
|
141
141
|
changes: "mcp__hex-line__changes (git diff with change symbols)",
|
|
142
142
|
bulk: "mcp__hex-line__bulk_replace (multi-file search-replace)"
|
|
143
143
|
};
|
|
144
|
+
var DEFERRED_HINT = "If schemas not loaded: ToolSearch('+hex-line read edit')";
|
|
144
145
|
var BASH_REDIRECTS = [
|
|
145
146
|
{ regex: /^cat\s+\S+/, key: "cat" },
|
|
146
147
|
{ regex: /^head\s+/, key: "head" },
|
|
@@ -282,16 +283,25 @@ function getHookMode() {
|
|
|
282
283
|
}
|
|
283
284
|
return _hookMode;
|
|
284
285
|
}
|
|
286
|
+
function safeExit(fd, data, code) {
|
|
287
|
+
writeSync(fd, data);
|
|
288
|
+
process.exit(code);
|
|
289
|
+
}
|
|
290
|
+
function debugLog(action, reason) {
|
|
291
|
+
writeSync(2, `[hex-hook] ${action}: ${reason}
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
285
294
|
function block(reason, context) {
|
|
295
|
+
const msg = context ? `${reason}
|
|
296
|
+
${context}` : reason;
|
|
286
297
|
const output = {
|
|
287
298
|
hookSpecificOutput: {
|
|
288
299
|
permissionDecision: "deny"
|
|
289
300
|
},
|
|
290
|
-
systemMessage:
|
|
291
|
-
${context}` : reason
|
|
301
|
+
systemMessage: msg
|
|
292
302
|
};
|
|
293
|
-
|
|
294
|
-
|
|
303
|
+
debugLog("BLOCK", reason);
|
|
304
|
+
safeExit(1, JSON.stringify(output), 2);
|
|
295
305
|
}
|
|
296
306
|
function advise(reason, context) {
|
|
297
307
|
const output = {
|
|
@@ -301,8 +311,7 @@ function advise(reason, context) {
|
|
|
301
311
|
systemMessage: context ? `${reason}
|
|
302
312
|
${context}` : reason
|
|
303
313
|
};
|
|
304
|
-
|
|
305
|
-
process.exit(0);
|
|
314
|
+
safeExit(1, JSON.stringify(output), 0);
|
|
306
315
|
}
|
|
307
316
|
function redirect(reason, context) {
|
|
308
317
|
if (getHookMode() === "advisory") {
|
|
@@ -324,24 +333,20 @@ function handlePreToolUse(data) {
|
|
|
324
333
|
if (BINARY_EXT.has(extOf(filePath))) {
|
|
325
334
|
process.exit(0);
|
|
326
335
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
336
|
+
const resolvedNorm = resolveToolPath(filePath).replace(/\\/g, "/");
|
|
337
|
+
const cwdNorm = process.cwd().replace(/\\/g, "/");
|
|
338
|
+
const homeNorm = homedir().replace(/\\/g, "/");
|
|
339
|
+
const claudeAllow = [
|
|
340
|
+
cwdNorm + "/.claude/settings.json",
|
|
341
|
+
cwdNorm + "/.claude/settings.local.json",
|
|
342
|
+
homeNorm + "/.claude/settings.json",
|
|
343
|
+
homeNorm + "/.claude/settings.local.json"
|
|
344
|
+
];
|
|
345
|
+
if (claudeAllow.some((p) => resolvedNorm.toLowerCase() === p.toLowerCase())) {
|
|
329
346
|
process.exit(0);
|
|
330
347
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (ALLOWED_CONFIGS.has(fileName)) {
|
|
334
|
-
let candidate = filePath;
|
|
335
|
-
if (candidate.startsWith("~/")) {
|
|
336
|
-
candidate = homedir().replace(/\\/g, "/") + candidate.slice(1);
|
|
337
|
-
}
|
|
338
|
-
const absPath = resolve(process.cwd(), candidate).replace(/\\/g, "/");
|
|
339
|
-
const projectClaude = resolve(process.cwd(), ".claude").replace(/\\/g, "/") + "/";
|
|
340
|
-
const globalClaude = resolve(homedir(), ".claude").replace(/\\/g, "/") + "/";
|
|
341
|
-
const cmp = process.platform === "win32" ? (a, b) => a.toLowerCase().startsWith(b.toLowerCase()) : (a, b) => a.startsWith(b);
|
|
342
|
-
if (cmp(absPath, projectClaude) || cmp(absPath, globalClaude)) {
|
|
343
|
-
process.exit(0);
|
|
344
|
-
}
|
|
348
|
+
if (resolvedNorm.includes("/.claude/")) {
|
|
349
|
+
redirect("Protected .claude/ path. Use built-in tools for .claude/ config files.");
|
|
345
350
|
}
|
|
346
351
|
if (toolName === "Read") {
|
|
347
352
|
if (isPartialRead(toolInput)) {
|
|
@@ -349,29 +354,30 @@ function handlePreToolUse(data) {
|
|
|
349
354
|
}
|
|
350
355
|
if (fileSize !== null && fileSize <= LARGE_FILE_BYTES) {
|
|
351
356
|
const ext2 = filePath ? extOf(filePath) : "";
|
|
352
|
-
const hint = filePath && OUTLINEABLE_EXT.has(ext2) ? `mcp__hex-line__outline(path="${filePath}")
|
|
353
|
-
advise(hint);
|
|
357
|
+
const hint = filePath && OUTLINEABLE_EXT.has(ext2) ? `Use mcp__hex-line__outline(path="${filePath}") for structure, then mcp__hex-line__read_file(path="${filePath}") with ranges.` : filePath ? `Use mcp__hex-line__read_file(path="${filePath}"). Built-in Read wastes edit context.` : "Use mcp__hex-line__read_file. Built-in Read wastes edit context.";
|
|
358
|
+
advise(hint, DEFERRED_HINT);
|
|
354
359
|
}
|
|
355
360
|
const ext = filePath ? extOf(filePath) : "";
|
|
356
361
|
const outlineHint = filePath && OUTLINEABLE_EXT.has(ext) ? `Use mcp__hex-line__outline(path="${filePath}") for structure, then mcp__hex-line__read_file(path="${filePath}") with ranges to read only what you need.` : filePath ? `Use mcp__hex-line__read_file(path="${filePath}") with ranges or offset/limit` : "Use mcp__hex-line__directory_tree or mcp__hex-line__read_file";
|
|
357
|
-
redirect(outlineHint, "Do not use built-in Read for full reads of large files
|
|
362
|
+
redirect(outlineHint, "Do not use built-in Read for full reads of large files.\n" + DEFERRED_HINT);
|
|
358
363
|
}
|
|
359
364
|
if (toolName === "Edit") {
|
|
360
365
|
const oldText = String(toolInput.old_string || "");
|
|
361
366
|
const isLargeEdit = Boolean(toolInput.replace_all) || oldText.length > LARGE_EDIT_CHARS || fileSize !== null && fileSize > LARGE_FILE_BYTES;
|
|
362
367
|
if (!isLargeEdit) {
|
|
363
|
-
|
|
368
|
+
const editHint = filePath ? `Prefer mcp__hex-line__edit_file(path="${filePath}") for hash-verified edits.` : "Prefer mcp__hex-line__edit_file for hash-verified edits.";
|
|
369
|
+
advise(editHint);
|
|
364
370
|
}
|
|
365
371
|
const target = filePath ? `Use mcp__hex-line__grep_search or mcp__hex-line__read_file, then mcp__hex-line__edit_file with path="${filePath}"` : "Use mcp__hex-line__grep_search or mcp__hex-line__read_file, then mcp__hex-line__edit_file";
|
|
366
|
-
redirect(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits
|
|
372
|
+
redirect(target, "For large or repeated edits: locate anchors/checksums first, then call edit_file once with batched edits.\n" + DEFERRED_HINT);
|
|
367
373
|
}
|
|
368
374
|
if (toolName === "Write") {
|
|
369
375
|
const pathNote = filePath ? ` with path="${filePath}"` : "";
|
|
370
|
-
redirect(`Use mcp__hex-line__write_file${pathNote}`, TOOL_HINTS.Write);
|
|
376
|
+
redirect(`Use mcp__hex-line__write_file${pathNote}`, TOOL_HINTS.Write + "\n" + DEFERRED_HINT);
|
|
371
377
|
}
|
|
372
378
|
if (toolName === "Grep") {
|
|
373
379
|
const pathNote = filePath ? ` with path="${filePath}"` : "";
|
|
374
|
-
redirect(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep);
|
|
380
|
+
redirect(`Use mcp__hex-line__grep_search${pathNote}`, TOOL_HINTS.Grep + "\n" + DEFERRED_HINT);
|
|
375
381
|
}
|
|
376
382
|
}
|
|
377
383
|
if (toolName === "Bash") {
|
|
@@ -457,8 +463,7 @@ function handlePostToolUse(data) {
|
|
|
457
463
|
`Original: ${originalCount} lines | Filtered: ${filteredCount} lines`,
|
|
458
464
|
"=".repeat(50)
|
|
459
465
|
].join("\n");
|
|
460
|
-
|
|
461
|
-
process.exit(2);
|
|
466
|
+
safeExit(2, output, 2);
|
|
462
467
|
}
|
|
463
468
|
function handleSessionStart() {
|
|
464
469
|
const settingsFiles = [
|
|
@@ -479,9 +484,8 @@ function handleSessionStart() {
|
|
|
479
484
|
}
|
|
480
485
|
}
|
|
481
486
|
const prefix = styleActive ? "Hex-line MCP available. Output style active.\n" : "Hex-line MCP available.\n";
|
|
482
|
-
const msg = prefix + "
|
|
483
|
-
|
|
484
|
-
process.exit(0);
|
|
487
|
+
const msg = prefix + "<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <exploration>\n <rule>Use outline for structure (code + markdown), not Read. ~10-20 lines vs hundreds.</rule>\n <rule>Use read_file with offset/limit or ranges for targeted reads.</rule>\n <rule>Use grep_search before editing to get hash anchors.</rule>\n </exploration>\n <editing>\n <path name='surgical'>grep_search \u2192 edit_file (fastest: hash-verified, no full read needed)</path>\n <path name='exploratory'>outline \u2192 read_file (ranges) \u2192 edit_file with base_revision</path>\n <path name='multi-file'>bulk_replace for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Carry revision from read_file into base_revision on edit_file.</tip>\n <tip>If edit returns CONFLICT, call verify \u2014 only reread when STALE.</tip>\n <tip>Batch multiple edits to same file in one edit_file call.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n </tips>\n <exceptions>Built-in Read OK for: images, PDFs, notebooks, Glob (always), .claude/settings.json</exceptions>\n</hex-line_instructions>";
|
|
488
|
+
safeExit(1, JSON.stringify({ systemMessage: msg }), 0);
|
|
485
489
|
}
|
|
486
490
|
var _norm = (p) => p.replace(/\\/g, "/");
|
|
487
491
|
if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {
|
package/dist/server.mjs
CHANGED
|
@@ -281,8 +281,23 @@ function validateWritePath(filePath) {
|
|
|
281
281
|
import { existsSync as existsSync2 } from "node:fs";
|
|
282
282
|
import { join as join3, dirname as dirname2, relative } from "node:path";
|
|
283
283
|
import { createRequire } from "node:module";
|
|
284
|
-
var
|
|
285
|
-
|
|
284
|
+
var REQUIRED_VIEWS = [
|
|
285
|
+
"hex_line_symbols",
|
|
286
|
+
"hex_line_line_facts",
|
|
287
|
+
"hex_line_edit_impacts",
|
|
288
|
+
"hex_line_edit_impact_facts",
|
|
289
|
+
"hex_line_clone_siblings"
|
|
290
|
+
];
|
|
291
|
+
var FACT_PRIORITY = /* @__PURE__ */ new Map([
|
|
292
|
+
["definition", 0],
|
|
293
|
+
["through_flow", 1],
|
|
294
|
+
["outgoing_flow", 2],
|
|
295
|
+
["incoming_flow", 3],
|
|
296
|
+
["callee", 4],
|
|
297
|
+
["caller", 5],
|
|
298
|
+
["clone", 6],
|
|
299
|
+
["hotspot", 7]
|
|
300
|
+
]);
|
|
286
301
|
var _dbs = /* @__PURE__ */ new Map();
|
|
287
302
|
var _driverUnavailable = false;
|
|
288
303
|
function getGraphDB(filePath) {
|
|
@@ -296,7 +311,7 @@ function getGraphDB(filePath) {
|
|
|
296
311
|
const require2 = createRequire(import.meta.url);
|
|
297
312
|
const Database = require2("better-sqlite3");
|
|
298
313
|
const db = new Database(dbPath, { readonly: true });
|
|
299
|
-
if (!
|
|
314
|
+
if (!validateContract(db)) {
|
|
300
315
|
db.close();
|
|
301
316
|
return null;
|
|
302
317
|
}
|
|
@@ -307,37 +322,51 @@ function getGraphDB(filePath) {
|
|
|
307
322
|
return null;
|
|
308
323
|
}
|
|
309
324
|
}
|
|
310
|
-
|
|
311
|
-
function _hasCloneView(db) {
|
|
312
|
-
if (_cloneViewAvailable.has(db)) return _cloneViewAvailable.get(db);
|
|
325
|
+
function validateContract(db) {
|
|
313
326
|
try {
|
|
314
|
-
|
|
315
|
-
|
|
327
|
+
for (const viewName of REQUIRED_VIEWS) {
|
|
328
|
+
const row = db.prepare(
|
|
329
|
+
"SELECT name FROM sqlite_master WHERE type = 'view' AND name = ? LIMIT 1"
|
|
330
|
+
).get(viewName);
|
|
331
|
+
if (!row) return false;
|
|
332
|
+
}
|
|
333
|
+
db.prepare("SELECT node_id, file, line_start, line_end, display_name, kind FROM hex_line_symbols LIMIT 1").all();
|
|
334
|
+
db.prepare("SELECT fact_kind, related_display_name, confidence, origin FROM hex_line_line_facts LIMIT 1").all();
|
|
335
|
+
db.prepare("SELECT symbol_node_id, external_callers_count, downstream_return_flow_count, downstream_property_flow_count, sink_reach_count FROM hex_line_edit_impacts LIMIT 1").all();
|
|
336
|
+
db.prepare("SELECT edited_symbol_id, fact_kind, target_display_name, path_kind, flow_hops FROM hex_line_edit_impact_facts LIMIT 1").all();
|
|
316
337
|
return true;
|
|
317
338
|
} catch {
|
|
318
|
-
_cloneViewAvailable.set(db, false);
|
|
319
339
|
return false;
|
|
320
340
|
}
|
|
321
341
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
} catch {
|
|
330
|
-
return false;
|
|
342
|
+
function shortKind(kind) {
|
|
343
|
+
return { function: "fn", class: "cls", method: "mtd", variable: "var" }[kind] || kind;
|
|
344
|
+
}
|
|
345
|
+
function compactSymbolCounts(node) {
|
|
346
|
+
const parts = [];
|
|
347
|
+
if ((node.callees_exact || 0) > 0 || (node.callers_exact || 0) > 0) {
|
|
348
|
+
parts.push(`${node.callees_exact}\u2193 ${node.callers_exact}\u2191`);
|
|
331
349
|
}
|
|
350
|
+
const flowParts = [];
|
|
351
|
+
if ((node.incoming_flow_count || 0) > 0) flowParts.push(`${node.incoming_flow_count}in`);
|
|
352
|
+
if ((node.outgoing_flow_count || 0) > 0) flowParts.push(`${node.outgoing_flow_count}out`);
|
|
353
|
+
if ((node.through_flow_count || 0) > 0) flowParts.push(`${node.through_flow_count}thru`);
|
|
354
|
+
if (flowParts.length > 0) parts.push(`flow ${flowParts.join(" ")}`);
|
|
355
|
+
if ((node.clone_sibling_count || 0) > 0) parts.push(`clone ${node.clone_sibling_count}`);
|
|
356
|
+
return parts;
|
|
332
357
|
}
|
|
333
358
|
function symbolAnnotation(db, file, name) {
|
|
334
359
|
try {
|
|
335
360
|
const node = db.prepare(
|
|
336
|
-
|
|
361
|
+
`SELECT display_name, kind, callers_exact, callees_exact, incoming_flow_count, outgoing_flow_count, through_flow_count, clone_sibling_count
|
|
362
|
+
FROM hex_line_symbols
|
|
363
|
+
WHERE file = ? AND name = ?
|
|
364
|
+
LIMIT 1`
|
|
337
365
|
).get(file, name);
|
|
338
366
|
if (!node) return null;
|
|
339
|
-
|
|
340
|
-
|
|
367
|
+
const parts = compactSymbolCounts(node);
|
|
368
|
+
const prefix = shortKind(node.kind);
|
|
369
|
+
return parts.length > 0 ? `[${prefix} ${parts.join(" | ")}]` : `[${prefix}]`;
|
|
341
370
|
} catch {
|
|
342
371
|
return null;
|
|
343
372
|
}
|
|
@@ -345,59 +374,85 @@ function symbolAnnotation(db, file, name) {
|
|
|
345
374
|
function fileAnnotations(db, file) {
|
|
346
375
|
try {
|
|
347
376
|
const nodes = db.prepare(
|
|
348
|
-
`SELECT display_name, kind,
|
|
349
|
-
FROM
|
|
377
|
+
`SELECT display_name, kind, callers_exact, callees_exact, incoming_flow_count, outgoing_flow_count, through_flow_count, clone_sibling_count
|
|
378
|
+
FROM hex_line_symbols
|
|
350
379
|
WHERE file = ?
|
|
351
380
|
ORDER BY line_start`
|
|
352
381
|
).all(file);
|
|
353
382
|
return nodes.map((node) => ({
|
|
354
383
|
name: node.display_name,
|
|
355
384
|
kind: node.kind,
|
|
356
|
-
|
|
357
|
-
|
|
385
|
+
callers_exact: node.callers_exact,
|
|
386
|
+
callees_exact: node.callees_exact,
|
|
387
|
+
incoming_flow_count: node.incoming_flow_count,
|
|
388
|
+
outgoing_flow_count: node.outgoing_flow_count,
|
|
389
|
+
through_flow_count: node.through_flow_count,
|
|
390
|
+
clone_sibling_count: node.clone_sibling_count
|
|
358
391
|
}));
|
|
359
392
|
} catch {
|
|
360
393
|
return [];
|
|
361
394
|
}
|
|
362
395
|
}
|
|
363
|
-
function
|
|
396
|
+
function formatLineFact(fact) {
|
|
397
|
+
const countParts = compactSymbolCounts(fact);
|
|
398
|
+
const suffix = countParts.length > 0 ? ` | ${countParts.join(" | ")}` : "";
|
|
399
|
+
switch (fact.fact_kind) {
|
|
400
|
+
case "definition":
|
|
401
|
+
return `[${shortKind(fact.kind)}${suffix}]`;
|
|
402
|
+
case "callee":
|
|
403
|
+
return fact.related_display_name ? `[callee:${fact.related_display_name}${suffix}]` : `[callee${suffix}]`;
|
|
404
|
+
case "caller":
|
|
405
|
+
return fact.related_display_name ? `[caller:${fact.related_display_name}${suffix}]` : `[caller${suffix}]`;
|
|
406
|
+
case "outgoing_flow":
|
|
407
|
+
return `[flow-out:${fact.target_anchor_kind || "?"}${suffix}]`;
|
|
408
|
+
case "incoming_flow":
|
|
409
|
+
return `[flow-in:${fact.target_anchor_kind || "?"}${suffix}]`;
|
|
410
|
+
case "through_flow":
|
|
411
|
+
return `[flow-through${suffix}]`;
|
|
412
|
+
case "clone":
|
|
413
|
+
return `[clone${suffix}]`;
|
|
414
|
+
case "hotspot":
|
|
415
|
+
return `[hotspot${suffix}]`;
|
|
416
|
+
default:
|
|
417
|
+
return `[${fact.fact_kind}${suffix}]`;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function priorityForFact(factKind) {
|
|
421
|
+
return FACT_PRIORITY.get(factKind) ?? 99;
|
|
422
|
+
}
|
|
423
|
+
function matchAnnotation(db, file, line) {
|
|
364
424
|
try {
|
|
365
|
-
const
|
|
366
|
-
`SELECT
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return affected.slice(0, 10);
|
|
425
|
+
const facts = db.prepare(
|
|
426
|
+
`SELECT
|
|
427
|
+
lf.display_name,
|
|
428
|
+
lf.kind,
|
|
429
|
+
lf.fact_kind,
|
|
430
|
+
lf.related_display_name,
|
|
431
|
+
lf.source_anchor_kind,
|
|
432
|
+
lf.target_anchor_kind,
|
|
433
|
+
hs.callers_exact,
|
|
434
|
+
hs.callees_exact,
|
|
435
|
+
hs.incoming_flow_count,
|
|
436
|
+
hs.outgoing_flow_count,
|
|
437
|
+
hs.through_flow_count,
|
|
438
|
+
hs.clone_sibling_count
|
|
439
|
+
FROM hex_line_line_facts lf
|
|
440
|
+
LEFT JOIN hex_line_symbols hs ON hs.node_id = lf.symbol_node_id
|
|
441
|
+
WHERE lf.file = ? AND lf.line_start <= ? AND lf.line_end >= ?
|
|
442
|
+
ORDER BY lf.line_start DESC`
|
|
443
|
+
).all(file, line, line);
|
|
444
|
+
if (facts.length === 0) return null;
|
|
445
|
+
facts.sort((left, right) => priorityForFact(left.fact_kind) - priorityForFact(right.fact_kind));
|
|
446
|
+
return formatLineFact(facts[0]);
|
|
391
447
|
} catch {
|
|
392
|
-
return
|
|
448
|
+
return null;
|
|
393
449
|
}
|
|
394
450
|
}
|
|
395
451
|
function cloneWarning(db, file, startLine, endLine) {
|
|
396
452
|
try {
|
|
397
|
-
if (!_hasCloneView(db)) return [];
|
|
398
453
|
const modified = db.prepare(
|
|
399
454
|
`SELECT node_id
|
|
400
|
-
FROM
|
|
455
|
+
FROM hex_line_symbols
|
|
401
456
|
WHERE file = ?
|
|
402
457
|
AND line_start <= ?
|
|
403
458
|
AND line_end >= ?`
|
|
@@ -407,17 +462,20 @@ function cloneWarning(db, file, startLine, endLine) {
|
|
|
407
462
|
const seen = /* @__PURE__ */ new Set();
|
|
408
463
|
for (const node of modified) {
|
|
409
464
|
const siblings = db.prepare(
|
|
410
|
-
`SELECT
|
|
411
|
-
FROM hex_line_clone_siblings
|
|
412
|
-
|
|
413
|
-
WHERE s1.node_id = ?`
|
|
465
|
+
`SELECT clone_peer_name, clone_peer_file, clone_peer_line, clone_type
|
|
466
|
+
FROM hex_line_clone_siblings
|
|
467
|
+
WHERE node_id = ?`
|
|
414
468
|
).all(node.node_id);
|
|
415
|
-
for (const
|
|
416
|
-
const key = `${
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
469
|
+
for (const sibling of siblings) {
|
|
470
|
+
const key = `${sibling.clone_peer_file}:${sibling.clone_peer_name}:${sibling.clone_peer_line}`;
|
|
471
|
+
if (seen.has(key)) continue;
|
|
472
|
+
seen.add(key);
|
|
473
|
+
clones.push({
|
|
474
|
+
name: sibling.clone_peer_name,
|
|
475
|
+
file: sibling.clone_peer_file,
|
|
476
|
+
line: sibling.clone_peer_line,
|
|
477
|
+
cloneType: sibling.clone_type
|
|
478
|
+
});
|
|
421
479
|
}
|
|
422
480
|
}
|
|
423
481
|
return clones.slice(0, 10);
|
|
@@ -425,21 +483,64 @@ function cloneWarning(db, file, startLine, endLine) {
|
|
|
425
483
|
return [];
|
|
426
484
|
}
|
|
427
485
|
}
|
|
428
|
-
function
|
|
486
|
+
function semanticImpact(db, file, startLine, endLine) {
|
|
429
487
|
try {
|
|
430
|
-
const
|
|
431
|
-
`SELECT display_name,
|
|
432
|
-
FROM
|
|
433
|
-
WHERE file = ?
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
).
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
488
|
+
const modified = db.prepare(
|
|
489
|
+
`SELECT symbol_node_id, display_name, external_callers_count, downstream_return_flow_count, downstream_property_flow_count, sink_reach_count, clone_sibling_count
|
|
490
|
+
FROM hex_line_edit_impacts
|
|
491
|
+
WHERE file = ?
|
|
492
|
+
AND line_start <= ?
|
|
493
|
+
AND line_end >= ?`
|
|
494
|
+
).all(file, endLine, startLine);
|
|
495
|
+
if (modified.length === 0) return [];
|
|
496
|
+
return modified.map((item) => {
|
|
497
|
+
const facts = db.prepare(
|
|
498
|
+
`SELECT fact_kind, target_display_name, target_file, target_line, intermediate_display_name, path_kind, flow_hops, source_anchor_kind, target_anchor_kind, access_path_json
|
|
499
|
+
FROM hex_line_edit_impact_facts
|
|
500
|
+
WHERE edited_symbol_id = ?
|
|
501
|
+
ORDER BY
|
|
502
|
+
CASE fact_kind
|
|
503
|
+
WHEN 'external_caller' THEN 0
|
|
504
|
+
WHEN 'return_flow_to_symbol' THEN 1
|
|
505
|
+
WHEN 'property_flow_to_symbol' THEN 2
|
|
506
|
+
WHEN 'flow_reaches_terminal_anchor' THEN 3
|
|
507
|
+
WHEN 'clone_sibling' THEN 4
|
|
508
|
+
ELSE 9
|
|
509
|
+
END,
|
|
510
|
+
target_file,
|
|
511
|
+
target_line`
|
|
512
|
+
).all(item.symbol_node_id);
|
|
513
|
+
const seen = /* @__PURE__ */ new Set();
|
|
514
|
+
const dedupedFacts = facts.filter((fact) => {
|
|
515
|
+
const key = [
|
|
516
|
+
fact.fact_kind,
|
|
517
|
+
fact.target_display_name || "",
|
|
518
|
+
fact.target_file || "",
|
|
519
|
+
fact.target_line || "",
|
|
520
|
+
fact.path_kind || "",
|
|
521
|
+
fact.flow_hops || "",
|
|
522
|
+
fact.source_anchor_kind || "",
|
|
523
|
+
fact.target_anchor_kind || "",
|
|
524
|
+
fact.access_path_json || ""
|
|
525
|
+
].join("|");
|
|
526
|
+
if (seen.has(key)) return false;
|
|
527
|
+
seen.add(key);
|
|
528
|
+
return true;
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
symbol: item.display_name,
|
|
532
|
+
counts: {
|
|
533
|
+
externalCallers: item.external_callers_count,
|
|
534
|
+
downstreamReturnFlow: item.downstream_return_flow_count,
|
|
535
|
+
downstreamPropertyFlow: item.downstream_property_flow_count,
|
|
536
|
+
sinkReach: item.sink_reach_count,
|
|
537
|
+
cloneSiblings: item.clone_sibling_count
|
|
538
|
+
},
|
|
539
|
+
facts: dedupedFacts
|
|
540
|
+
};
|
|
541
|
+
});
|
|
441
542
|
} catch {
|
|
442
|
-
return
|
|
543
|
+
return [];
|
|
443
544
|
}
|
|
444
545
|
}
|
|
445
546
|
function getRelativePath(filePath) {
|
|
@@ -991,8 +1092,17 @@ ${text}
|
|
|
991
1092
|
const annos = fileAnnotations(db, relFile);
|
|
992
1093
|
if (annos.length > 0) {
|
|
993
1094
|
const items = annos.map((a) => {
|
|
994
|
-
const
|
|
995
|
-
|
|
1095
|
+
const parts = [];
|
|
1096
|
+
if ((a.callees_exact || 0) > 0 || (a.callers_exact || 0) > 0) {
|
|
1097
|
+
parts.push(`${a.callees_exact}\u2193 ${a.callers_exact}\u2191`);
|
|
1098
|
+
}
|
|
1099
|
+
const flow = [];
|
|
1100
|
+
if ((a.incoming_flow_count || 0) > 0) flow.push(`${a.incoming_flow_count}in`);
|
|
1101
|
+
if ((a.outgoing_flow_count || 0) > 0) flow.push(`${a.outgoing_flow_count}out`);
|
|
1102
|
+
if ((a.through_flow_count || 0) > 0) flow.push(`${a.through_flow_count}thru`);
|
|
1103
|
+
if (flow.length > 0) parts.push(`flow ${flow.join(" ")}`);
|
|
1104
|
+
if ((a.clone_sibling_count || 0) > 0) parts.push(`clone ${a.clone_sibling_count}`);
|
|
1105
|
+
return parts.length > 0 ? `${a.name} [${a.kind} ${parts.join(" | ")}]` : `${a.name} [${a.kind}]`;
|
|
996
1106
|
});
|
|
997
1107
|
graphLine = `
|
|
998
1108
|
Graph: ${items.join(" | ")}`;
|
|
@@ -1362,13 +1472,14 @@ function applyReplaceLinesEdit(edit, ctx) {
|
|
|
1362
1472
|
const { start: csStart, end: csEnd, hex: csHex } = parseChecksum(rangeChecksum2);
|
|
1363
1473
|
if (!coverage.ok) {
|
|
1364
1474
|
const snip = buildErrorSnippet(origLines, actualStart - 1);
|
|
1475
|
+
const retryChecksum = buildRangeChecksum(currentSnapshot, actualStart, actualEnd);
|
|
1365
1476
|
throw new Error(
|
|
1366
1477
|
`${coverage.reason}
|
|
1367
1478
|
|
|
1368
1479
|
Current content (lines ${snip.start}-${snip.end}):
|
|
1369
1480
|
${snip.text}
|
|
1370
1481
|
|
|
1371
|
-
Tip: Use updated hashes above for retry
|
|
1482
|
+
` + (retryChecksum ? `Retry checksum: ${retryChecksum}` : "Tip: Use updated hashes above for retry.")
|
|
1372
1483
|
);
|
|
1373
1484
|
}
|
|
1374
1485
|
const actual = buildRangeChecksum(currentSnapshot, csStart, csEnd);
|
|
@@ -1427,7 +1538,7 @@ function editFile(filePath, edits, opts = {}) {
|
|
|
1427
1538
|
const currentSnapshot = readSnapshot(real);
|
|
1428
1539
|
const baseSnapshot = opts.baseRevision ? getSnapshotByRevision(opts.baseRevision) : null;
|
|
1429
1540
|
const hasBaseSnapshot = !!(baseSnapshot && baseSnapshot.path === real);
|
|
1430
|
-
const staleRevision = !!opts.baseRevision && opts.baseRevision !== currentSnapshot.revision;
|
|
1541
|
+
const staleRevision = !!opts.baseRevision && opts.baseRevision !== currentSnapshot.revision && hasBaseSnapshot;
|
|
1431
1542
|
const changedRanges = staleRevision && hasBaseSnapshot ? computeChangedRanges(baseSnapshot.lines, currentSnapshot.lines) : [];
|
|
1432
1543
|
const conflictPolicy = opts.conflictPolicy || "conservative";
|
|
1433
1544
|
const original = currentSnapshot.content;
|
|
@@ -1608,13 +1719,30 @@ ${serializeReadBlock(block)}`;
|
|
|
1608
1719
|
const db = getGraphDB(real);
|
|
1609
1720
|
const relFile = db ? getRelativePath(real) : null;
|
|
1610
1721
|
if (db && relFile && fullDiff && minLine <= maxLine) {
|
|
1611
|
-
const
|
|
1612
|
-
if (
|
|
1613
|
-
const
|
|
1722
|
+
const impacts = semanticImpact(db, relFile, minLine, maxLine);
|
|
1723
|
+
if (impacts.length > 0) {
|
|
1724
|
+
const sections = impacts.map((impact) => {
|
|
1725
|
+
const totals = [];
|
|
1726
|
+
if (impact.counts.externalCallers > 0) totals.push(`${impact.counts.externalCallers} external callers`);
|
|
1727
|
+
if (impact.counts.downstreamReturnFlow > 0) totals.push(`${impact.counts.downstreamReturnFlow} downstream return-flow`);
|
|
1728
|
+
if (impact.counts.downstreamPropertyFlow > 0) totals.push(`${impact.counts.downstreamPropertyFlow} property-flow`);
|
|
1729
|
+
if (impact.counts.sinkReach > 0) totals.push(`${impact.counts.sinkReach} terminal flow reach`);
|
|
1730
|
+
if (impact.counts.cloneSiblings > 0) totals.push(`${impact.counts.cloneSiblings} clone siblings`);
|
|
1731
|
+
const headline = totals.length > 0 ? totals.join(", ") : "no downstream graph facts";
|
|
1732
|
+
const factLines = impact.facts.slice(0, 5).map((fact) => {
|
|
1733
|
+
const target = fact.target_display_name ? `${fact.target_display_name} (${fact.target_file}:${fact.target_line})` : `${fact.target_file}:${fact.target_line}`;
|
|
1734
|
+
const via = fact.path_kind ? ` via ${fact.path_kind}` : "";
|
|
1735
|
+
return `${fact.fact_kind}: ${target}${via}`;
|
|
1736
|
+
});
|
|
1737
|
+
return [
|
|
1738
|
+
`${impact.symbol}: ${headline}`,
|
|
1739
|
+
...factLines.map((line) => ` ${line}`)
|
|
1740
|
+
].join("\n");
|
|
1741
|
+
});
|
|
1614
1742
|
msg += `
|
|
1615
1743
|
|
|
1616
|
-
\u26A0
|
|
1617
|
-
|
|
1744
|
+
\u26A0 Semantic impact:
|
|
1745
|
+
${sections.join("\n")}`;
|
|
1618
1746
|
}
|
|
1619
1747
|
const clones = cloneWarning(db, relFile, minLine, maxLine);
|
|
1620
1748
|
if (clones.length > 0) {
|
|
@@ -1648,6 +1776,7 @@ try {
|
|
|
1648
1776
|
var DEFAULT_LIMIT2 = 100;
|
|
1649
1777
|
var MAX_OUTPUT = 10 * 1024 * 1024;
|
|
1650
1778
|
var TIMEOUT2 = 3e4;
|
|
1779
|
+
var MAX_SEARCH_OUTPUT_CHARS = 8e4;
|
|
1651
1780
|
function spawnRg(args) {
|
|
1652
1781
|
return new Promise((resolve_, reject) => {
|
|
1653
1782
|
let stdout = "";
|
|
@@ -1844,7 +1973,26 @@ async function contentMode(pattern, target, opts, plain, totalLimit) {
|
|
|
1844
1973
|
}
|
|
1845
1974
|
flushGroup();
|
|
1846
1975
|
if (db) blocks.sort((a, b) => (b.meta.graphScore || 0) - (a.meta.graphScore || 0));
|
|
1847
|
-
|
|
1976
|
+
const parts = [];
|
|
1977
|
+
let budget = MAX_SEARCH_OUTPUT_CHARS;
|
|
1978
|
+
let capped = false;
|
|
1979
|
+
for (const block of blocks) {
|
|
1980
|
+
const serialized = block.type === "edit_ready_block" ? serializeSearchBlock(block, { plain }) : serializeDiagnosticBlock(block);
|
|
1981
|
+
if (parts.length > 0 && budget - serialized.length < 0) {
|
|
1982
|
+
capped = true;
|
|
1983
|
+
break;
|
|
1984
|
+
}
|
|
1985
|
+
parts.push(serialized);
|
|
1986
|
+
budget -= serialized.length;
|
|
1987
|
+
}
|
|
1988
|
+
if (capped) {
|
|
1989
|
+
const remaining = blocks.length - parts.length;
|
|
1990
|
+
parts.push(serializeDiagnosticBlock(buildDiagnosticBlock({
|
|
1991
|
+
kind: "output_capped",
|
|
1992
|
+
message: `OUTPUT_CAPPED: ${remaining} more search block(s) omitted (${MAX_SEARCH_OUTPUT_CHARS} char limit). Narrow with path= or glob= filters.`
|
|
1993
|
+
})));
|
|
1994
|
+
}
|
|
1995
|
+
return parts.join("\n\n");
|
|
1848
1996
|
}
|
|
1849
1997
|
|
|
1850
1998
|
// lib/outline.mjs
|
|
@@ -2840,7 +2988,7 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
|
|
|
2840
2988
|
}
|
|
2841
2989
|
|
|
2842
2990
|
// server.mjs
|
|
2843
|
-
var version = true ? "1.
|
|
2991
|
+
var version = true ? "1.10.0" : (await null).createRequire(import.meta.url)("./package.json").version;
|
|
2844
2992
|
var { server, StdioServerTransport } = await createServerRuntime({
|
|
2845
2993
|
name: "hex-line-mcp",
|
|
2846
2994
|
version
|
|
@@ -2901,7 +3049,7 @@ ERROR: ${e.message}`);
|
|
|
2901
3049
|
});
|
|
2902
3050
|
server.registerTool("edit_file", {
|
|
2903
3051
|
title: "Edit File",
|
|
2904
|
-
description: "Apply verified partial edits to one file.",
|
|
3052
|
+
description: "Apply hash-verified partial edits to one file. Batch multiple edits in one call. Carry base_revision from prior read/edit for auto-rebase on concurrent changes.",
|
|
2905
3053
|
inputSchema: z2.object({
|
|
2906
3054
|
path: z2.string().describe("File to edit"),
|
|
2907
3055
|
edits: z2.union([z2.string(), z2.array(z2.any())]).describe(
|
|
@@ -3036,7 +3184,7 @@ server.registerTool("outline", {
|
|
|
3036
3184
|
});
|
|
3037
3185
|
server.registerTool("verify", {
|
|
3038
3186
|
title: "Verify Checksums",
|
|
3039
|
-
description: "
|
|
3187
|
+
description: "Check if held checksums are still valid without rereading. Use after edit_file returns CONFLICT to decide: VALID (retry), STALE (reread ranges), INVALID (reread file).",
|
|
3040
3188
|
inputSchema: z2.object({
|
|
3041
3189
|
path: z2.string().describe("File path"),
|
|
3042
3190
|
checksums: z2.array(z2.string()).describe('Checksum strings, e.g. ["1-50:f7e2a1b0", "51-100:abcd1234"]'),
|
|
@@ -3056,7 +3204,7 @@ server.registerTool("verify", {
|
|
|
3056
3204
|
});
|
|
3057
3205
|
server.registerTool("directory_tree", {
|
|
3058
3206
|
title: "Directory Tree",
|
|
3059
|
-
description: "
|
|
3207
|
+
description: "Gitignore-aware directory tree. Use pattern glob to find files/dirs by name instead of Bash find/ls. Skips node_modules, .git, dist.",
|
|
3060
3208
|
inputSchema: z2.object({
|
|
3061
3209
|
path: z2.string().describe("Directory path"),
|
|
3062
3210
|
pattern: z2.string().optional().describe('Glob filter on names (e.g. "*-mcp", "*.mjs"). Returns flat match list instead of tree'),
|
|
@@ -3076,7 +3224,7 @@ server.registerTool("directory_tree", {
|
|
|
3076
3224
|
});
|
|
3077
3225
|
server.registerTool("get_file_info", {
|
|
3078
3226
|
title: "File Info",
|
|
3079
|
-
description: "File metadata without reading content: size, line count,
|
|
3227
|
+
description: "File metadata without reading content: size, line count, mtime, binary detection. Use before read_file on unknown files to decide offset/limit strategy.",
|
|
3080
3228
|
inputSchema: z2.object({
|
|
3081
3229
|
path: z2.string().describe("File path")
|
|
3082
3230
|
}),
|
|
@@ -3091,7 +3239,7 @@ server.registerTool("get_file_info", {
|
|
|
3091
3239
|
});
|
|
3092
3240
|
server.registerTool("changes", {
|
|
3093
3241
|
title: "Semantic Diff",
|
|
3094
|
-
description: "
|
|
3242
|
+
description: "Semantic diff against git ref (default: HEAD). Shows added/removed/modified symbols. Use to review changes before commit.",
|
|
3095
3243
|
inputSchema: z2.object({
|
|
3096
3244
|
path: z2.string().describe("File or directory path"),
|
|
3097
3245
|
compare_against: z2.string().optional().describe('Git ref to compare against (default: "HEAD")')
|
|
@@ -3107,7 +3255,7 @@ server.registerTool("changes", {
|
|
|
3107
3255
|
});
|
|
3108
3256
|
server.registerTool("bulk_replace", {
|
|
3109
3257
|
title: "Bulk Replace",
|
|
3110
|
-
description: "Search-and-replace across multiple files
|
|
3258
|
+
description: "Search-and-replace text across multiple files. Use for renames, refactors. Compact or full diff output.",
|
|
3111
3259
|
inputSchema: z2.object({
|
|
3112
3260
|
replacements: z2.union([z2.string(), replacementPairsSchema]).describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
|
|
3113
3261
|
glob: z2.string().optional().describe('File glob (default: "**/*.{md,mjs,json,yml,ts,js}")'),
|
package/output-style.md
CHANGED
|
@@ -16,35 +16,30 @@ keep-coding-instructions: true
|
|
|
16
16
|
| Grep | `mcp__hex-line__grep_search` | Edit-ready matches |
|
|
17
17
|
| Edit (text rename) | `mcp__hex-line__bulk_replace` | Multi-file text rename/refactor |
|
|
18
18
|
| Bash `find`/`tree` | `mcp__hex-line__directory_tree` | Pattern search, gitignore-aware |
|
|
19
|
+
| Full code read | `mcp__hex-line__outline` then `read_file` with ranges | Structure first, read targeted |
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
**Bootstrap**: if hex-line calls fail, load schemas: `ToolSearch('+hex-line read edit')`
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
1. `outline` first
|
|
24
|
-
2. `read_file` with `offset`/`limit` or `ranges`
|
|
25
|
-
3. `paths` or `ranges` when batching several targets
|
|
23
|
+
## Workflow Paths
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
| Path | When | Flow |
|
|
26
|
+
|------|------|------|
|
|
27
|
+
| Surgical | Know the target | `grep_search` → `edit_file` |
|
|
28
|
+
| Exploratory | Need context first | `outline` → `read_file` (ranges) → `edit_file` |
|
|
29
|
+
| Multi-file | Text rename/refactor | `bulk_replace` |
|
|
30
|
+
| Verify | Check freshness | `verify` → reread only if STALE |
|
|
28
31
|
|
|
29
32
|
Bash OK for: npm/node/git/docker/curl, pipes, compound commands.
|
|
30
33
|
**Built-in OK for:** images, PDFs, notebooks, Glob (always), `.claude/settings.json`, `.claude/settings.local.json`.
|
|
31
34
|
|
|
32
35
|
## Edit Workflow
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
6. only reread (`read_file`) when `verify` confirms STALE
|
|
41
|
-
|
|
42
|
-
Post-edit output uses `block: post_edit` with checksum — use it directly for follow-up edits or verify.
|
|
43
|
-
|
|
44
|
-
Avoid:
|
|
45
|
-
- chained same-file `edit_file` calls when all edits are already known
|
|
46
|
-
- full-file rewrites for local changes
|
|
47
|
-
- using `bulk_replace` for structural block rewrites
|
|
37
|
+
| Do | Don't |
|
|
38
|
+
|----|-------|
|
|
39
|
+
| Batch all hunks in one `edit_file` | Chain same-file `edit_file` calls |
|
|
40
|
+
| Carry `revision` → `base_revision` | Full-file rewrite for local changes |
|
|
41
|
+
| `verify` before reread | `bulk_replace` for block rewrites |
|
|
42
|
+
| `post_edit` checksum for follow-up | — |
|
|
48
43
|
|
|
49
44
|
|
|
50
45
|
## hex-graph — Code Analysis
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levnikolaevich/hex-line-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"mcpName": "io.github.levnikolaevich/hex-line-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 10 tools: read, edit, write, grep, outline, verify, directory_tree, file_info, changes, bulk_replace.",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
"homepage": "https://github.com/levnikolaevich/claude-code-skills/tree/master/mcp/hex-line-mcp",
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"eslint": "^10.1.0"
|
|
76
|
+
"eslint": "^10.1.0",
|
|
77
|
+
"typescript": "^6.0.2"
|
|
77
78
|
}
|
|
78
79
|
}
|