@levnikolaevich/hex-line-mcp 1.3.3 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hook.mjs +428 -0
- package/dist/server.mjs +2645 -0
- package/package.json +8 -8
- package/benchmark/atomic.mjs +0 -502
- package/benchmark/graph.mjs +0 -80
- package/benchmark/index.mjs +0 -144
- package/benchmark/workflows.mjs +0 -350
- package/hook.mjs +0 -466
- package/lib/benchmark-helpers.mjs +0 -541
- package/lib/bulk-replace.mjs +0 -65
- package/lib/changes.mjs +0 -176
- package/lib/coerce.mjs +0 -1
- package/lib/edit.mjs +0 -534
- package/lib/format.mjs +0 -138
- package/lib/graph-enrich.mjs +0 -226
- package/lib/hash.mjs +0 -1
- package/lib/info.mjs +0 -91
- package/lib/normalize.mjs +0 -1
- package/lib/outline.mjs +0 -145
- package/lib/read.mjs +0 -138
- package/lib/revisions.mjs +0 -238
- package/lib/search.mjs +0 -268
- package/lib/security.mjs +0 -112
- package/lib/setup.mjs +0 -275
- package/lib/tree.mjs +0 -236
- package/lib/update-check.mjs +0 -1
- package/lib/verify.mjs +0 -70
- package/server.mjs +0 -375
package/lib/graph-enrich.mjs
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Graph enrichment for hex-line tools.
|
|
3
|
-
*
|
|
4
|
-
* Reads .codegraph/index.db (created by hex-graph-mcp) in readonly mode via a
|
|
5
|
-
* small explicit compatibility contract:
|
|
6
|
-
* - hex_line_contract
|
|
7
|
-
* - hex_line_symbol_annotations
|
|
8
|
-
* - hex_line_call_edges
|
|
9
|
-
*
|
|
10
|
-
* Graceful fallback: if better-sqlite3, contract views, or DB are missing,
|
|
11
|
-
* enrichment is disabled silently for that project.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
|
-
import { join, dirname, relative } from "node:path";
|
|
16
|
-
import { createRequire } from "node:module";
|
|
17
|
-
|
|
18
|
-
const HEX_LINE_CONTRACT_VERSION = 1;
|
|
19
|
-
const _dbs = new Map();
|
|
20
|
-
let _driverUnavailable = false;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get readonly graph DB for a project root.
|
|
24
|
-
* Returns null if DB missing or contract unavailable.
|
|
25
|
-
* @param {string} filePath - any file path inside the project
|
|
26
|
-
* @returns {object|null} better-sqlite3 Database instance or null
|
|
27
|
-
*/
|
|
28
|
-
export function getGraphDB(filePath) {
|
|
29
|
-
if (_driverUnavailable) return null;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const projectRoot = findProjectRoot(filePath);
|
|
33
|
-
if (!projectRoot) return null;
|
|
34
|
-
|
|
35
|
-
const dbPath = join(projectRoot, ".codegraph", "index.db");
|
|
36
|
-
if (!existsSync(dbPath)) return null;
|
|
37
|
-
if (_dbs.has(dbPath)) return _dbs.get(dbPath);
|
|
38
|
-
|
|
39
|
-
const require = createRequire(import.meta.url);
|
|
40
|
-
const Database = require("better-sqlite3");
|
|
41
|
-
const db = new Database(dbPath, { readonly: true });
|
|
42
|
-
if (!validateHexLineContract(db)) {
|
|
43
|
-
db.close();
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
_dbs.set(dbPath, db);
|
|
47
|
-
return db;
|
|
48
|
-
} catch {
|
|
49
|
-
_driverUnavailable = true;
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Test helper: close cached DB handles so each test can start clean.
|
|
56
|
-
*/
|
|
57
|
-
export function _resetGraphDBCache() {
|
|
58
|
-
for (const db of _dbs.values()) {
|
|
59
|
-
try { db.close(); } catch { /* ignore */ }
|
|
60
|
-
}
|
|
61
|
-
_dbs.clear();
|
|
62
|
-
_driverUnavailable = false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function validateHexLineContract(db) {
|
|
66
|
-
try {
|
|
67
|
-
const contract = db.prepare("SELECT contract_version FROM hex_line_contract LIMIT 1").get();
|
|
68
|
-
if (!contract || contract.contract_version !== HEX_LINE_CONTRACT_VERSION) return false;
|
|
69
|
-
db.prepare("SELECT node_id, file, line_start, line_end, display_name, kind, callees, callers FROM hex_line_symbol_annotations LIMIT 1").all();
|
|
70
|
-
db.prepare("SELECT source_id, target_id, source_file, source_line, source_display_name, target_file, target_line, target_display_name FROM hex_line_call_edges LIMIT 1").all();
|
|
71
|
-
return true;
|
|
72
|
-
} catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get [N↓ M↑] annotation for a symbol.
|
|
79
|
-
* @param {object} db - better-sqlite3 instance
|
|
80
|
-
* @param {string} file - relative file path
|
|
81
|
-
* @param {string} name - symbol name
|
|
82
|
-
* @returns {string|null} e.g. "[5↓ 3↑]" or null
|
|
83
|
-
*/
|
|
84
|
-
export function symbolAnnotation(db, file, name) {
|
|
85
|
-
try {
|
|
86
|
-
const node = db.prepare(
|
|
87
|
-
"SELECT callees, callers FROM hex_line_symbol_annotations WHERE file = ? AND name = ? LIMIT 1"
|
|
88
|
-
).get(file, name);
|
|
89
|
-
if (!node) return null;
|
|
90
|
-
|
|
91
|
-
if (node.callees === 0 && node.callers === 0) return null;
|
|
92
|
-
return `[${node.callees}\u2193 ${node.callers}\u2191]`;
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get all symbol annotations for a file (for read_file Graph: header).
|
|
100
|
-
* @param {object} db
|
|
101
|
-
* @param {string} file - relative file path
|
|
102
|
-
* @returns {Array<{name, kind, callees, callers}>}
|
|
103
|
-
*/
|
|
104
|
-
export function fileAnnotations(db, file) {
|
|
105
|
-
try {
|
|
106
|
-
const nodes = db.prepare(
|
|
107
|
-
`SELECT display_name, kind, callees, callers
|
|
108
|
-
FROM hex_line_symbol_annotations
|
|
109
|
-
WHERE file = ?
|
|
110
|
-
ORDER BY line_start`
|
|
111
|
-
).all(file);
|
|
112
|
-
|
|
113
|
-
return nodes.map((node) => ({
|
|
114
|
-
name: node.display_name,
|
|
115
|
-
kind: node.kind,
|
|
116
|
-
callees: node.callees,
|
|
117
|
-
callers: node.callers,
|
|
118
|
-
}));
|
|
119
|
-
} catch {
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Call impact: callers affected by changes in given line range.
|
|
126
|
-
* @param {object} db
|
|
127
|
-
* @param {string} file - relative file path
|
|
128
|
-
* @param {number} startLine
|
|
129
|
-
* @param {number} endLine
|
|
130
|
-
* @returns {Array<{name, file, line}>} affected symbols (max 10)
|
|
131
|
-
*/
|
|
132
|
-
export function callImpact(db, file, startLine, endLine) {
|
|
133
|
-
try {
|
|
134
|
-
const modified = db.prepare(
|
|
135
|
-
`SELECT node_id
|
|
136
|
-
FROM hex_line_symbol_annotations
|
|
137
|
-
WHERE file = ?
|
|
138
|
-
AND line_start <= ?
|
|
139
|
-
AND line_end >= ?`
|
|
140
|
-
).all(file, endLine, startLine);
|
|
141
|
-
|
|
142
|
-
if (modified.length === 0) return [];
|
|
143
|
-
|
|
144
|
-
const affected = [];
|
|
145
|
-
const seen = new Set();
|
|
146
|
-
|
|
147
|
-
for (const node of modified) {
|
|
148
|
-
const dependents = db.prepare(
|
|
149
|
-
`SELECT source_display_name AS name, source_file AS file, source_line AS line
|
|
150
|
-
FROM hex_line_call_edges
|
|
151
|
-
WHERE target_id = ?`
|
|
152
|
-
).all(node.node_id);
|
|
153
|
-
|
|
154
|
-
for (const dep of dependents) {
|
|
155
|
-
const key = `${dep.file}:${dep.name}`;
|
|
156
|
-
if (!seen.has(key) && dep.file !== file) {
|
|
157
|
-
seen.add(key);
|
|
158
|
-
affected.push({ name: dep.name, file: dep.file, line: dep.line });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return affected.slice(0, 10);
|
|
164
|
-
} catch {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Get symbol kind + annotation for a grep match.
|
|
171
|
-
* @param {object} db
|
|
172
|
-
* @param {string} file - relative file path
|
|
173
|
-
* @param {number} line - line number
|
|
174
|
-
* @returns {string|null} e.g. "[fn 5↓ 3↑]" or null
|
|
175
|
-
*/
|
|
176
|
-
export function matchAnnotation(db, file, line) {
|
|
177
|
-
try {
|
|
178
|
-
const node = db.prepare(
|
|
179
|
-
`SELECT display_name, kind, callees, callers
|
|
180
|
-
FROM hex_line_symbol_annotations
|
|
181
|
-
WHERE file = ? AND line_start <= ? AND line_end >= ?
|
|
182
|
-
ORDER BY line_start DESC
|
|
183
|
-
LIMIT 1`
|
|
184
|
-
).get(file, line, line);
|
|
185
|
-
if (!node) return null;
|
|
186
|
-
|
|
187
|
-
const kindShort = { function: "fn", class: "cls", method: "mtd", variable: "var" }[node.kind] || node.kind;
|
|
188
|
-
if (node.callees === 0 && node.callers === 0) return `[${kindShort}]`;
|
|
189
|
-
return `[${kindShort} ${node.callees}\u2193 ${node.callers}\u2191]`;
|
|
190
|
-
} catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Get relative path from project root (matching DB paths).
|
|
197
|
-
* @param {string} filePath - absolute file path
|
|
198
|
-
* @returns {string|null} relative path with forward slashes, or null
|
|
199
|
-
*/
|
|
200
|
-
export function getRelativePath(filePath) {
|
|
201
|
-
const root = findProjectRoot(filePath);
|
|
202
|
-
if (!root) return null;
|
|
203
|
-
return relative(root, filePath).replace(/\\/g, "/");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// --- Helpers ---
|
|
207
|
-
|
|
208
|
-
function findProjectRoot(filePath) {
|
|
209
|
-
// First pass: look for .codegraph/index.db (strongest signal)
|
|
210
|
-
let dir = dirname(filePath);
|
|
211
|
-
for (let i = 0; i < 10; i++) {
|
|
212
|
-
if (existsSync(join(dir, ".codegraph", "index.db"))) return dir;
|
|
213
|
-
const parent = dirname(dir);
|
|
214
|
-
if (parent === dir) break;
|
|
215
|
-
dir = parent;
|
|
216
|
-
}
|
|
217
|
-
// Second pass: fallback to .git
|
|
218
|
-
dir = dirname(filePath);
|
|
219
|
-
for (let i = 0; i < 10; i++) {
|
|
220
|
-
if (existsSync(join(dir, ".git"))) return dir;
|
|
221
|
-
const parent = dirname(dir);
|
|
222
|
-
if (parent === dir) break;
|
|
223
|
-
dir = parent;
|
|
224
|
-
}
|
|
225
|
-
return null;
|
|
226
|
-
}
|
package/lib/hash.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "@levnikolaevich/hex-common/text-protocol/hash";
|
package/lib/info.mjs
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File metadata without reading content.
|
|
3
|
-
*
|
|
4
|
-
* Returns: size, line count, modification time, type, binary detection.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { statSync, openSync, readSync, closeSync } from "node:fs";
|
|
8
|
-
import { resolve, isAbsolute, extname, basename } from "node:path";
|
|
9
|
-
import { normalizePath } from "./security.mjs";
|
|
10
|
-
import { formatSize, relativeTime, countFileLines } from "./format.mjs";
|
|
11
|
-
|
|
12
|
-
const MAX_LINE_COUNT_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
13
|
-
|
|
14
|
-
const EXT_NAMES = {
|
|
15
|
-
".ts": "TypeScript source", ".tsx": "TypeScript JSX source",
|
|
16
|
-
".js": "JavaScript source", ".jsx": "JavaScript JSX source",
|
|
17
|
-
".mjs": "JavaScript ESM source", ".cjs": "JavaScript CJS source",
|
|
18
|
-
".py": "Python source", ".rb": "Ruby source", ".rs": "Rust source",
|
|
19
|
-
".go": "Go source", ".java": "Java source", ".kt": "Kotlin source",
|
|
20
|
-
".swift": "Swift source", ".c": "C source", ".cpp": "C++ source",
|
|
21
|
-
".h": "C/C++ header", ".cs": "C# source", ".php": "PHP source",
|
|
22
|
-
".sh": "Shell script", ".bash": "Bash script", ".zsh": "Zsh script",
|
|
23
|
-
".json": "JSON data", ".yaml": "YAML data", ".yml": "YAML data",
|
|
24
|
-
".toml": "TOML config", ".xml": "XML document", ".html": "HTML document",
|
|
25
|
-
".css": "CSS stylesheet", ".scss": "SCSS stylesheet", ".less": "LESS stylesheet",
|
|
26
|
-
".md": "Markdown document", ".txt": "Plain text", ".csv": "CSV data",
|
|
27
|
-
".sql": "SQL script", ".graphql": "GraphQL schema",
|
|
28
|
-
".png": "PNG image", ".jpg": "JPEG image", ".jpeg": "JPEG image",
|
|
29
|
-
".gif": "GIF image", ".svg": "SVG image", ".ico": "Icon file",
|
|
30
|
-
".pdf": "PDF document", ".zip": "ZIP archive", ".tar": "TAR archive",
|
|
31
|
-
".gz": "Gzip archive", ".wasm": "WebAssembly binary",
|
|
32
|
-
".lock": "Lock file", ".env": "Environment config",
|
|
33
|
-
".dockerfile": "Dockerfile", ".vue": "Vue component", ".svelte": "Svelte component",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function detectBinary(filePath, size) {
|
|
38
|
-
if (size === 0) return false;
|
|
39
|
-
const fd = openSync(filePath, "r");
|
|
40
|
-
const probe = Buffer.alloc(Math.min(size, 8192));
|
|
41
|
-
const bytesRead = readSync(fd, probe, 0, probe.length, 0);
|
|
42
|
-
closeSync(fd);
|
|
43
|
-
for (let i = 0; i < bytesRead; i++) {
|
|
44
|
-
if (probe[i] === 0) return true;
|
|
45
|
-
}
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get file metadata without reading full content.
|
|
51
|
-
* @param {string} filePath
|
|
52
|
-
* @returns {string} Formatted metadata
|
|
53
|
-
*/
|
|
54
|
-
export function fileInfo(filePath) {
|
|
55
|
-
if (!filePath) throw new Error("Empty file path");
|
|
56
|
-
const normalized = normalizePath(filePath);
|
|
57
|
-
const abs = isAbsolute(normalized) ? normalized : resolve(process.cwd(), normalized);
|
|
58
|
-
|
|
59
|
-
const stat = statSync(abs);
|
|
60
|
-
if (!stat.isFile()) throw new Error(`Not a regular file: ${abs}`);
|
|
61
|
-
|
|
62
|
-
const size = stat.size;
|
|
63
|
-
const mtime = stat.mtime;
|
|
64
|
-
const ext = extname(abs).toLowerCase();
|
|
65
|
-
const name = basename(abs);
|
|
66
|
-
|
|
67
|
-
// File type
|
|
68
|
-
let typeName = EXT_NAMES[ext] || (ext ? `${ext.slice(1).toUpperCase()} file` : "Unknown type");
|
|
69
|
-
if (name === "Dockerfile") typeName = "Dockerfile";
|
|
70
|
-
if (name === "Makefile") typeName = "Makefile";
|
|
71
|
-
|
|
72
|
-
// Binary detection
|
|
73
|
-
const isBinary = size > 0 ? detectBinary(abs, size) : false;
|
|
74
|
-
|
|
75
|
-
// Line count (only for non-binary, <=10MB)
|
|
76
|
-
const lineCount = !isBinary && size > 0 ? countFileLines(abs, size, MAX_LINE_COUNT_SIZE) : null;
|
|
77
|
-
|
|
78
|
-
// Format output
|
|
79
|
-
const sizeStr = lineCount !== null
|
|
80
|
-
? `Size: ${formatSize(size)} (${lineCount} lines)`
|
|
81
|
-
: `Size: ${formatSize(size)}`;
|
|
82
|
-
const timeStr = `Modified: ${mtime.toISOString().replace("T", " ").slice(0, 19)} (${relativeTime(mtime)})`;
|
|
83
|
-
|
|
84
|
-
return [
|
|
85
|
-
`File: ${normalized}`,
|
|
86
|
-
sizeStr,
|
|
87
|
-
timeStr,
|
|
88
|
-
`Type: ${typeName}`,
|
|
89
|
-
`Binary: ${isBinary ? "yes" : "no"}`,
|
|
90
|
-
].join("\n");
|
|
91
|
-
}
|
package/lib/normalize.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "@levnikolaevich/hex-common/output/normalize";
|
package/lib/outline.mjs
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AST-based file outline via tree-sitter WASM.
|
|
3
|
-
*
|
|
4
|
-
* Returns structural overview: functions, classes, interfaces with line ranges.
|
|
5
|
-
* 10-20 lines instead of 500 -> 95% token reduction.
|
|
6
|
-
* Output maps directly to read_file ranges.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { extname } from "node:path";
|
|
10
|
-
import { getParser, getLanguage } from "@levnikolaevich/hex-common/parser/tree-sitter";
|
|
11
|
-
import { grammarForExtension, supportedExtensions } from "@levnikolaevich/hex-common/parser/languages";
|
|
12
|
-
import { readUtf8Normalized } from "@levnikolaevich/hex-common/text/file-text";
|
|
13
|
-
import { validatePath, normalizePath } from "./security.mjs";
|
|
14
|
-
import { getGraphDB, symbolAnnotation, getRelativePath } from "./graph-enrich.mjs";
|
|
15
|
-
|
|
16
|
-
const LANG_CONFIGS = {
|
|
17
|
-
".js": { outline: ["function_declaration", "class_declaration", "variable_declaration", "export_statement", "lexical_declaration"], skip: ["import_statement"], recurse: ["class_body"] },
|
|
18
|
-
".mjs": { outline: ["function_declaration", "class_declaration", "variable_declaration", "export_statement", "lexical_declaration"], skip: ["import_statement"], recurse: ["class_body"] },
|
|
19
|
-
".jsx": { outline: ["function_declaration", "class_declaration", "variable_declaration", "export_statement", "lexical_declaration"], skip: ["import_statement"], recurse: ["class_body"] },
|
|
20
|
-
".ts": { outline: ["function_declaration", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration", "variable_declaration", "export_statement", "lexical_declaration"], skip: ["import_statement"], recurse: ["class_body"] },
|
|
21
|
-
".tsx": { outline: ["function_declaration", "class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration", "variable_declaration", "export_statement", "lexical_declaration"], skip: ["import_statement"], recurse: ["class_body"] },
|
|
22
|
-
".py": { outline: ["function_definition", "class_definition", "decorated_definition"], skip: ["import_statement", "import_from_statement"], recurse: ["class_body", "block"] },
|
|
23
|
-
".go": { outline: ["function_declaration", "method_declaration", "type_declaration"], skip: ["import_declaration"], recurse: [] },
|
|
24
|
-
".rs": { outline: ["function_item", "struct_item", "enum_item", "impl_item", "trait_item", "const_item", "static_item"], skip: ["use_declaration"], recurse: ["impl_item"] },
|
|
25
|
-
".java": { outline: ["class_declaration", "interface_declaration", "method_declaration", "enum_declaration"], skip: ["import_declaration"], recurse: ["class_body"] },
|
|
26
|
-
".c": { outline: ["function_definition", "struct_specifier", "enum_specifier", "type_definition"], skip: ["preproc_include"], recurse: [] },
|
|
27
|
-
".h": { outline: ["function_definition", "struct_specifier", "enum_specifier", "type_definition"], skip: ["preproc_include"], recurse: [] },
|
|
28
|
-
".cpp": { outline: ["function_definition", "class_specifier", "struct_specifier", "namespace_definition"], skip: ["preproc_include"], recurse: ["class_specifier"] },
|
|
29
|
-
".cs": { outline: ["class_declaration", "interface_declaration", "method_declaration", "namespace_declaration"], skip: ["using_directive"], recurse: ["class_body"] },
|
|
30
|
-
".rb": { outline: ["method", "class", "module"], skip: ["require", "require_relative"], recurse: ["class", "module"] },
|
|
31
|
-
".php": { outline: ["function_definition", "class_declaration", "method_declaration"], skip: ["namespace_use_declaration"], recurse: ["class_body"] },
|
|
32
|
-
".kt": { outline: ["function_declaration", "class_declaration", "object_declaration"], skip: ["import_header"], recurse: ["class_body"] },
|
|
33
|
-
".swift": { outline: ["function_declaration", "class_declaration", "struct_declaration", "protocol_declaration"], skip: ["import_declaration"], recurse: ["class_body"] },
|
|
34
|
-
".sh": { outline: ["function_definition"], skip: [], recurse: [] },
|
|
35
|
-
".bash": { outline: ["function_definition"], skip: [], recurse: [] },
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function extractOutline(rootNode, config, sourceLines) {
|
|
39
|
-
const entries = [];
|
|
40
|
-
const skipTypes = new Set(config.skip);
|
|
41
|
-
const outlineTypes = new Set(config.outline);
|
|
42
|
-
const recurseTypes = new Set(config.recurse);
|
|
43
|
-
|
|
44
|
-
function walk(node, depth) {
|
|
45
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
46
|
-
const child = node.child(i);
|
|
47
|
-
const type = child.type;
|
|
48
|
-
const startLine = child.startPosition.row + 1;
|
|
49
|
-
const endLine = child.endPosition.row + 1;
|
|
50
|
-
|
|
51
|
-
if (skipTypes.has(type)) continue;
|
|
52
|
-
|
|
53
|
-
if (outlineTypes.has(type)) {
|
|
54
|
-
const firstLine = sourceLines[startLine - 1] || "";
|
|
55
|
-
const nameMatch = firstLine.match(/(?:function|class|interface|type|enum|struct|def|fn|pub\s+fn)\s+(\w+)|(?:const|let|var|export\s+(?:const|let|var|function|class))\s+(\w+)/);
|
|
56
|
-
const name = nameMatch ? (nameMatch[1] || nameMatch[2]) : null;
|
|
57
|
-
|
|
58
|
-
entries.push({
|
|
59
|
-
start: startLine,
|
|
60
|
-
end: endLine,
|
|
61
|
-
depth,
|
|
62
|
-
text: firstLine.trim().slice(0, 120),
|
|
63
|
-
name,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
for (let j = 0; j < child.childCount; j++) {
|
|
67
|
-
const sub = child.child(j);
|
|
68
|
-
if (recurseTypes.has(sub.type)) walk(sub, depth + 1);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const skippedRanges = [];
|
|
75
|
-
for (let i = 0; i < rootNode.childCount; i++) {
|
|
76
|
-
const child = rootNode.child(i);
|
|
77
|
-
if (skipTypes.has(child.type)) {
|
|
78
|
-
skippedRanges.push({
|
|
79
|
-
start: child.startPosition.row + 1,
|
|
80
|
-
end: child.endPosition.row + 1,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
walk(rootNode, 0);
|
|
86
|
-
return { entries, skippedRanges };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function outlineFromContent(content, ext) {
|
|
90
|
-
const config = LANG_CONFIGS[ext];
|
|
91
|
-
const grammar = grammarForExtension(ext);
|
|
92
|
-
if (!config || !grammar) return null;
|
|
93
|
-
|
|
94
|
-
const sourceLines = content.split("\n");
|
|
95
|
-
|
|
96
|
-
let lang;
|
|
97
|
-
try {
|
|
98
|
-
lang = await getLanguage(grammar);
|
|
99
|
-
} catch (e) {
|
|
100
|
-
throw new Error(`Outline error: ${e.message}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const parser = await getParser();
|
|
104
|
-
parser.setLanguage(lang);
|
|
105
|
-
const tree = parser.parse(content);
|
|
106
|
-
return extractOutline(tree.rootNode, config, sourceLines);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function formatOutline(entries, skippedRanges, sourceLineCount, db, relFile) {
|
|
110
|
-
const lines = [];
|
|
111
|
-
|
|
112
|
-
if (skippedRanges.length > 0) {
|
|
113
|
-
const first = skippedRanges[0].start;
|
|
114
|
-
const last = skippedRanges[skippedRanges.length - 1].end;
|
|
115
|
-
const count = skippedRanges.reduce((sum, r) => sum + (r.end - r.start + 1), 0);
|
|
116
|
-
lines.push(`${first}-${last}: (${count} imports/declarations)`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const e of entries) {
|
|
120
|
-
const indent = " ".repeat(e.depth);
|
|
121
|
-
const anno = db ? symbolAnnotation(db, relFile, e.name) : null;
|
|
122
|
-
const suffix = anno ? ` ${anno}` : "";
|
|
123
|
-
lines.push(`${indent}${e.start}-${e.end}: ${e.text}${suffix}`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
lines.push("");
|
|
127
|
-
lines.push(`(${entries.length} symbols, ${sourceLineCount} source lines)`);
|
|
128
|
-
return lines.join("\n");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function fileOutline(filePath) {
|
|
132
|
-
filePath = normalizePath(filePath);
|
|
133
|
-
const real = validatePath(filePath);
|
|
134
|
-
const ext = extname(real).toLowerCase();
|
|
135
|
-
|
|
136
|
-
if (!LANG_CONFIGS[ext]) {
|
|
137
|
-
return `Outline unavailable for ${ext} files. Use read_file directly for non-code files (markdown, config, text). Supported code extensions: ${supportedExtensions().join(", ")}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const content = readUtf8Normalized(real);
|
|
141
|
-
const result = await outlineFromContent(content, ext);
|
|
142
|
-
const db = getGraphDB(real);
|
|
143
|
-
const relFile = db ? getRelativePath(real) : null;
|
|
144
|
-
return `File: ${filePath}\n\n${formatOutline(result.entries, result.skippedRanges, content.split("\n").length, db, relFile)}`;
|
|
145
|
-
}
|
package/lib/read.mjs
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File read with FNV-1a hash annotations and range checksums.
|
|
3
|
-
*
|
|
4
|
-
* Output format: {tag}.{lineNum}\t{content}
|
|
5
|
-
* Appends: checksum: {start}-{end}:{8hex}
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { statSync } from "node:fs";
|
|
9
|
-
import { fnv1a, lineTag, rangeChecksum } from "@levnikolaevich/hex-common/text-protocol/hash";
|
|
10
|
-
import { validatePath, normalizePath } from "./security.mjs";
|
|
11
|
-
import { getGraphDB, fileAnnotations, getRelativePath } from "./graph-enrich.mjs";
|
|
12
|
-
import { relativeTime, listDirectory, readText, MAX_OUTPUT_CHARS } from "./format.mjs";
|
|
13
|
-
import { rememberSnapshot } from "./revisions.mjs";
|
|
14
|
-
|
|
15
|
-
const DEFAULT_LIMIT = 2000;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Read a file with hash-annotated lines.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} filePath
|
|
21
|
-
* @param {object} opts - { offset, limit, plain, ranges }
|
|
22
|
-
* @returns {string} formatted output
|
|
23
|
-
*/
|
|
24
|
-
export function readFile(filePath, opts = {}) {
|
|
25
|
-
filePath = normalizePath(filePath);
|
|
26
|
-
const real = validatePath(filePath);
|
|
27
|
-
const stat = statSync(real);
|
|
28
|
-
|
|
29
|
-
// Directory listing fallback
|
|
30
|
-
if (stat.isDirectory()) {
|
|
31
|
-
const { text } = listDirectory(real, { metadata: true });
|
|
32
|
-
return `Directory: ${filePath}\n\n\`\`\`\n${text}\n\`\`\``;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const snapshot = rememberSnapshot(real, readText(real), { mtimeMs: stat.mtimeMs, size: stat.size });
|
|
36
|
-
const lines = snapshot.lines;
|
|
37
|
-
const total = lines.length;
|
|
38
|
-
|
|
39
|
-
// Determine ranges to read
|
|
40
|
-
let ranges;
|
|
41
|
-
if (opts.ranges && opts.ranges.length > 0) {
|
|
42
|
-
ranges = opts.ranges.map((r) => ({
|
|
43
|
-
start: Math.max(1, r.start || 1),
|
|
44
|
-
end: Math.min(total, r.end || total),
|
|
45
|
-
}));
|
|
46
|
-
} else {
|
|
47
|
-
const startLine = Math.max(1, opts.offset || 1);
|
|
48
|
-
const maxLines = (opts.limit && opts.limit > 0) ? opts.limit : DEFAULT_LIMIT;
|
|
49
|
-
ranges = [{ start: startLine, end: Math.min(total, startLine - 1 + maxLines) }];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const parts = [];
|
|
53
|
-
|
|
54
|
-
let cappedAtLine = 0;
|
|
55
|
-
|
|
56
|
-
for (const range of ranges) {
|
|
57
|
-
const selected = lines.slice(range.start - 1, range.end);
|
|
58
|
-
const lineHashes = [];
|
|
59
|
-
const formatted = [];
|
|
60
|
-
let charCount = 0;
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < selected.length; i++) {
|
|
63
|
-
const line = selected[i];
|
|
64
|
-
const num = range.start + i;
|
|
65
|
-
const hash32 = fnv1a(line);
|
|
66
|
-
const entry = opts.plain
|
|
67
|
-
? `${num}|${line}`
|
|
68
|
-
: `${lineTag(hash32)}.${num}\t${line}`;
|
|
69
|
-
|
|
70
|
-
if (charCount + entry.length > MAX_OUTPUT_CHARS && formatted.length > 0) {
|
|
71
|
-
cappedAtLine = num;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
lineHashes.push(hash32);
|
|
75
|
-
formatted.push(entry);
|
|
76
|
-
charCount += entry.length + 1;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Update range end to actual lines shown
|
|
80
|
-
const actualEnd = formatted.length > 0
|
|
81
|
-
? range.start + formatted.length - 1
|
|
82
|
-
: range.start;
|
|
83
|
-
range.end = actualEnd;
|
|
84
|
-
|
|
85
|
-
parts.push(formatted.join("\n"));
|
|
86
|
-
|
|
87
|
-
// Range checksum (only for lines actually shown)
|
|
88
|
-
const cs = rangeChecksum(lineHashes, range.start, actualEnd);
|
|
89
|
-
parts.push(`\nchecksum: ${cs}`);
|
|
90
|
-
|
|
91
|
-
if (cappedAtLine) break;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Header
|
|
95
|
-
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
96
|
-
const mtime = stat.mtime;
|
|
97
|
-
const ago = relativeTime(mtime);
|
|
98
|
-
let header = `File: ${filePath} (${total} lines, ${sizeKB}KB, ${ago})`;
|
|
99
|
-
if (ranges.length === 1) {
|
|
100
|
-
const r = ranges[0];
|
|
101
|
-
if (r.start > 1 || r.end < total) {
|
|
102
|
-
header += ` [showing ${r.start}-${r.end}]`;
|
|
103
|
-
}
|
|
104
|
-
if (r.end < total) {
|
|
105
|
-
header += ` (${total - r.end} more below)`;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Graph enrichment (optional — silent if no DB)
|
|
110
|
-
const db = getGraphDB(real);
|
|
111
|
-
const relFile = db ? getRelativePath(real) : null;
|
|
112
|
-
let graphLine = "";
|
|
113
|
-
if (db && relFile) {
|
|
114
|
-
const annos = fileAnnotations(db, relFile);
|
|
115
|
-
if (annos.length > 0) {
|
|
116
|
-
const items = annos.map(a => {
|
|
117
|
-
const counts = (a.callees || a.callers) ? ` ${a.callees}\u2193 ${a.callers}\u2191` : "";
|
|
118
|
-
return `${a.name} [${a.kind}${counts}]`;
|
|
119
|
-
});
|
|
120
|
-
graphLine = `\nGraph: ${items.join(" | ")}`;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let result =
|
|
125
|
-
`${header}${graphLine}\nrevision: ${snapshot.revision}\nfile: ${snapshot.fileChecksum}\n\n\`\`\`\n${parts.join("\n")}\n\`\`\``;
|
|
126
|
-
|
|
127
|
-
// Auto-hint for large files read from start without offset
|
|
128
|
-
if (total > 200 && (!opts.offset || opts.offset <= 1) && !cappedAtLine) {
|
|
129
|
-
result += `\n\n\u26A1 Tip: This file has ${total} lines. Use outline first, then read_file with offset/limit for 75% fewer tokens.`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Character cap notice
|
|
133
|
-
if (cappedAtLine) {
|
|
134
|
-
result += `\n\nOUTPUT_CAPPED at line ${cappedAtLine} (${MAX_OUTPUT_CHARS} char limit). Use offset=${cappedAtLine} to continue reading.`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return result;
|
|
138
|
-
}
|